Compare commits
231 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73db717a0c | ||
|
|
f1552c8354 | ||
|
|
fb87fc32f0 | ||
|
|
b04b0c670d | ||
|
|
b33845df8a | ||
|
|
12c9d8d8f8 | ||
|
|
fe97bbcbbf | ||
|
|
10533c3eba | ||
|
|
b4415c90f9 | ||
|
|
2970bbdf6f | ||
|
|
95a420534d | ||
|
|
be56a21574 | ||
|
|
3741a235fe | ||
|
|
523b6c4c2d | ||
|
|
c3b2300310 | ||
|
|
1c4a20d86f | ||
|
|
f5af475012 | ||
|
|
f149b74332 | ||
|
|
8cd51386cb | ||
|
|
fb2cca88df | ||
|
|
031d4dc85a | ||
|
|
bf66d1c0f5 | ||
|
|
0edfa88755 | ||
|
|
dba1f3bcbc | ||
|
|
e204cc8f12 | ||
|
|
9e3c60f43f | ||
|
|
b03dd5b6b2 | ||
|
|
2aa9886ebb | ||
|
|
225e52b8b9 | ||
|
|
0ae1f9906f | ||
|
|
a5918b50f8 | ||
|
|
fa0374ee61 | ||
|
|
5b1e1aee3d | ||
|
|
3ba11d08fb | ||
|
|
35d0f42d07 | ||
|
|
f08f222337 | ||
|
|
7c4ad278d5 | ||
|
|
d3d2930bd2 | ||
|
|
b571c20acc | ||
|
|
f6cc0302c4 | ||
|
|
d9abcf0c83 | ||
|
|
5346849439 | ||
|
|
9742b0c5b1 | ||
|
|
41cfe26898 | ||
|
|
05a254eee9 | ||
|
|
8ef53aa363 | ||
|
|
470abc25e4 | ||
|
|
7fe6c7e78b | ||
|
|
6c8a4f8d9f | ||
|
|
2a8c8c0cbb | ||
|
|
12bbe5459d | ||
|
|
e0d9e05fd4 | ||
|
|
8bea61cb99 | ||
|
|
d09666aba9 | ||
|
|
48e0ba1d3d | ||
|
|
fb72be41f4 | ||
|
|
ef1db6d089 | ||
|
|
9d4246a285 | ||
|
|
e0f5c39297 | ||
|
|
e4d353b105 | ||
|
|
0d665495b8 | ||
|
|
fdaf857885 | ||
|
|
987b41d2cb | ||
|
|
444080f320 | ||
|
|
b5007ba276 | ||
|
|
bf19d2b156 | ||
|
|
a78515e5bf | ||
|
|
581126c748 | ||
|
|
7b3e345a2a | ||
|
|
cc7cefd3ed | ||
|
|
de6528b7d8 | ||
|
|
9efdfe0de4 | ||
|
|
f936918278 | ||
|
|
00125577a9 | ||
|
|
083c47b0f0 | ||
|
|
8021686154 | ||
|
|
f55ca296da | ||
|
|
0d6bca3e5d | ||
|
|
be1ff61e4a | ||
|
|
e36d41b46f | ||
|
|
b4693dc1b3 | ||
|
|
ca6010789e | ||
|
|
f8a80ccf5f | ||
|
|
6461dccbdc | ||
|
|
b06f4f2574 | ||
|
|
7695e3a9c8 | ||
|
|
ae475b408f | ||
|
|
6d9d048287 | ||
|
|
09e4d101c5 | ||
|
|
36340caf4c | ||
|
|
9a80ab0700 | ||
|
|
339d972d8d | ||
|
|
6d1e1030bd | ||
|
|
8a3665ce55 | ||
|
|
8351f0beab | ||
|
|
c26808c59e | ||
|
|
50b1dfc746 | ||
|
|
2ec8ec2327 | ||
|
|
168480b0ee | ||
|
|
58f78afc2e | ||
|
|
d60020f5ac | ||
|
|
437efe5473 | ||
|
|
5737904cec | ||
|
|
431a8af08b | ||
|
|
be29c4a243 | ||
|
|
0a027fad9a | ||
|
|
e21a8e3cc2 | ||
|
|
4279d6514d | ||
|
|
457db5994b | ||
|
|
71c942d290 | ||
|
|
ee5bc8523b | ||
|
|
70f4112643 | ||
|
|
174f6b7ea8 | ||
|
|
b14fb6f3c4 | ||
|
|
d6e27e776b | ||
|
|
e6839b45e3 | ||
|
|
229134cf31 | ||
|
|
b552173185 | ||
|
|
399a1f8fee | ||
|
|
9293d9b0ed | ||
|
|
4a75952fc7 | ||
|
|
c19906eb87 | ||
|
|
00ea189074 | ||
|
|
c8e2061e2a | ||
|
|
78dc93721d | ||
|
|
33beecddda | ||
|
|
e99d93ca30 | ||
|
|
0cc54484a4 | ||
|
|
4df7d59de0 | ||
|
|
9a54da84dc | ||
|
|
83315f7999 | ||
|
|
eb5ebfd304 | ||
|
|
77c9124a3d | ||
|
|
317da76ced | ||
|
|
e8d8ced006 | ||
|
|
0321f9bddf | ||
|
|
5937892ebc | ||
|
|
e429bea2f4 | ||
|
|
d33ad5c616 | ||
|
|
3e0614a293 | ||
|
|
80e39212b8 | ||
|
|
d7a6ceb3a6 | ||
|
|
9d3a9e9d1e | ||
|
|
e95c789d4b | ||
|
|
a508161265 | ||
|
|
a935c80adc | ||
|
|
1d607bf13e | ||
|
|
ad57467c93 | ||
|
|
430eaefc3e | ||
|
|
3d60724651 | ||
|
|
31d9f663fc | ||
|
|
f324697c7c | ||
|
|
befae7e677 | ||
|
|
7a2c2587ba | ||
|
|
cf3f9d68b3 | ||
|
|
c99f7c554f | ||
|
|
f01c0945da | ||
|
|
1bcd0c4471 | ||
|
|
07f28e6ec7 | ||
|
|
7074f9f776 | ||
|
|
5029a9ad5a | ||
|
|
0bff30e954 | ||
|
|
601d37aa3d | ||
|
|
e2b1ca10c9 | ||
|
|
11942ed6dc | ||
|
|
f65914599b | ||
|
|
392c31f5fe | ||
|
|
f4ddaacb3c | ||
|
|
db64aef666 | ||
|
|
95c8bbd7fb | ||
|
|
67b4f37d53 | ||
|
|
427288c16b | ||
|
|
4202eee084 | ||
|
|
eca53bfab0 | ||
|
|
cd76d109f5 | ||
|
|
2787f9f19b | ||
|
|
4e3c6e52aa | ||
|
|
ae5ceedfe9 | ||
|
|
022e2eb1db | ||
|
|
2fe1ef4774 | ||
|
|
70afb2dd4d | ||
|
|
efa02e3f94 | ||
|
|
5eefd41059 | ||
|
|
6ca1b7c240 | ||
|
|
31191fd48c | ||
|
|
a6ed54ed48 | ||
|
|
95132b4fe1 | ||
|
|
df0c5d99e6 | ||
|
|
4b0724530e | ||
|
|
e5681a93f9 | ||
|
|
fb838a694e | ||
|
|
8c6f188767 | ||
|
|
55e15dc79e | ||
|
|
514073cb75 | ||
|
|
f6ece6522e | ||
|
|
817f0997cf | ||
|
|
1d651270d3 | ||
|
|
6ea58fac1a | ||
|
|
c7eb63d4ff | ||
|
|
4062365b19 | ||
|
|
7f48a927de | ||
|
|
4a740cc556 | ||
|
|
628b92f20b | ||
|
|
8f9616c230 | ||
|
|
df475dfde5 | ||
|
|
a835b56a51 | ||
|
|
5555a66638 | ||
|
|
dad355b932 | ||
|
|
90a097d45c | ||
|
|
dba01bc014 | ||
|
|
1d0a3ce998 | ||
|
|
076ad850bb | ||
|
|
868546abe4 | ||
|
|
6b421fabd1 | ||
|
|
ab3c9658bd | ||
|
|
8c7886d429 | ||
|
|
110a3e7e0f | ||
|
|
ee0b70f51b | ||
|
|
df48061a8f | ||
|
|
b1387d50f0 | ||
|
|
73dd218a95 | ||
|
|
ee4cedcc81 | ||
|
|
d1b2f9c6a9 | ||
|
|
40dee44978 | ||
|
|
db04c70d4a | ||
|
|
7401c6ac97 | ||
|
|
f7a1222cc3 | ||
|
|
069f09c43c | ||
|
|
35f821f6a1 | ||
|
|
67f60847ee | ||
|
|
65f6b142a5 |
13
.github/workflows/ci.py
vendored
13
.github/workflows/ci.py
vendored
@@ -5,6 +5,7 @@
|
||||
import io
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
@@ -35,15 +36,15 @@ def install_deps():
|
||||
run('sudo apt-get update')
|
||||
run('sudo apt-get install -y libgl1-mesa-dev libxi-dev libxrandr-dev libxinerama-dev'
|
||||
' libxcursor-dev libxcb-xkb-dev libdbus-1-dev libxkbcommon-dev libharfbuzz-dev'
|
||||
' libpng-dev libfontconfig-dev libxkbcommon-x11-dev libcanberra-dev')
|
||||
' libpng-dev liblcms2-dev libfontconfig-dev libxkbcommon-x11-dev libcanberra-dev')
|
||||
if is_bundle:
|
||||
install_bundle()
|
||||
else:
|
||||
run('pip install Pillow pygments')
|
||||
run('pip3 install Pillow pygments')
|
||||
|
||||
|
||||
def build_kitty():
|
||||
python = 'python3' if is_bundle else sys.executable
|
||||
python = shutil.which('python3') if is_bundle else sys.executable
|
||||
cmd = '{} setup.py build --verbose'.format(python)
|
||||
if os.environ.get('KITTY_SANITIZE') == '1':
|
||||
cmd += ' --debug --sanitize'
|
||||
@@ -71,7 +72,7 @@ def replace_in_file(path, src, dest):
|
||||
|
||||
def setup_bundle_env():
|
||||
global SW
|
||||
os.environ['SW'] = SW = '/Users/Shared/buildbot/sw/sw' if is_macos else os.path.join(os.environ['GITHUB_WORKSPACE'], 'sw')
|
||||
os.environ['SW'] = SW = '/Users/Shared/kitty-build/sw/sw' if is_macos else os.path.join(os.environ['GITHUB_WORKSPACE'], 'sw')
|
||||
os.environ['PKG_CONFIG_PATH'] = SW + '/lib/pkgconfig'
|
||||
if is_macos:
|
||||
os.environ['PATH'] = '{}:{}'.format('/usr/local/opt/sphinx-doc/bin', os.environ['PATH'])
|
||||
@@ -85,8 +86,8 @@ def install_bundle():
|
||||
cwd = os.getcwd()
|
||||
os.makedirs(SW)
|
||||
os.chdir(SW)
|
||||
with urlopen('https://download.calibre-ebook.com/travis/kitty/{}.tar.xz'.format(
|
||||
'osx' if is_macos else 'linux-64')) as f:
|
||||
with urlopen('https://download.calibre-ebook.com/ci/kitty/{}-64.tar.xz'.format(
|
||||
'macos' if is_macos else 'linux')) as f:
|
||||
data = f.read()
|
||||
with tarfile.open(fileobj=io.BytesIO(data), mode='r:xz') as tf:
|
||||
tf.extractall()
|
||||
|
||||
@@ -7,6 +7,7 @@ import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from contextlib import suppress
|
||||
|
||||
from bypy.constants import (
|
||||
LIBDIR, PREFIX, PYTHON, SRC as KITTY_DIR, ismacos, worker_env
|
||||
@@ -47,6 +48,16 @@ def build_c_extensions(ext_dir, args):
|
||||
shutil.copytree(
|
||||
KITTY_DIR, writeable_src_dir, symlinks=True,
|
||||
ignore=shutil.ignore_patterns('b', 'build', 'dist', '*_commands.json', '*.o'))
|
||||
|
||||
# Build the launcher as it is needed for the spawn test
|
||||
with suppress(FileNotFoundError):
|
||||
os.remove(os.path.join(writeable_src_dir, 'kitty', 'launcher', 'kitty'))
|
||||
if run(PYTHON, 'setup.py', 'build-launcher', cwd=writeable_src_dir) != 0:
|
||||
print('Building of kitty launcher failed', file=sys.stderr)
|
||||
os.chdir(KITTY_DIR)
|
||||
run_shell()
|
||||
raise SystemExit('Building of kitty launcher failed')
|
||||
|
||||
cmd = [PYTHON, 'setup.py']
|
||||
bundle = 'macos-freeze' if ismacos else 'linux-freeze'
|
||||
cmd.append(bundle)
|
||||
|
||||
@@ -26,7 +26,7 @@ kitty_constants = iv['kitty_constants']
|
||||
|
||||
def binary_includes():
|
||||
return tuple(map(get_dll_path, (
|
||||
'expat', 'sqlite3', 'ffi', 'z', 'lzma', 'png16',
|
||||
'expat', 'sqlite3', 'ffi', 'z', 'lzma', 'png16', 'lcms2',
|
||||
|
||||
# dont include freetype because fontconfig is closely coupled to it
|
||||
# and also distros often patch freetype
|
||||
@@ -35,7 +35,7 @@ def binary_includes():
|
||||
'harfbuzz', 'xkbcommon', 'xkbcommon-x11',
|
||||
'ncursesw', 'readline',
|
||||
))) + (
|
||||
get_dll_path('bz2', 2), get_dll_path('ssl', 3), get_dll_path('crypto', 3),
|
||||
get_dll_path('bz2', 2), get_dll_path('ssl', 2), get_dll_path('crypto', 2),
|
||||
get_dll_path('python' + py_ver, 2),
|
||||
)
|
||||
|
||||
|
||||
@@ -262,8 +262,9 @@ class Freeze(object):
|
||||
'z.1',
|
||||
'harfbuzz.0',
|
||||
'png16.16',
|
||||
'crypto.1.0.0',
|
||||
'ssl.1.0.0',
|
||||
'lcms2.2',
|
||||
'crypto.1.1',
|
||||
'ssl.1.1',
|
||||
):
|
||||
print('\nAdding', x)
|
||||
x = 'lib%s.dylib' % x
|
||||
|
||||
@@ -71,8 +71,8 @@
|
||||
{
|
||||
"name": "openssl",
|
||||
"unix": {
|
||||
"filename": "openssl-1.0.2u.tar.gz",
|
||||
"hash": "sha256:ecd0c6ffb493dd06707d38b14bb4d8c2288bb7033735606569d8f90f89669d16",
|
||||
"filename": "openssl-1.1.1g.tar.gz",
|
||||
"hash": "sha256:ddb04774f1e32f0c49751e21b67216ac87852ceb056b75209af2443400636d46",
|
||||
"urls": ["https://www.openssl.org/source/{filename}"]
|
||||
}
|
||||
},
|
||||
@@ -153,6 +153,15 @@
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"name": "lcms2",
|
||||
"unix": {
|
||||
"filename": "lcms2-2.11.tar.gz",
|
||||
"hash": "sha256:478c9c3938d7a91b1171de4616f8b04308a8676d73eadc19505b7ace41327f28",
|
||||
"urls": ["https://github.com/mm2/Little-CMS/archive/2.11/{filename}"]
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"name": "graphite",
|
||||
"os": "linux",
|
||||
@@ -179,7 +188,7 @@
|
||||
"unix": {
|
||||
"filename": "libiconv-1.16.tar.gz",
|
||||
"hash": "sha256:e6a1b1b589654277ee790cce3734f07876ac4ccfaecbee8afa0b649cf529cc04",
|
||||
"urls": ["http://ftp.gnu.org/pub/gnu/libiconv/{filename}"]
|
||||
"urls": ["https://ftp.gnu.org/pub/gnu/libiconv/{filename}"]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -187,13 +196,12 @@
|
||||
"name": "glib",
|
||||
"os": "linux",
|
||||
"unix": {
|
||||
"filename": "glib-2.57.1.tar.xz",
|
||||
"hash": "sha256:d029e7c4536835f1f103472f7510332c28d58b9b7d6cd0e9f45c2653e670d9b4",
|
||||
"urls": ["http://ftp.gnome.org/pub/GNOME/sources/glib/2.57/{filename}"]
|
||||
"filename": "glib-2.65.1.tar.xz",
|
||||
"hash": "sha256:bc63bf6c32713e0ee1dddc28e03f23b4a20c78bcb9a2c5b0f4eea41e46fb9cee",
|
||||
"urls": ["https://ftp.gnome.org/pub/GNOME/sources/glib/2.65/{filename}"]
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"name": "freetype",
|
||||
"os": "linux",
|
||||
|
||||
@@ -18,6 +18,7 @@ Run-time dependencies:
|
||||
* harfbuzz >= 1.5.0
|
||||
* zlib
|
||||
* libpng
|
||||
* liblcms2
|
||||
* freetype (not needed on macOS)
|
||||
* fontconfig (not needed on macOS)
|
||||
* libcanberra (not needed on macOS)
|
||||
@@ -63,10 +64,19 @@ This :file:`kitty.app` unlike the released one does not include its own copy of
|
||||
python and the other dependencies. So if you ever un-install/upgrade those dependencies
|
||||
you might have to rebuild the app.
|
||||
|
||||
Note that the released :file:`kitty.dmg` includes all dependencies, unlike the
|
||||
:file:`kitty.app` built above and is built automatically by using the :file:`kitty` branch of
|
||||
`build-calibre <https://github.com/kovidgoyal/build-calibre>`_ however, that
|
||||
is designed to run on Linux and is not for the faint of heart.
|
||||
.. note::
|
||||
The released :file:`kitty.dmg` includes all dependencies, unlike the
|
||||
:file:`kitty.app` built above and is built automatically by using the
|
||||
:file:`kitty` branch of `build-calibre
|
||||
<https://github.com/kovidgoyal/build-calibre>`_ however, that is designed to
|
||||
run on Linux and is not for the faint of heart.
|
||||
|
||||
|
||||
.. note::
|
||||
Apple disallows certain functionality, such as notifications for unsigned applications.
|
||||
If you need this functionality, you can try signing the built kitty.app with
|
||||
a self signed certificate, see for example, `here
|
||||
<https://stackoverflow.com/questions/27474751/how-can-i-codesign-an-app-without-being-in-the-mac-developer-program/27474942>`_.
|
||||
|
||||
|
||||
Note for Linux/macOS packagers
|
||||
|
||||
@@ -4,6 +4,111 @@ Changelog
|
||||
|kitty| is a feature full, cross-platform, *fast*, GPU based terminal emulator.
|
||||
To update |kitty|, :doc:`follow the instructions <binary>`.
|
||||
|
||||
0.19.1 [2020-10-06]
|
||||
-------------------
|
||||
|
||||
- hints kitten: Add an ``ip`` type for easy selection of IP addresses
|
||||
(:pull:`3009`)
|
||||
|
||||
- Fix a regression that caused a segfault when using
|
||||
:opt:`scrollback_pager_history_size` and it needs to be expanded (:iss:`3011`)
|
||||
|
||||
- Fix update available notifications repeating (:pull:`3006`)
|
||||
|
||||
|
||||
0.19.0 [2020-10-04]
|
||||
-------------------
|
||||
|
||||
- Add support for `hyperlinks from terminal programs
|
||||
<https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda>`_.
|
||||
Controlled via :opt:`allow_hyperlinks` (:iss:`68`)
|
||||
|
||||
- Add support for easily editing or downloading files over SSH sessions
|
||||
without the need for any special software, see :doc:`kittens/remote_file`
|
||||
|
||||
- A new :doc:`kittens/hyperlinked_grep` kitten to easily search files and open
|
||||
the results at the matched line by clicking on them.
|
||||
|
||||
- Allow customizing the :doc:`actions kitty takes <open_actions>` when clicking on URLs
|
||||
|
||||
- Improve rendering of borders when using minimal borders. Use less space and
|
||||
do not display a box around active windows
|
||||
|
||||
- Add a new extensible escape code to allow terminal programs to trigger
|
||||
desktop notifications. See :ref:`desktop_notifications` (:iss:`1474`)
|
||||
|
||||
- Implement special rendering for various characters from the set of "Symbols
|
||||
for Legacy Computing" from the Unicode 13 standard
|
||||
|
||||
- Unicode input kitten: Allow choosing symbols from the NERD font as well.
|
||||
These are mostly Private Use symbols not in any standard, however are common. (:iss:`2972`)
|
||||
|
||||
- Allow specifying border sizes in either pts or pixels. Change the default to
|
||||
0.5pt borders as this works best with the new minimal border style
|
||||
|
||||
- Add support for displaying correct colors with non-sRGB PNG files (Adds a
|
||||
dependency on liblcms2)
|
||||
|
||||
- hints kitten: Add a new :option:`kitty +kitten hints --type` of ``hyperlink`` useful
|
||||
for activating hyperlinks using just the keyboard
|
||||
|
||||
- Allow tracking focus change events in watchers (:iss:`2918`)
|
||||
|
||||
- Allow specifying watchers in session files and via a command line argument
|
||||
(:iss:`2933`)
|
||||
|
||||
- Add a setting :opt:`tab_activity_symbol` to show a symbol in the tab title
|
||||
if one of the windows has some activity after it was last focused
|
||||
(:iss:`2515`)
|
||||
|
||||
- macOS: Switch to using the User Notifications framework for notifications.
|
||||
The current notifications framework has been deprecated in Big Sur. The new
|
||||
framework only allows notifications from signed and notarized applications,
|
||||
so people using kitty from homebrew/source are out of luck. Complain to
|
||||
Apple.
|
||||
|
||||
- When in the main screen and a program grabs the mouse, do not use the scroll
|
||||
wheel events to scroll the scrollback buffer, instead send them to the
|
||||
program (:iss:`2939`)
|
||||
|
||||
- Fix unfocused windows in which a bell occurs not changing their border color
|
||||
to red until a relayout
|
||||
|
||||
- Linux: Fix automatic detection of bold/italic faces for fonts such as IBM
|
||||
Plex Mono that have the regular face with a full name that is the same as the
|
||||
family name (:iss:`2951`)
|
||||
|
||||
- Fix a regression that broke :opt:`kitten_alias` (:iss:`2952`)
|
||||
|
||||
- Fix a regression that broke the ``move_window_to_top`` action (:pull:`2953`)
|
||||
|
||||
- Fix a memory leak when changing font sizes
|
||||
|
||||
- Fix some lines in the scrollback buffer not being properly rendered after a
|
||||
window resize/font size change (:iss:`2619`)
|
||||
|
||||
|
||||
0.18.3 [2020-08-11]
|
||||
-------------------
|
||||
|
||||
- hints kitten: Allow customizing hint colors (:pull:`2894`)
|
||||
|
||||
- Wayland: Fix a typo in the previous release that broke reading mouse cursor size (:iss:`2895`)
|
||||
|
||||
- Fix a regression in the previous release that could cause an exception during
|
||||
startup in rare circumstances (:iss:`2896`)
|
||||
|
||||
- Fix image leaving behind a black rectangle when switch away and back to
|
||||
alternate screen (:iss:`2901`)
|
||||
|
||||
- Fix one pixel mis-alignment of rounded corners when either the cell
|
||||
dimensions or the thickness of the line is an odd number of pixels
|
||||
(:iss:`2907`)
|
||||
|
||||
- Fix a regression that broke specifying OS window size in the session file
|
||||
(:iss:`2908`)
|
||||
|
||||
|
||||
0.18.2 [2020-07-28]
|
||||
--------------------
|
||||
|
||||
@@ -25,7 +130,7 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
|
||||
- When a character from the Unicode Dingbat block is followed by a space, use
|
||||
the extra space to render a larger version of the character (:iss:`2850`)
|
||||
|
||||
- macOS: Fix the LC_TYPE env var being set to UTF-8 on systems in which the
|
||||
- macOS: Fix the LC_CTYPE env var being set to UTF-8 on systems in which the
|
||||
language and country code do not form a valid locale (:iss:`1233`)
|
||||
|
||||
- macOS: Fix :kbd:`cmd+plus` not changing font size (:iss:`2839`)
|
||||
|
||||
@@ -555,7 +555,7 @@ def process_shortcut_link(env: Any, refnode: Any, has_explicit_title: bool, titl
|
||||
|
||||
|
||||
def write_conf_docs(app: Any, all_kitten_names: Iterable[str]) -> None:
|
||||
app.add_lexer('conf', ConfLexer())
|
||||
app.add_lexer('conf', ConfLexer)
|
||||
app.add_object_type(
|
||||
'opt', 'opt',
|
||||
indextemplate="pair: %s; Config Setting",
|
||||
@@ -604,7 +604,7 @@ def setup(app: Any) -> None:
|
||||
write_cli_docs(kn)
|
||||
write_remote_control_protocol_docs()
|
||||
write_conf_docs(app, kn)
|
||||
app.add_lexer('session', SessionLexer())
|
||||
app.add_lexer('session', SessionLexer)
|
||||
app.add_role('link', link_role)
|
||||
app.add_role('iss', partial(num_role, 'issues'))
|
||||
app.add_role('pull', partial(num_role, 'pull'))
|
||||
|
||||
27
docs/faq.rst
27
docs/faq.rst
@@ -232,3 +232,30 @@ available. The manual way to figure it out is:
|
||||
This shows the hex value, ``13`` in this case.
|
||||
|
||||
3. Use ``\x(hexval)`` in your ``send_text`` command in kitty. So in this example, ``\x13``
|
||||
|
||||
|
||||
I am using tmux and have a problem
|
||||
--------------------------------------
|
||||
|
||||
First, terminal multiplexers are `a bad idea
|
||||
<https://github.com/kovidgoyal/kitty/issues/391#issuecomment-638320745>`_, do
|
||||
not use them, if at all possible. kitty contains features that do all of what
|
||||
tmux does, but better, with the exception of remote persistence (:iss:`391`).
|
||||
If you still want to use tmux, read on.
|
||||
|
||||
Image display will not work, see `tmux issue
|
||||
<https://github.com/tmux/tmux/issues/1391>`_.
|
||||
|
||||
If you are using tmux with multiple terminals or you start it under one
|
||||
terminal and then switch to another and these terminals have different TERM
|
||||
variables, tmux will break. You will need to restart it as tmux does not
|
||||
support multiple terminfo definitions.
|
||||
|
||||
Copying to clipboard via OSC 52 will not work, because tmux does not support
|
||||
the extended version of that protocol, you will need to add ``no-append`` to
|
||||
:opt:`clipboard_control` in kitty.conf.
|
||||
|
||||
If you use any of the advanced features that kitty has innovated, such as
|
||||
styled underlines, desktop notifications, extended keyboard support, etc.
|
||||
they may or may not work, depending on the whims of tmux's maintainer, your
|
||||
version of tmux, etc.
|
||||
|
||||
@@ -54,27 +54,34 @@ In C:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
struct ttysize ts;
|
||||
ioctl(0, TIOCGWINSZ, &ts);
|
||||
printf("number of columns: %i, number of rows: %i, screen width: %i, screen height: %i\n", sz.ws_col, sz.ws_row, sz.ws_xpixel, sz.ws_ypixel);
|
||||
#include <stdio.h>
|
||||
#include <sys/ioctl.h>
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
struct winsize sz;
|
||||
ioctl(0, TIOCGWINSZ, &sz);
|
||||
printf("number of rows: %i, number of columns: %i, screen width: %i, screen height: %i\n", sz.ws_row, sz.ws_col, sz.ws_xpixel, sz.ws_ypixel);
|
||||
return 0;
|
||||
}
|
||||
|
||||
In Python:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import array, fcntl, termios
|
||||
import array, fcntl, sys, termios
|
||||
buf = array.array('H', [0, 0, 0, 0])
|
||||
fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, buf)
|
||||
print('number of columns: {}, number of rows: {}, screen width: {}, screen height: {}'.format(*buf))
|
||||
print('number of rows: {}, number of columns: {}, screen width: {}, screen height: {}'.format(*buf))
|
||||
|
||||
Note that some terminals return ``0`` for the width and height values. Such
|
||||
terminals should be modified to return the correct values. Examples of
|
||||
terminals that return correct values: ``kitty, xterm``
|
||||
|
||||
You can also use the *CSI t* escape code to get the screen size. Send
|
||||
``<ESC>[14t`` to *stdout* and kitty will reply on *stdin* with
|
||||
``<ESC>[4;<height>;<width>t`` where *height* and *width* are the window size in
|
||||
pixels. This escape code is supported in many terminals, not just kitty.
|
||||
``<ESC>[14t`` to ``STDOUT`` and kitty will reply on ``STDIN`` with
|
||||
``<ESC>[4;<height>;<width>t`` where ``height`` and ``width`` are the window
|
||||
size in pixels. This escape code is supported in many terminals, not just
|
||||
kitty.
|
||||
|
||||
A minimal example
|
||||
------------------
|
||||
@@ -154,9 +161,10 @@ of transmitting paletted images.
|
||||
RGB and RGBA data
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In these formats the pixel data is stored directly as 3 or 4 bytes per pixel, respectively.
|
||||
When specifying images in this format, the image dimensions **must** be sent in the control data.
|
||||
For example::
|
||||
In these formats the pixel data is stored directly as 3 or 4 bytes per pixel,
|
||||
respectively. The colors in the data **must** be in the *sRGB color space*. When
|
||||
specifying images in this format, the image dimensions **must** be sent in the
|
||||
control data. For example::
|
||||
|
||||
<ESC>_Gf=24,s=10,v=20;<payload><ESC>\
|
||||
|
||||
|
||||
@@ -10,10 +10,12 @@ kitty - the fast, featureful, GPU based terminal emulator
|
||||
buttery smooth scrolling. Uses threaded rendering to minimize input latency.
|
||||
|
||||
* Supports all modern terminal features: :doc:`graphics (images)
|
||||
<graphics-protocol>`, unicode, true-color, OpenType ligatures, mouse
|
||||
protocol, focus tracking, `bracketed paste
|
||||
<https://cirw.in/blog/bracketed-paste>`_ and several :doc:`new terminal
|
||||
protocol extensions <protocol-extensions>`.
|
||||
<graphics-protocol>`, unicode, `true-color
|
||||
<https://gist.github.com/XVilka/8346728>`_,
|
||||
OpenType ligatures, mouse protocol, :doc:`hyperlinks <open_actions>`,
|
||||
focus tracking, `bracketed paste <https://cirw.in/blog/bracketed-paste>`_
|
||||
and several :doc:`new terminal protocol extensions
|
||||
<protocol-extensions>`.
|
||||
|
||||
* Supports tiling multiple terminal windows side by side in different
|
||||
:ref:`layouts <layouts>` without needing to use an extra program like tmux
|
||||
@@ -353,6 +355,15 @@ For example:
|
||||
focus
|
||||
launch emacs
|
||||
|
||||
# Add a watcher that will be called with various events that occur
|
||||
# on all subsequent windows. See the documentation of the launch command
|
||||
# for details on watchers.
|
||||
watcher /some/python/file.py
|
||||
launch mpd
|
||||
launch irssi
|
||||
# Remove the watcher for further windows
|
||||
watcher clear
|
||||
|
||||
|
||||
Mouse features
|
||||
-------------------
|
||||
|
||||
@@ -24,6 +24,9 @@ images and other types of documents directly in your terminal, even over SSH.
|
||||
`nnn <https://github.com/jarun/nnn/>`_
|
||||
another terminal file manager, with previews of file contents powered by kitty's graphics protocol.
|
||||
|
||||
`hunter <https://github.com/rabite0/hunter>`_
|
||||
another terminal file manager, with previews of file contents powered by kitty's graphics protocol.
|
||||
|
||||
`koneko <https://github.com/twenty5151/koneko>`_
|
||||
browse images from the pixiv artist community directly in kitty
|
||||
|
||||
@@ -109,3 +112,7 @@ Miscellaneous
|
||||
|
||||
`kitti3 <https://github.com/LandingEllipse/kitti3>`_
|
||||
allow using kitty as a drop-down terminal under the i3 window manager
|
||||
|
||||
`weechat-hints <https://github.com/GermainZ/kitty-weechat-hints>`_
|
||||
URL hints kitten for WeeChat that works without having to use WeeChat's
|
||||
raw-mode.
|
||||
|
||||
@@ -156,8 +156,8 @@ from ``kitty.fast_data_types``.
|
||||
|
||||
For example, to send a left click at position x: 2, y: 3 to the active window::
|
||||
|
||||
from kitty.fast_data_types import send_mouse_event, GLFW_MOUSE_BUTTON_LEFT, PRESS
|
||||
send_mouse_event(boss.active_window.screen, 2, 3, GLFW_MOUSE_BUTTON_LEFT, PRESS, 0)
|
||||
from kitty.fast_data_types import send_mouse_event, PRESS
|
||||
send_mouse_event(boss.active_window.screen, 2, 3, 1, PRESS, 0)
|
||||
|
||||
The function will only send the event if the program is receiving events of
|
||||
that type, and will return ``True`` if it sent the event, and ``False`` if not.
|
||||
@@ -196,3 +196,7 @@ Kittens created by kitty users
|
||||
`insert password <https://github.com/kovidgoyal/kitty/issues/1222>`_
|
||||
Insert a password from a CLI password manager, taking care to only do it at
|
||||
a password prompt.
|
||||
|
||||
`weechat-hints <https://github.com/GermainZ/kitty-weechat-hints>`_
|
||||
URL hints kitten for WeeChat that works without having to use WeeChat's
|
||||
raw-mode.
|
||||
|
||||
68
docs/kittens/hyperlinked_grep.rst
Normal file
68
docs/kittens/hyperlinked_grep.rst
Normal file
@@ -0,0 +1,68 @@
|
||||
Hyperlinked grep
|
||||
=================
|
||||
|
||||
This kitten allows you to search your files using `ripgrep
|
||||
<https://www.google.com/search?q=ripgrep>`_ and open the results
|
||||
directly in your favorite editor in the terminal, at the line containing
|
||||
the search result, simply by clicking on the result you want.
|
||||
|
||||
To set it up, first create :file:`~/.config/kitty/open-actions.conf` with the
|
||||
following contents:
|
||||
|
||||
.. code:: conf
|
||||
|
||||
# Open any file with a fragment in vim, fragments are generated
|
||||
# by the hyperlink_grep kitten and nothing else so far.
|
||||
protocol file
|
||||
fragment_matches [0-9]+
|
||||
action launch --type=overlay vim +${FRAGMENT} ${FILE_PATH}
|
||||
|
||||
# Open text files without fragments in the editor
|
||||
protocol file
|
||||
mime text/*
|
||||
action launch --type=overlay ${EDITOR} ${FILE_PATH}
|
||||
|
||||
|
||||
Now, run a search with::
|
||||
|
||||
kitty +kitten hyperlinked_grep something
|
||||
|
||||
Hold down the :kbd:`ctrl+shift` keys and click on any of the
|
||||
result lines, to open the file in vim at the matching line. If
|
||||
you use some editor other than vim, you should adjust the
|
||||
:file:`open-actions.conf` file accordingly.
|
||||
|
||||
Finally, add an alias to your shell's rc files to invoke the kitten as ``hg``::
|
||||
|
||||
alias hg='kitty +kitten hyperlinked_grep'
|
||||
|
||||
|
||||
You can now run searches with::
|
||||
|
||||
hg some-search-term
|
||||
|
||||
If you want to enable completion, for the kitten, you can delegate completion
|
||||
to rg. For that, instead of using an alias create a simple wrapper script named
|
||||
:file:`hg` somewhere in your ``PATH``:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
#!/bin/sh
|
||||
exec kitty +kitten hyperlinked_grep "$@"
|
||||
|
||||
Then, for example, for ZSH, add the following to :file:`.zshrc`::
|
||||
|
||||
compdef _rg hg
|
||||
|
||||
To learn more about kitty's powerful framework for customizing URL click
|
||||
actions, :doc:`see here <../open_actions>`.
|
||||
|
||||
Hopefully, someday this functionality will make it into some `upstream grep
|
||||
<https://github.com/BurntSushi/ripgrep/issues/665>`_
|
||||
program directly removing the need for this kitten.
|
||||
|
||||
|
||||
.. note::
|
||||
While you can pass any of ripgrep's comand line options to the kitten and
|
||||
they will be forwarded to rg, do not use options that change the output
|
||||
formatting as the kitten works by parsing the output from ripgrep.
|
||||
35
docs/kittens/remote_file.rst
Normal file
35
docs/kittens/remote_file.rst
Normal file
@@ -0,0 +1,35 @@
|
||||
Remote files
|
||||
==============
|
||||
|
||||
|kitty| has the ability to easily *Edit*, *Open* or *Download* files
|
||||
from a computer into which you are SSHed. In your SSH session run::
|
||||
|
||||
ls --hyperlink=auto
|
||||
|
||||
Then hold down :kbd:`ctrl+shift` and click the name of the file.
|
||||
|
||||
.. figure:: ../screenshots/remote_file.png
|
||||
:alt: Remote file actions
|
||||
:align: center
|
||||
:scale: 100%
|
||||
|
||||
Remote file actions
|
||||
|
||||
|kitty| will ask you what you want to do with the remote file. You can choose
|
||||
to *Edit* it in which case kitty will download it and open it locally in your
|
||||
``EDITOR``. As you make changes to the file, they are automatically transferred
|
||||
to the remote computer. Note that this happens without needing to install *any*
|
||||
special software on the server, beyond ``ls`` that supports hyperlinks.
|
||||
|
||||
.. note::
|
||||
Nested SSH sessions are not supported. The kitten will always try to copy
|
||||
remote files from the first SSH host. This is because there is no way for
|
||||
|kitty| to detect and follow a nested SSH session robustly.
|
||||
|
||||
.. note::
|
||||
If you have not setup automatic password-less SSH access, then, when
|
||||
editing starts you will be asked to enter your password just once,
|
||||
thereafter the SSH connection will be re-used.
|
||||
|
||||
Similarly, you can choose to save the file to the local computer or download
|
||||
and open it in its default file handler.
|
||||
@@ -74,6 +74,9 @@ functions for the events you are interested in, for example:
|
||||
def on_resize(boss, window, data):
|
||||
# Here data will contain old_geometry and new_geometry
|
||||
|
||||
def on_focus_change(boss, window, data):
|
||||
# Here data kill contain focused
|
||||
|
||||
def on_close(boss, window, data):
|
||||
# called when window is closed, typically when the program running in
|
||||
# it exits.
|
||||
|
||||
91
docs/open_actions.rst
Normal file
91
docs/open_actions.rst
Normal file
@@ -0,0 +1,91 @@
|
||||
Customizing the actions taken when clicking on links
|
||||
======================================================
|
||||
|
||||
|kitty| has support for `terminal hyperlinks <https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda>`_. These
|
||||
are generated by many terminal programs, such as ``ls``, ``gcc``, ``systemd``,
|
||||
``mdcat``, etc. You can customize exactly what happens when clicking on these hyperlinks
|
||||
in |kitty|.
|
||||
|
||||
You can tell kitty to take arbitrarily many, complex actions
|
||||
when a link is clicked. Let us illustrate with some examples, first. Create
|
||||
the file :file:`~/.config/kitty/open-actions.conf` with the following:
|
||||
|
||||
.. code:: conf
|
||||
|
||||
# Open any image in the full kitty window by clicking on it
|
||||
protocol file
|
||||
mime image/*
|
||||
action launch --type=overlay kitty +kitten icat --hold ${FILE_PATH}
|
||||
|
||||
Now, run ``ls --hyperlink=auto`` in kitty and click on the filename of an
|
||||
image, holding down :kbd:`ctrl+shift`. It will be opened over the current
|
||||
window. Press any key to close it.
|
||||
|
||||
Each entry in :file:`open-actions.conf` consists of one or more
|
||||
:ref:`matching_criteria`, such as ``protocol`` and ``mime`` and one or more
|
||||
``action`` entries. In the example above kitty uses the :doc:`launch <launch>`
|
||||
action which can be used to run external programs. Entries are separated by
|
||||
blank lines.
|
||||
|
||||
Actions are very powerful, anything that you can map to a key combination in
|
||||
`kitty.conf` can be used as an action. You can specify more than one action per
|
||||
entry if you like, for example:
|
||||
|
||||
|
||||
.. code:: conf
|
||||
|
||||
# Tail a log file (*.log) in a new OS Window and reduce its font size
|
||||
protocol file
|
||||
ext log
|
||||
action launch --title ${FILE} --type=os-window tail -f ${FILE_PATH}
|
||||
action change_font_size current -2
|
||||
|
||||
|
||||
In the action specification you can expand environment variables, as shown in
|
||||
the examples above. In addition to regular environment variables, there are
|
||||
some special variables, documented below:
|
||||
|
||||
``URL``
|
||||
The full URL being opened
|
||||
|
||||
``FILE_PATH``
|
||||
The path portion of the URL (unquoted)
|
||||
|
||||
``FILE``
|
||||
The file portion of the path of the URL (unquoted)
|
||||
|
||||
``FRAGMENT``
|
||||
The fragment (unquoted), if any of the URL or the empty string.
|
||||
|
||||
.. _matching_criteria:
|
||||
|
||||
Matching criteria
|
||||
------------------
|
||||
|
||||
An entry in :file:`open-actions.conf` must have one or more matching criteria.
|
||||
URLs that match all criteria for an entry will trigger that entries' actions.
|
||||
Processing stops at the first matching entry, so put more specific matching
|
||||
criteria at the start of the list. Entries in the file are separated by blank
|
||||
lines. The various available criteria are:
|
||||
|
||||
``protocol``
|
||||
A comma separated list of protocols, for example: ``http, https``. If
|
||||
absent, there is no constraint on protocol
|
||||
|
||||
``url``
|
||||
A regular expression that must match against the entire (unquoted) URL
|
||||
|
||||
``fragment_matches``
|
||||
A regular expression that must match against the fragment (part after #) in
|
||||
the URL
|
||||
|
||||
``mime``
|
||||
A comma separated list of MIME types, for example: ``text/*, image/*,
|
||||
application/pdf``
|
||||
|
||||
``ext``
|
||||
A comma separated list of file extensions, for example: ``jpeg, tar.gz``
|
||||
|
||||
``file``
|
||||
A shell glob pattern that must match the filename, for example:
|
||||
``image-??.png``
|
||||
@@ -228,3 +228,127 @@ no ill-effects in other terminal emulators.
|
||||
In case you're using software that can't be easily adapted to this
|
||||
protocol extension, it can be disabled by specifying ``no-append`` to the
|
||||
:opt:`clipboard_control` setting.
|
||||
|
||||
|
||||
.. _desktop_notifications:
|
||||
|
||||
|
||||
Desktop notifications
|
||||
---------------------------------
|
||||
|
||||
|kitty| implements an extensible escape code (OSC 99) to show desktop
|
||||
notifications. It is easy to use from shell scripts and fully extensible to
|
||||
show title and body. Clicking on the notification can optionally focus the
|
||||
window it came from, and/or send an escape code back to the application running
|
||||
in that window.
|
||||
|
||||
The design of the escape code is partially based on the discussion in
|
||||
the defunct
|
||||
`terminal-wg <https://gitlab.freedesktop.org/terminal-wg/specifications/-/issues/13>`_
|
||||
|
||||
The escape code has the form::
|
||||
|
||||
<OSC> 99 ; metadata ; payload <terminator>
|
||||
|
||||
Here ``<OSC>`` is :code:`<ESC>]` and ``<terminator>`` is
|
||||
:code:`<ESC><backslash>`. The metadata is a section of colon separated
|
||||
:code:`key=value` pairs. Every key must be a single character from the set
|
||||
:code:`a-zA-Z` and every value must be a word consisting of characters from
|
||||
the set :code:`a-zA-Z0-9-_/\+.,(){}[]*&^%$#@!`~`. The payload must be
|
||||
interpreted based on the metadata section. The two semi-colons *must* always be
|
||||
present even when no metadata is present.
|
||||
|
||||
Before going into details, lets see how one can display a simple, single line
|
||||
notification from a shell script::
|
||||
|
||||
printf '\x1b]99;;Hello world\x1b\\'
|
||||
|
||||
To show a message with a title and a body::
|
||||
|
||||
printf '\x1b]99;i=1:d=0;Hello world\x1b\\'
|
||||
printf '\x1b]99;i=1:d=1:p=body;This is cool\x1b\\'
|
||||
|
||||
The most important key in the metadata is the ``p`` key, it controls how the
|
||||
payload is interpreted. A value of ``title`` means the payload is setting the
|
||||
title for the notification. A value of ``body`` means it is setting the body,
|
||||
and so on, see the table below for full details.
|
||||
|
||||
The design of the escape code is fundamentally chunked, this is because
|
||||
different terminal emulators have different limits on how large a single escape
|
||||
code can be. Chunking is accomplished by the ``i`` and ``d`` keys. The ``i``
|
||||
key is the *notification id* which can be any string containing the characters
|
||||
``[a-zA-Z0-9_-+.]``. The ``d`` key stands for *done* and
|
||||
can only take the values ``0`` and ``1``. A value of ``0`` means the
|
||||
notification is not yet done and the terminal emulator should hold off
|
||||
displaying it. A value of ``1`` means the notification is done, and should be
|
||||
displayed. You can specify the title or body multiple times and the terminal
|
||||
emulator will concatenate them, thereby allowing arbitrarily long text
|
||||
(terminal emulators are free to impose a sensible limit to avoid
|
||||
Denial-of-Service attacks).
|
||||
|
||||
Both the ``title`` and ``body`` payloads must be either UTF-8 encoded plain
|
||||
text with no embedded escape codes, or UTF-8 text that is base64 encoded, in
|
||||
which case there must be an ``e=1`` key in the metadata to indicate the payload
|
||||
is base64 encoded.
|
||||
|
||||
When the user clicks the notification, a couple of things can happen, the
|
||||
terminal emulator can focus the window from which the notification came, and/or
|
||||
it can send back an escape code to the application indicating the notification
|
||||
was activated. This is controlled by the ``a`` key which takes a comma
|
||||
separated set of values, ``report`` and ``focus``. The value ``focus`` means
|
||||
focus the window from which the notification was issued and is the default.
|
||||
``report`` means send an escape code back to the application. The format of the
|
||||
returned escape code is::
|
||||
|
||||
<OSC> 99 ; i=identifier ; <terminator>
|
||||
|
||||
The value of ``identifier`` comes from the ``i`` key in the escape code sent by
|
||||
the application. If the application sends no identifier, then the terminal
|
||||
*must* use ``i=0``. Actions can be preceded by a negative sign to turn them
|
||||
off, so for example if you do not want any action, turn off the default
|
||||
``focus`` action with::
|
||||
|
||||
a=-focus
|
||||
|
||||
Complete specification of all the metadata keys is in the table below. If a
|
||||
terminal emulator encounters a key in the metadata it does not understand,
|
||||
the key *must* be ignored, to allow for future extensibility of this escape
|
||||
code. Similarly if values for known keys are unknown, the terminal emulator
|
||||
*should* either ignore the entire escape code or perform a best guess effort
|
||||
to display it based on what it does understand.
|
||||
|
||||
.. note::
|
||||
It is possible to extend this escape code to allow specifying an icon for
|
||||
the notification, however, given that some platforms, such as macOS, dont
|
||||
allow displaying custom icons on a notification, at all, it was decided to
|
||||
leave it out of the spec for the time being.
|
||||
|
||||
Similarly, features such as scheduled notifications could be added in future
|
||||
revisions.
|
||||
|
||||
|
||||
======= ==================== ========= =================
|
||||
Key Value Default Description
|
||||
======= ==================== ========= =================
|
||||
``a`` Comma separated list ``focus`` What action to perform when the
|
||||
of ``report``, notification is clicked
|
||||
``focus``, with
|
||||
optional leading
|
||||
``-``
|
||||
|
||||
``d`` ``0`` or ``1`` ``1`` Indicates if the notification is
|
||||
complete or not.
|
||||
|
||||
``e`` ``0`` or ``1`` ``0`` If set to ``1`` means the payload is base64 encoded UTF-8,
|
||||
otherwise it is plain UTF-8 text with no C0 control codes in it
|
||||
|
||||
``i`` ``[a-zA-Z0-9-_+.]`` ``0`` Identifier for the notification
|
||||
|
||||
``p`` One of ``title`` or ``title`` Whether the payload is the notification title or body. If a
|
||||
``body``. notification has no title, the body will be used as title.
|
||||
======= ==================== ========= =================
|
||||
|
||||
|
||||
.. note::
|
||||
|kitty| also supports the legacy OSC 9 protocol developed by iTerm2 for
|
||||
desktop notifications.
|
||||
|
||||
@@ -10,18 +10,18 @@ form::
|
||||
Where ``<ESC>`` is the byte ``0x1b``. The JSON object has the form::
|
||||
|
||||
{
|
||||
'cmd': "command name",
|
||||
'version': "kitty version",
|
||||
'no_response': Optional Boolean,
|
||||
'payload': <Optional JSON object>,
|
||||
"cmd": "command name",
|
||||
"version": <kitty version>,
|
||||
"no_response": Optional Boolean,
|
||||
"payload": <Optional JSON object>,
|
||||
}
|
||||
|
||||
The ``version`` above is a string of the form :code:`0.14.2`. If you are developing a
|
||||
The ``version`` above is an array of the form :code:`[0, 14, 2]`. If you are developing a
|
||||
standalone client, use the kitty version that you are developing against. Using
|
||||
a version greater than the version of the kitty instance you are talking to,
|
||||
will cause a failure.
|
||||
|
||||
Set ``no_response`` to True if you don't want a response from kitty.
|
||||
Set ``no_response`` to ``true`` if you don't want a response from kitty.
|
||||
|
||||
The optional payload is a JSON object that is specific to the actual command being sent.
|
||||
The fields in the object for every command are documented below.
|
||||
|
||||
BIN
docs/screenshots/remote_file.png
Normal file
BIN
docs/screenshots/remote_file.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
@@ -98,6 +98,18 @@ def parse_ucd() -> None:
|
||||
elif category.startswith('S'):
|
||||
all_symbols.add(codepoint)
|
||||
|
||||
with open('nerd-fonts-glyphs.txt') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
code, category, name = line.split(' ', 2)
|
||||
codepoint = int(code, 16)
|
||||
if name and codepoint not in name_map:
|
||||
name_map[codepoint] = name.upper()
|
||||
for word in name.lower().split():
|
||||
add_word(word, codepoint)
|
||||
|
||||
# Some common synonyms
|
||||
word_search_map['bee'] |= word_search_map['honeybee']
|
||||
word_search_map['lambda'] |= word_search_map['lamda']
|
||||
@@ -274,7 +286,9 @@ def category_test(
|
||||
comment: str,
|
||||
use_static: bool = False,
|
||||
extra_chars: Union[FrozenSet[int], Set[int]] = frozenset(),
|
||||
exclude: Union[Set[int], FrozenSet[int]] = frozenset()
|
||||
exclude: Union[Set[int], FrozenSet[int]] = frozenset(),
|
||||
least_check_return: Optional[str] = None,
|
||||
ascii_range: Optional[str] = None
|
||||
) -> None:
|
||||
static = 'static inline ' if use_static else ''
|
||||
chars: Set[int] = set()
|
||||
@@ -284,6 +298,11 @@ def category_test(
|
||||
chars -= exclude
|
||||
p(f'{static}bool\n{name}(char_type code) {{')
|
||||
p(f'\t// {comment} ({len(chars)} codepoints)' + ' {{' '{')
|
||||
if least_check_return is not None:
|
||||
least = min(chars)
|
||||
p(f'\tif (LIKELY(code < {least})) return {least_check_return};')
|
||||
if ascii_range is not None:
|
||||
p(f'\tif (LIKELY(0x20 <= code && code <= 0x7e)) return {ascii_range};')
|
||||
p('\tswitch(code) {')
|
||||
for spec in get_ranges(list(chars)):
|
||||
write_case(spec, p)
|
||||
@@ -337,11 +356,15 @@ def gen_ucd() -> None:
|
||||
{c for c in class_maps if c.startswith('M')},
|
||||
'M category (marks)',
|
||||
# See https://github.com/harfbuzz/harfbuzz/issues/169
|
||||
extra_chars=emoji_skin_tone_modifiers | {zwj}
|
||||
extra_chars=emoji_skin_tone_modifiers | {zwj},
|
||||
least_check_return='false'
|
||||
)
|
||||
category_test(
|
||||
'is_ignored_char', p, 'Cc Cf Cs'.split(),
|
||||
'Control characters and non-characters', extra_chars=non_characters, exclude={zwj})
|
||||
'Control characters and non-characters',
|
||||
extra_chars=non_characters, exclude={zwj},
|
||||
ascii_range='false'
|
||||
)
|
||||
category_test('is_word_char', p, {c for c in class_maps if c[0] in 'LN'}, 'L and N categories')
|
||||
category_test('is_CZ_category', p, cz, 'C and Z categories')
|
||||
category_test('is_P_category', p, {c for c in class_maps if c[0] == 'P'}, 'P category (punctuation)')
|
||||
@@ -421,7 +444,7 @@ def gen_names() -> None:
|
||||
p('}; // }}}\n')
|
||||
|
||||
# The trie
|
||||
p('typedef struct {{ uint32_t children_offset; uint32_t match_offset; }} word_trie;\n')
|
||||
p('typedef struct { uint32_t children_offset; uint32_t match_offset; } word_trie;\n')
|
||||
all_trie_nodes: List['TrieNode'] = [] # noqa
|
||||
|
||||
class TrieNode:
|
||||
@@ -482,6 +505,7 @@ def gen_wcwidth() -> None:
|
||||
|
||||
with create_header('kitty/wcwidth-std.h') as p:
|
||||
p('static int\nwcwidth_std(int32_t code) {')
|
||||
p('\tif (LIKELY(0x20 <= code && code <= 0x7e)) return 1;')
|
||||
p('\tswitch(code) {')
|
||||
|
||||
non_printing = class_maps['Cc'] | class_maps['Cf'] | class_maps['Cs']
|
||||
@@ -500,7 +524,7 @@ def gen_wcwidth() -> None:
|
||||
p('\t}')
|
||||
p('\treturn 1;\n}')
|
||||
|
||||
p('static bool\nis_emoji_presentation_base(uint32_t code) {')
|
||||
p('static inline bool\nis_emoji_presentation_base(uint32_t code) {')
|
||||
p('\tswitch(code) {')
|
||||
for spec in get_ranges(list(emoji_presentation_bases)):
|
||||
write_case(spec, p)
|
||||
|
||||
@@ -529,7 +529,6 @@ void _glfwPlatformGetMonitorWorkarea(_GLFWmonitor* monitor,
|
||||
*width = (int)frameRect.size.width;
|
||||
if (height)
|
||||
*height = (int)frameRect.size.height;
|
||||
|
||||
}
|
||||
|
||||
GLFWvidmode* _glfwPlatformGetVideoModes(_GLFWmonitor* monitor, int* count)
|
||||
|
||||
@@ -1595,10 +1595,10 @@ void _glfwPlatformSetWindowSizeLimits(_GLFWwindow* window,
|
||||
|
||||
void _glfwPlatformSetWindowAspectRatio(_GLFWwindow* window, int numer, int denom)
|
||||
{
|
||||
if (numer != GLFW_DONT_CARE && denom != GLFW_DONT_CARE)
|
||||
[window->ns.object setContentAspectRatio:NSMakeSize(numer, denom)];
|
||||
else
|
||||
if (numer == GLFW_DONT_CARE || denom == GLFW_DONT_CARE)
|
||||
[window->ns.object setResizeIncrements:NSMakeSize(1.0, 1.0)];
|
||||
else
|
||||
[window->ns.object setContentAspectRatio:NSMakeSize(numer, denom)];
|
||||
}
|
||||
|
||||
void _glfwPlatformSetWindowSizeIncrements(_GLFWwindow* window, int widthincr, int heightincr)
|
||||
|
||||
@@ -10,6 +10,7 @@ from typing import Callable, Dict, List, Optional, Tuple
|
||||
|
||||
_plat = sys.platform.lower()
|
||||
is_linux = 'linux' in _plat
|
||||
is_openbsd = 'openbsd' in _plat
|
||||
base = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
@@ -72,8 +73,9 @@ def init_env(env: Env, pkg_config: Callable, at_least_version: Callable, test_co
|
||||
|
||||
if module in ('x11', 'wayland'):
|
||||
ans.cflags.append('-pthread')
|
||||
ans.ldpaths.append('-pthread')
|
||||
ans.ldpaths.extend('-lrt -lm -ldl'.split())
|
||||
ans.ldpaths.extend('-pthread -lm'.split())
|
||||
if not is_openbsd:
|
||||
ans.ldpaths.extend('-lrt -ldl'.split())
|
||||
at_least_version('xkbcommon', 0, 5)
|
||||
|
||||
if module == 'x11':
|
||||
|
||||
2
glfw/glfw3.h
vendored
2
glfw/glfw3.h
vendored
@@ -1502,7 +1502,7 @@ typedef void (* GLFWwindowiconifyfun)(GLFWwindow*,int);
|
||||
* @endcode
|
||||
*
|
||||
* @param[in] window The window that was maximized or restored.
|
||||
* @param[in] iconified `true` if the window was maximized, or
|
||||
* @param[in] maximized `true` if the window was maximized, or
|
||||
* `false` if it was restored.
|
||||
*
|
||||
* @sa @ref window_maximize
|
||||
|
||||
2
glfw/ibus_glfw.c
vendored
2
glfw/ibus_glfw.c
vendored
@@ -269,7 +269,7 @@ setup_connection(_GLFWIBUSData *ibus) {
|
||||
ibus->conn = NULL;
|
||||
}
|
||||
debug("Connecting to IBUS daemon @ %s for IME input management\n", ibus->address);
|
||||
ibus->conn = glfw_dbus_connect_to(ibus->address, "Failed to connect to the IBUS daemon, with error", "ibus", false);
|
||||
ibus->conn = glfw_dbus_connect_to(ibus->address, "Failed to connect to the IBUS daemon, with error", "ibus", true);
|
||||
if (!ibus->conn) return false;
|
||||
free((void*)ibus->input_ctx_path); ibus->input_ctx_path = NULL;
|
||||
if (!glfw_dbus_call_method_with_reply(
|
||||
|
||||
2
glfw/null_joystick.c
vendored
2
glfw/null_joystick.c
vendored
@@ -33,7 +33,7 @@
|
||||
////// GLFW platform API //////
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
int _glfwPlatformInitJoysticks(void)
|
||||
bool _glfwPlatformInitJoysticks(void)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
6
glfw/null_monitor.c
vendored
6
glfw/null_monitor.c
vendored
@@ -70,8 +70,12 @@ void _glfwPlatformFreeMonitor(_GLFWmonitor* monitor)
|
||||
_glfwFreeGammaArrays(&monitor->null.ramp);
|
||||
}
|
||||
|
||||
void _glfwPlatformGetMonitorPos(_GLFWmonitor* monitor UNUSED, int* xpos UNUSED, int* ypos UNUSED)
|
||||
void _glfwPlatformGetMonitorPos(_GLFWmonitor* monitor UNUSED, int* xpos, int* ypos)
|
||||
{
|
||||
if (xpos)
|
||||
*xpos = 0;
|
||||
if (ypos)
|
||||
*ypos = 0;
|
||||
}
|
||||
|
||||
void _glfwPlatformGetMonitorContentScale(_GLFWmonitor* monitor UNUSED,
|
||||
|
||||
23
glfw/null_window.c
vendored
23
glfw/null_window.c
vendored
@@ -274,10 +274,25 @@ void _glfwPlatformGetWindowFrameSize(_GLFWwindow* window,
|
||||
{
|
||||
if (window->null.decorated && !window->monitor)
|
||||
{
|
||||
*left = 1;
|
||||
*top = 10;
|
||||
*right = 1;
|
||||
*bottom = 1;
|
||||
if (left)
|
||||
*left = 1;
|
||||
if (top)
|
||||
*top = 10;
|
||||
if (right)
|
||||
*right = 1;
|
||||
if (bottom)
|
||||
*bottom = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (left)
|
||||
*left = 0;
|
||||
if (top)
|
||||
*top = 0;
|
||||
if (right)
|
||||
*right = 0;
|
||||
if (bottom)
|
||||
*bottom = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
glfw/wl_cursors.c
vendored
2
glfw/wl_cursors.c
vendored
@@ -16,7 +16,7 @@ pixels_from_scale(int scale) {
|
||||
const char *env = getenv("XCURSOR_SIZE");
|
||||
if (env) {
|
||||
const int retval = atoi(env);
|
||||
if (retval > 0 && retval < 2048) factor = 32;
|
||||
if (retval > 0 && retval < 2048) factor = retval;
|
||||
}
|
||||
queried_env = true;
|
||||
}
|
||||
|
||||
2
glfw/wl_init.c
vendored
2
glfw/wl_init.c
vendored
@@ -802,6 +802,7 @@ void _glfwPlatformTerminate(void)
|
||||
glfw_xkb_release(&_glfw.wl.xkb);
|
||||
glfw_dbus_terminate(&_glfw.wl.dbus);
|
||||
|
||||
glfw_wlc_destroy();
|
||||
if (_glfw.wl.cursor.handle)
|
||||
{
|
||||
_glfw_dlclose(_glfw.wl.cursor.handle);
|
||||
@@ -810,7 +811,6 @@ void _glfwPlatformTerminate(void)
|
||||
|
||||
if (_glfw.wl.cursorSurface)
|
||||
wl_surface_destroy(_glfw.wl.cursorSurface);
|
||||
glfw_wlc_destroy();
|
||||
if (_glfw.wl.subcompositor)
|
||||
wl_subcompositor_destroy(_glfw.wl.subcompositor);
|
||||
if (_glfw.wl.compositor)
|
||||
|
||||
20
glfw/x11_window.c
vendored
20
glfw/x11_window.c
vendored
@@ -422,14 +422,16 @@ static char* convertLatin1toUTF8(const char* source)
|
||||
size_t size = 1;
|
||||
const char* sp;
|
||||
|
||||
for (sp = source; *sp; sp++)
|
||||
size += (*sp & 0x80) ? 2 : 1;
|
||||
if (source) {
|
||||
for (sp = source; *sp; sp++) size += (*sp & 0x80) ? 2 : 1;
|
||||
}
|
||||
|
||||
char* target = calloc(size, 1);
|
||||
char* tp = target;
|
||||
|
||||
for (sp = source; *sp; sp++)
|
||||
tp += encodeUTF8(tp, *sp);
|
||||
if (source) {
|
||||
for (sp = source; *sp; sp++) tp += encodeUTF8(tp, *sp);
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
@@ -771,7 +773,7 @@ static Atom writeTargetToProperty(const XSelectionRequestEvent* request)
|
||||
|
||||
if (j < formatCount)
|
||||
{
|
||||
XChangeProperty(_glfw.x11.display,
|
||||
if (selectionString) XChangeProperty(_glfw.x11.display,
|
||||
request->requestor,
|
||||
targets[i + 1],
|
||||
targets[i],
|
||||
@@ -823,7 +825,7 @@ static Atom writeTargetToProperty(const XSelectionRequestEvent* request)
|
||||
{
|
||||
// The requested target is one we support
|
||||
|
||||
XChangeProperty(_glfw.x11.display,
|
||||
if (selectionString) XChangeProperty(_glfw.x11.display,
|
||||
request->requestor,
|
||||
request->property,
|
||||
request->target,
|
||||
@@ -954,8 +956,10 @@ static const char* getSelectionString(Atom selection)
|
||||
(XPointer) ¬ification))
|
||||
{
|
||||
monotonic_t time = glfwGetTime();
|
||||
if (time - start > s_to_monotonic_t(2ll))
|
||||
if (time - start > s_to_monotonic_t(2ll)) {
|
||||
free(string);
|
||||
return "";
|
||||
}
|
||||
waitForX11Event(s_to_monotonic_t(2ll) - (time - start));
|
||||
}
|
||||
|
||||
@@ -1518,7 +1522,7 @@ static void processEvent(XEvent *event)
|
||||
count = 3;
|
||||
formats = (Atom*) event->xclient.data.l + 2;
|
||||
}
|
||||
char **atom_names = calloc(count, sizeof(char**));
|
||||
char **atom_names = calloc(count, sizeof(char*));
|
||||
if (atom_names) {
|
||||
get_atom_names(formats, count, atom_names);
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
See https://sw.kovidgoyal.net/kitty/graphics-protocol.html
|
||||
@@ -1 +0,0 @@
|
||||
See https://sw.kovidgoyal.net/kitty/key-encoding.html
|
||||
@@ -13,7 +13,8 @@ from kitty.constants import cache_dir
|
||||
from kitty.typing import BossType
|
||||
|
||||
from ..tui.handler import result_handler
|
||||
from ..tui.operations import alternate_screen, set_cursor_visible, styled
|
||||
from ..tui.operations import alternate_screen, styled
|
||||
from ..tui.utils import get_key_press
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import readline
|
||||
@@ -70,7 +71,7 @@ class HistoryCompleter:
|
||||
def option_text() -> str:
|
||||
return '''\
|
||||
--type -t
|
||||
choices=line,yesno
|
||||
choices=line,yesno,choices
|
||||
default=line
|
||||
Type of input. Defaults to asking for a line of text.
|
||||
|
||||
@@ -83,6 +84,16 @@ message is shown.
|
||||
--name -n
|
||||
The name for this question. Used to store history of previous answers which can
|
||||
be used for completions and via the browse history readline bindings.
|
||||
|
||||
|
||||
--choice -c
|
||||
type=list
|
||||
dest=choices
|
||||
A choice for the choices type. Every choice has the syntax: letter:text Where
|
||||
letter is the accelerator key and text is the corresponding text. There can be
|
||||
an optional color specification after the letter to indicate what color it should
|
||||
be.
|
||||
For example: y:Yes and n;red:No
|
||||
'''
|
||||
|
||||
|
||||
@@ -97,23 +108,34 @@ class Response(TypedDict):
|
||||
response: Optional[str]
|
||||
|
||||
|
||||
def yesno(cli_opts: AskCLIOptions, items: List[str]) -> Response:
|
||||
import tty
|
||||
def choice(cli_opts: AskCLIOptions, items: List[str]) -> Response:
|
||||
with alternate_screen():
|
||||
if cli_opts.message:
|
||||
print(styled(cli_opts.message, bold=True))
|
||||
print()
|
||||
print(' ', styled('Y', fg='green') + 'es', ' ', styled('N', fg='red') + 'o', set_cursor_visible(False))
|
||||
sys.stdout.flush()
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
try:
|
||||
response = sys.stdin.buffer.read(1)
|
||||
yes = response in (b'y', b'Y', b'\r', b'\n' b' ')
|
||||
return {'items': items, 'response': 'y' if yes else 'n'}
|
||||
finally:
|
||||
sys.stdout.write(set_cursor_visible(True))
|
||||
tty.setcbreak(sys.stdin.fileno())
|
||||
sys.stdout.flush()
|
||||
allowed = ''
|
||||
for choice in cli_opts.choices:
|
||||
color = 'green'
|
||||
letter, text = choice.split(':', maxsplit=1)
|
||||
if ';' in letter:
|
||||
letter, color = letter.split(';', maxsplit=1)
|
||||
letter = letter.lower()
|
||||
idx = text.lower().index(letter)
|
||||
allowed += letter
|
||||
print(text[:idx], styled(text[idx], fg=color), text[idx + 1:], sep='', end=' ')
|
||||
print()
|
||||
response = get_key_press(allowed, '')
|
||||
return {'items': items, 'response': response}
|
||||
|
||||
|
||||
def yesno(cli_opts: AskCLIOptions, items: List[str]) -> Response:
|
||||
with alternate_screen():
|
||||
if cli_opts.message:
|
||||
print(styled(cli_opts.message, bold=True))
|
||||
print()
|
||||
print(' ', styled('Y', fg='green') + 'es', ' ', styled('N', fg='red') + 'o')
|
||||
response = get_key_press('yn', 'n')
|
||||
return {'items': items, 'response': response}
|
||||
|
||||
|
||||
def main(args: List[str]) -> Response:
|
||||
@@ -132,6 +154,8 @@ def main(args: List[str]) -> Response:
|
||||
|
||||
if cli_opts.type == 'yesno':
|
||||
return yesno(cli_opts, items)
|
||||
if cli_opts.type == 'choices':
|
||||
return choice(cli_opts, items)
|
||||
|
||||
import readline as rl
|
||||
readline = rl
|
||||
|
||||
@@ -111,7 +111,7 @@ end:
|
||||
if (job_data[i] && job_data[i]->started) wait_for_thread(threads, i);
|
||||
}
|
||||
}
|
||||
for (i = 0; i < num_threads; i++) job_data[i] = free_job(job_data[i]);
|
||||
if (job_data) { for (i = 0; i < num_threads; i++) job_data[i] = free_job(job_data[i]); }
|
||||
free(job_data);
|
||||
free_threads(threads);
|
||||
return ret;
|
||||
@@ -130,7 +130,7 @@ run_search(Options *opts, GlobalData *global, const char * const *lines, const s
|
||||
ALLOC_VEC(text_t, chars, 8192 * 20);
|
||||
if (chars.data == NULL) return 1;
|
||||
ALLOC_VEC(Candidate, candidates, 8192);
|
||||
if (candidates.data == NULL) { FREE_VEC(chars) return 1; }
|
||||
if (candidates.data == NULL) { FREE_VEC(chars); return 1; }
|
||||
|
||||
for (size_t i = 0; i < num_lines; i++) {
|
||||
sz = sizes[i];
|
||||
|
||||
@@ -28,8 +28,9 @@
|
||||
#define ENSURE_SPACE(TYPE, vec, amt) \
|
||||
if (vec.size + amt >= vec.capacity) { \
|
||||
vec.capacity = MAX(vec.capacity * 2, vec.size + amt); \
|
||||
vec.data = (TYPE*)realloc(vec.data, sizeof(TYPE) * vec.capacity); \
|
||||
if (vec.data == NULL) { REPORT_OOM; ret = 1; break; } \
|
||||
void *temp = realloc(vec.data, sizeof(TYPE) * vec.capacity); \
|
||||
if (temp == NULL) { REPORT_OOM; ret = 1; free(vec.data); vec.data = NULL; vec.size = 0; vec.capacity = 0; break; } \
|
||||
else vec.data = temp; \
|
||||
}
|
||||
|
||||
#define NEXT(vec) (vec.data[vec.size])
|
||||
|
||||
@@ -45,12 +45,21 @@ ESCAPE = K['ESCAPE']
|
||||
|
||||
class Mark:
|
||||
|
||||
__slots__ = ('index', 'start', 'end', 'text', 'groupdict')
|
||||
__slots__ = ('index', 'start', 'end', 'text', 'is_hyperlink', 'group_id', 'groupdict')
|
||||
|
||||
def __init__(self, index: int, start: int, end: int, text: str, groupdict: Any):
|
||||
def __init__(
|
||||
self,
|
||||
index: int, start: int, end: int,
|
||||
text: str,
|
||||
groupdict: Any,
|
||||
is_hyperlink: bool = False,
|
||||
group_id: Optional[str] = None
|
||||
):
|
||||
self.index, self.start, self.end = index, start, end
|
||||
self.text = text
|
||||
self.groupdict = groupdict
|
||||
self.is_hyperlink = is_hyperlink
|
||||
self.group_id = group_id
|
||||
|
||||
|
||||
@lru_cache(maxsize=2048)
|
||||
@@ -72,7 +81,7 @@ def decode_hint(x: str, alphabet: str = DEFAULT_HINT_ALPHABET) -> int:
|
||||
return i
|
||||
|
||||
|
||||
def highlight_mark(m: Mark, text: str, current_input: str, alphabet: str) -> str:
|
||||
def highlight_mark(m: Mark, text: str, current_input: str, alphabet: str, colors: Dict[str, str]) -> str:
|
||||
hint = encode_hint(m.index, alphabet)
|
||||
if current_input and not hint.startswith(current_input):
|
||||
return faint(text)
|
||||
@@ -80,19 +89,19 @@ def highlight_mark(m: Mark, text: str, current_input: str, alphabet: str) -> str
|
||||
text = text[len(hint):]
|
||||
return styled(
|
||||
hint,
|
||||
fg='black',
|
||||
bg='green',
|
||||
fg=colors['foreground'],
|
||||
bg=colors['background'],
|
||||
bold=True
|
||||
) + styled(
|
||||
text, fg='gray', fg_intense=True, bold=True
|
||||
text, fg=colors['text'], fg_intense=True, bold=True
|
||||
)
|
||||
|
||||
|
||||
def render(text: str, current_input: str, all_marks: Sequence[Mark], ignore_mark_indices: Set[int], alphabet: str) -> str:
|
||||
def render(text: str, current_input: str, all_marks: Sequence[Mark], ignore_mark_indices: Set[int], alphabet: str, colors: Dict[str, str]) -> str:
|
||||
for mark in reversed(all_marks):
|
||||
if mark.index in ignore_mark_indices:
|
||||
continue
|
||||
mtext = highlight_mark(mark, text[mark.start:mark.end], current_input, alphabet)
|
||||
mtext = highlight_mark(mark, text[mark.start:mark.end], current_input, alphabet, colors)
|
||||
text = text[:mark.start] + mtext + text[mark.end:]
|
||||
|
||||
text = text.replace('\0', '')
|
||||
@@ -105,6 +114,9 @@ class Hints(Handler):
|
||||
def __init__(self, text: str, all_marks: Sequence[Mark], index_map: Dict[int, Mark], args: HintsCLIOptions):
|
||||
self.text, self.index_map = text, index_map
|
||||
self.alphabet = args.alphabet or DEFAULT_HINT_ALPHABET
|
||||
self.colors = {'foreground': args.hints_foreground_color,
|
||||
'background': args.hints_background_color,
|
||||
'text': args.hints_text_color}
|
||||
self.all_marks = all_marks
|
||||
self.ignore_mark_indices: Set[int] = set()
|
||||
self.args = args
|
||||
@@ -198,7 +210,7 @@ class Hints(Handler):
|
||||
|
||||
def draw_screen(self) -> None:
|
||||
if self.current_text is None:
|
||||
self.current_text = render(self.text, self.current_input, self.all_marks, self.ignore_mark_indices, self.alphabet)
|
||||
self.current_text = render(self.text, self.current_input, self.all_marks, self.ignore_mark_indices, self.alphabet, self.colors)
|
||||
self.cmd.clear_screen()
|
||||
self.write(self.current_text)
|
||||
|
||||
@@ -224,6 +236,11 @@ def postprocessor(func: PostprocessorFunc) -> PostprocessorFunc:
|
||||
return func
|
||||
|
||||
|
||||
class InvalidMatch(Exception):
|
||||
"""Raised when a match turns out to be invalid."""
|
||||
pass
|
||||
|
||||
|
||||
@postprocessor
|
||||
def url(text: str, s: int, e: int) -> Tuple[int, int]:
|
||||
if s > 4 and text[s - 5:s] == 'link:': # asciidoc URLs
|
||||
@@ -268,11 +285,29 @@ def quotes(text: str, s: int, e: int) -> Tuple[int, int]:
|
||||
return s, e
|
||||
|
||||
|
||||
@postprocessor
|
||||
def ip(text: str, s: int, e: int) -> Tuple[int, int]:
|
||||
from ipaddress import ip_address
|
||||
# Check validity of IPs (or raise InvalidMatch)
|
||||
ip = text[s:e]
|
||||
|
||||
try:
|
||||
ip_address(ip)
|
||||
except Exception:
|
||||
raise InvalidMatch("Invalid IP")
|
||||
|
||||
return s, e
|
||||
|
||||
|
||||
def mark(pattern: str, post_processors: Iterable[PostprocessorFunc], text: str, args: HintsCLIOptions) -> Generator[Mark, None, None]:
|
||||
pat = re.compile(pattern)
|
||||
for idx, (s, e, groupdict) in enumerate(regex_finditer(pat, args.minimum_match_length, text)):
|
||||
for func in post_processors:
|
||||
s, e = func(text, s, e)
|
||||
try:
|
||||
for func in post_processors:
|
||||
s, e = func(text, s, e)
|
||||
except InvalidMatch:
|
||||
continue
|
||||
|
||||
mark_text = text[s:e].replace('\n', '').replace('\0', '')
|
||||
yield Mark(idx, s, e, mark_text, groupdict)
|
||||
|
||||
@@ -313,6 +348,15 @@ def functions_for(args: HintsCLIOptions) -> Tuple[str, List[PostprocessorFunc]]:
|
||||
pattern = '(?m)^\\s*(.+)[\\s\0]*$'
|
||||
elif args.type == 'hash':
|
||||
pattern = '[0-9a-f]{7,128}'
|
||||
elif args.type == 'ip':
|
||||
pattern = (
|
||||
# # IPv4 with no validation
|
||||
r"((?:\d{1,3}\.){3}\d{1,3}"
|
||||
r"|"
|
||||
# # IPv6 with no validation
|
||||
r"(?:[a-fA-F0-9]{0,4}:){2,7}[a-fA-F0-9]{1,4})"
|
||||
)
|
||||
post_processors.append(ip)
|
||||
elif args.type == 'word':
|
||||
chars = args.word_characters
|
||||
if chars is None:
|
||||
@@ -366,13 +410,66 @@ def load_custom_processor(customize_processing: str) -> Any:
|
||||
return runpy.run_path(custom_path, run_name='__main__')
|
||||
|
||||
|
||||
def remove_sgr(text: str) -> str:
|
||||
return re.sub(r'\x1b\[.*?m', '', text)
|
||||
|
||||
|
||||
def process_hyperlinks(text: str) -> Tuple[str, Tuple[Mark, ...]]:
|
||||
hyperlinks: List[Mark] = []
|
||||
removed_size = idx = 0
|
||||
active_hyperlink_url: Optional[str] = None
|
||||
active_hyperlink_id: Optional[str] = None
|
||||
active_hyperlink_start_offset = 0
|
||||
|
||||
def add_hyperlink(end: int) -> None:
|
||||
nonlocal idx, active_hyperlink_url, active_hyperlink_id, active_hyperlink_start_offset
|
||||
assert active_hyperlink_url is not None
|
||||
hyperlinks.append(Mark(
|
||||
idx, active_hyperlink_start_offset, end,
|
||||
active_hyperlink_url,
|
||||
groupdict={},
|
||||
is_hyperlink=True, group_id=active_hyperlink_id
|
||||
))
|
||||
active_hyperlink_url = active_hyperlink_id = None
|
||||
active_hyperlink_start_offset = 0
|
||||
idx += 1
|
||||
|
||||
def process_hyperlink(m: 're.Match') -> str:
|
||||
nonlocal removed_size, active_hyperlink_url, active_hyperlink_id, active_hyperlink_start_offset
|
||||
raw = m.group()
|
||||
start = m.start() - removed_size
|
||||
removed_size += len(raw)
|
||||
if active_hyperlink_url is not None:
|
||||
add_hyperlink(start)
|
||||
raw = raw[4:-2]
|
||||
parts = raw.split(';', 1)
|
||||
if len(parts) == 2 and parts[1]:
|
||||
active_hyperlink_url = parts[1]
|
||||
active_hyperlink_start_offset = start
|
||||
if parts[0]:
|
||||
for entry in parts[0].split(':'):
|
||||
if entry.startswith('id=') and len(entry) > 3:
|
||||
active_hyperlink_id = entry[3:]
|
||||
break
|
||||
|
||||
return ''
|
||||
|
||||
text = re.sub(r'\x1b\]8.+?\x1b\\', process_hyperlink, text)
|
||||
if active_hyperlink_url is not None:
|
||||
add_hyperlink(len(text))
|
||||
return text, tuple(hyperlinks)
|
||||
|
||||
|
||||
def run(args: HintsCLIOptions, text: str, extra_cli_args: Sequence[str] = ()) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
text = parse_input(text)
|
||||
text = parse_input(remove_sgr(text))
|
||||
text, hyperlinks = process_hyperlinks(text)
|
||||
pattern, post_processors = functions_for(args)
|
||||
if args.type == 'linenum':
|
||||
args.customize_processing = '::linenum::'
|
||||
if args.customize_processing:
|
||||
if args.type == 'hyperlink':
|
||||
all_marks = hyperlinks
|
||||
elif args.customize_processing:
|
||||
m = load_custom_processor(args.customize_processing)
|
||||
if 'mark' in m:
|
||||
all_marks = tuple(m['mark'](text, args, Mark, extra_cli_args))
|
||||
@@ -381,9 +478,8 @@ def run(args: HintsCLIOptions, text: str, extra_cli_args: Sequence[str] = ()) ->
|
||||
else:
|
||||
all_marks = tuple(mark(pattern, post_processors, text, args))
|
||||
if not all_marks:
|
||||
input(_('No {} found, press Enter to quit.').format(
|
||||
'URLs' if args.type == 'url' else 'matches'
|
||||
))
|
||||
none_of = {'url': 'URLs', 'hyperlink': 'hyperlinks'}.get(args.type, 'matches')
|
||||
input(_('No {} found, press Enter to quit.').format(none_of))
|
||||
return None
|
||||
|
||||
largest_index = all_marks[-1].index
|
||||
@@ -418,7 +514,7 @@ programs.
|
||||
|
||||
--type
|
||||
default=url
|
||||
choices=url,regex,path,line,hash,word,linenum
|
||||
choices=url,regex,path,line,hash,word,linenum,hyperlink,ip
|
||||
The type of text to search for. A value of :code:`linenum` is special, it looks
|
||||
for error messages using the pattern specified with :option:`--regex`, which
|
||||
must have the named groups, :code:`path` and :code:`line`. If not specified,
|
||||
@@ -513,6 +609,24 @@ type=bool-set
|
||||
Have the hints increase from top to bottom instead of decreasing from top to bottom.
|
||||
|
||||
|
||||
--hints-foreground-color
|
||||
default=black
|
||||
type=str
|
||||
The foreground color for hints
|
||||
|
||||
|
||||
--hints-background-color
|
||||
default=green
|
||||
type=str
|
||||
The background color for hints
|
||||
|
||||
|
||||
--hints-text-color
|
||||
default=gray
|
||||
type=str
|
||||
The foreground color for text pointed to by the hints
|
||||
|
||||
|
||||
--customize-processing
|
||||
Name of a python file in the kitty config directory which will be imported to provide
|
||||
custom implementations for pattern finding and performing actions
|
||||
@@ -552,7 +666,12 @@ def main(args: List[str]) -> Optional[Dict[str, Any]]:
|
||||
if items and not (opts.customize_processing or opts.type == 'linenum'):
|
||||
print('Extra command line arguments present: {}'.format(' '.join(items)), file=sys.stderr)
|
||||
input(_('Press Enter to quit'))
|
||||
return run(opts, text, items)
|
||||
try:
|
||||
return run(opts, text, items)
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
input(_('Press Enter to quit'))
|
||||
|
||||
|
||||
def linenum_handle_result(args: List[str], data: Dict[str, Any], target_window_id: int, boss: BossType, extra_cli_args: Sequence[str], *a: Any) -> None:
|
||||
@@ -583,7 +702,7 @@ def linenum_handle_result(args: List[str], data: Dict[str, Any], target_window_i
|
||||
}[action])(*cmd)
|
||||
|
||||
|
||||
@result_handler(type_of_input='screen')
|
||||
@result_handler(type_of_input='screen-ansi')
|
||||
def handle_result(args: List[str], data: Dict[str, Any], target_window_id: int, boss: BossType) -> None:
|
||||
if data['customize_processing']:
|
||||
m = load_custom_processor(data['customize_processing'])
|
||||
@@ -635,13 +754,20 @@ def handle_result(args: List[str], data: Dict[str, Any], target_window_id: int,
|
||||
w = boss.window_id_map.get(target_window_id)
|
||||
if w is not None:
|
||||
cwd = w.cwd_of_child
|
||||
if w is None:
|
||||
w = boss.active_window
|
||||
program = None if program == 'default' else program
|
||||
for m, groupdict in zip(matches, groupdicts):
|
||||
if groupdict:
|
||||
m = []
|
||||
for k, v in groupdict.items():
|
||||
m.append('{}={}'.format(k, v or ''))
|
||||
boss.open_url(m, program, cwd=cwd)
|
||||
if text_type == 'hyperlink':
|
||||
for m in matches:
|
||||
if w is not None:
|
||||
w.open_url(m, hyperlink_id=1, cwd=cwd)
|
||||
else:
|
||||
for m, groupdict in zip(matches, groupdicts):
|
||||
if groupdict:
|
||||
m = []
|
||||
for k, v in groupdict.items():
|
||||
m.append('{}={}'.format(k, v or ''))
|
||||
boss.open_url(m, program, cwd=cwd)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
0
kittens/hyperlinked_grep/__init__.py
Normal file
0
kittens/hyperlinked_grep/__init__.py
Normal file
65
kittens/hyperlinked_grep/main.py
Normal file
65
kittens/hyperlinked_grep/main.py
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Callable, cast
|
||||
from urllib.parse import quote_from_bytes
|
||||
|
||||
|
||||
def write_hyperlink(write: Callable[[bytes], None], url: bytes, line: bytes, frag: bytes = b'') -> None:
|
||||
text = b'\033]8;;' + url
|
||||
if frag:
|
||||
text += b'#' + frag
|
||||
text += b'\033\\' + line + b'\033]8;;\033\\'
|
||||
write(text)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not sys.stdout.isatty() and '--pretty' not in sys.argv:
|
||||
os.execlp('rg', 'rg', *sys.argv[1:])
|
||||
cmdline = ['rg', '--pretty', '--with-filename'] + sys.argv[1:]
|
||||
p = subprocess.Popen(cmdline, stdout=subprocess.PIPE)
|
||||
assert p.stdout is not None
|
||||
write: Callable[[bytes], None] = cast(Callable[[bytes], None], sys.stdout.buffer.write)
|
||||
sgr_pat = re.compile(br'\x1b\[.*?m')
|
||||
osc_pat = re.compile(b'\x1b\\].*?\x1b\\\\')
|
||||
num_pat = re.compile(b'^(\\d+):')
|
||||
|
||||
in_result: bytes = b''
|
||||
hostname = socket.gethostname().encode('utf-8')
|
||||
|
||||
try:
|
||||
for line in p.stdout:
|
||||
line = osc_pat.sub(b'', line) # remove any existing hyperlinks
|
||||
clean_line = sgr_pat.sub(b'', line).rstrip() # remove SGR formatting
|
||||
if not clean_line:
|
||||
in_result = b''
|
||||
write(b'\n')
|
||||
continue
|
||||
if in_result:
|
||||
m = num_pat.match(clean_line)
|
||||
if m is not None:
|
||||
write_hyperlink(write, in_result, line, frag=m.group(1))
|
||||
else:
|
||||
if line.strip():
|
||||
path = quote_from_bytes(os.path.abspath(clean_line)).encode('utf-8')
|
||||
in_result = b'file://' + hostname + path
|
||||
write_hyperlink(write, in_result, line)
|
||||
else:
|
||||
write(line)
|
||||
except KeyboardInterrupt:
|
||||
p.send_signal(signal.SIGINT)
|
||||
p.stdout.close()
|
||||
except EOFError:
|
||||
p.stdout.close()
|
||||
raise SystemExit(p.wait())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -30,7 +30,7 @@ from ..tui.images import (
|
||||
ConvertFailed, GraphicsCommand, NoImageMagick, OpenFailed, convert, fsenc,
|
||||
identify
|
||||
)
|
||||
from ..tui.operations import clear_images_on_screen
|
||||
from ..tui.operations import clear_images_on_screen, raw_mode
|
||||
|
||||
OPTIONS = '''\
|
||||
--align
|
||||
@@ -110,6 +110,11 @@ default=0
|
||||
Z-index of the image. When negative, text will be displayed on top of the image. Use
|
||||
a double minus for values under the threshold for drawing images under cell background
|
||||
colors. For example, --1 evaluates as -1,073,741,825.
|
||||
|
||||
|
||||
--hold
|
||||
type=bool-set
|
||||
Wait for keypress before exiting after displaying the images.
|
||||
'''
|
||||
|
||||
|
||||
@@ -473,11 +478,14 @@ def main(args: List[str] = sys.argv) -> None:
|
||||
errors.append(e)
|
||||
if parsed_opts.place:
|
||||
sys.stdout.buffer.write(b'\0338') # restore cursor
|
||||
if not errors:
|
||||
return
|
||||
for err in errors:
|
||||
print(err, file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
if errors:
|
||||
for err in errors:
|
||||
print(err, file=sys.stderr)
|
||||
if cli_opts.hold:
|
||||
with open(os.ctermid()) as tty:
|
||||
with raw_mode(tty.fileno()):
|
||||
tty.buffer.read(1)
|
||||
raise SystemExit(1 if errors else 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -10,8 +10,8 @@ from typing import Callable, Dict, List, Tuple
|
||||
|
||||
from kitty.cli import parse_args
|
||||
from kitty.cli_stub import PanelCLIOptions
|
||||
from kitty.options_stub import Options
|
||||
from kitty.constants import is_macos
|
||||
from kitty.os_window_size import WindowSizeData
|
||||
|
||||
OPTIONS = r'''
|
||||
--lines
|
||||
@@ -108,7 +108,7 @@ def setup_x11_window(win_id: int) -> None:
|
||||
func(win_id, window_width, window_height)
|
||||
|
||||
|
||||
def initial_window_size_func(opts: Options, cached_values: Dict) -> Callable[[int, int, float, float, float, float], Tuple[int, int]]:
|
||||
def initial_window_size_func(opts: WindowSizeData, cached_values: Dict) -> Callable[[int, int, float, float, float, float], Tuple[int, int]]:
|
||||
from kitty.fast_data_types import glfw_primary_monitor_size, set_smallest_allowed_resize
|
||||
|
||||
def initial_window_size(cell_width: int, cell_height: int, dpi_x: float, dpi_y: float, xscale: float, yscale: float) -> Tuple[int, int]:
|
||||
|
||||
0
kittens/remote_file/__init__.py
Normal file
0
kittens/remote_file/__init__.py
Normal file
325
kittens/remote_file/main.py
Normal file
325
kittens/remote_file/main.py
Normal file
@@ -0,0 +1,325 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from kitty.cli import parse_args
|
||||
from kitty.cli_stub import RemoteFileCLIOptions
|
||||
from kitty.constants import cache_dir
|
||||
from kitty.typing import BossType
|
||||
from kitty.utils import (
|
||||
SSHConnectionData, command_for_open, get_editor, open_cmd
|
||||
)
|
||||
|
||||
from ..tui.handler import result_handler
|
||||
from ..tui.operations import (
|
||||
faint, raw_mode, reset_terminal, styled
|
||||
)
|
||||
from ..tui.utils import get_key_press
|
||||
|
||||
|
||||
def key(x: str) -> str:
|
||||
return styled(x, bold=True, fg='green')
|
||||
|
||||
|
||||
def option_text() -> str:
|
||||
return '''\
|
||||
--mode -m
|
||||
choices=ask,edit
|
||||
default=ask
|
||||
Which mode to operate in.
|
||||
|
||||
|
||||
--path -p
|
||||
Path to the remote file.
|
||||
|
||||
|
||||
--hostname -h
|
||||
Hostname of the remote host.
|
||||
|
||||
|
||||
--ssh-connection-data
|
||||
The data used to connect over ssh.
|
||||
'''
|
||||
|
||||
|
||||
def show_error(msg: str) -> None:
|
||||
print(styled(msg, fg='red'))
|
||||
print()
|
||||
print('Press any key to exit...')
|
||||
sys.stdout.flush()
|
||||
with raw_mode():
|
||||
while True:
|
||||
try:
|
||||
q = sys.stdin.buffer.read(1)
|
||||
if q:
|
||||
break
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
break
|
||||
|
||||
|
||||
def ask_action(opts: RemoteFileCLIOptions) -> str:
|
||||
print('What would you like to do with the remote file on {}:'.format(styled(opts.hostname or 'unknown', bold=True, fg='magenta')))
|
||||
print(styled(opts.path or '', fg='yellow', fg_intense=True))
|
||||
print()
|
||||
|
||||
def help_text(x: str) -> str:
|
||||
return faint(x)
|
||||
|
||||
print('{}dit the file'.format(key('E')))
|
||||
print(help_text('The file will be downloaded and opened in an editor. Any changes you save will'
|
||||
' be automatically sent back to the remote machine'))
|
||||
print()
|
||||
|
||||
print('{}pen the file'.format(key('O')))
|
||||
print(help_text('The file will be downloaded and opened by the default open program'))
|
||||
print()
|
||||
|
||||
print('{}ave the file'.format(key('S')))
|
||||
print(help_text('The file will be downloaded to a destination you select'))
|
||||
print()
|
||||
|
||||
print('{}ancel'.format(key('C')))
|
||||
print()
|
||||
|
||||
sys.stdout.flush()
|
||||
response = get_key_press('ceos', 'c')
|
||||
return {'e': 'edit', 'o': 'open', 's': 'save'}.get(response, 'cancel')
|
||||
|
||||
|
||||
def hostname_matches(from_hyperlink: str, actual: str) -> bool:
|
||||
if from_hyperlink == actual:
|
||||
return True
|
||||
if from_hyperlink.partition('.')[0] == actual.partition('.')[0]:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class ControlMaster:
|
||||
|
||||
def __init__(self, conn_data: SSHConnectionData, remote_path: str, cli_opts: RemoteFileCLIOptions, dest: str = ''):
|
||||
self.conn_data = conn_data
|
||||
self.cli_opts = cli_opts
|
||||
self.remote_path = remote_path
|
||||
self.dest = dest
|
||||
self.tdir = ''
|
||||
self.cmd_prefix = cmd = [
|
||||
conn_data.binary, '-o', f'ControlPath=~/.ssh/kitty-master-{os.getpid()}-%r@%h:%p',
|
||||
'-o', 'TCPKeepAlive=yes', '-o', 'ControlPersist=yes'
|
||||
]
|
||||
if conn_data.port:
|
||||
cmd += ['-p', str(conn_data.port)]
|
||||
self.batch_cmd_prefix = cmd + ['-o', 'BatchMode=yes']
|
||||
|
||||
def __enter__(self) -> 'ControlMaster':
|
||||
subprocess.check_call(
|
||||
self.cmd_prefix + ['-o', 'ControlMaster=auto', '-fN', self.conn_data.hostname])
|
||||
subprocess.check_call(
|
||||
self.batch_cmd_prefix + ['-O', 'check', self.conn_data.hostname])
|
||||
if not self.dest:
|
||||
self.tdir = tempfile.mkdtemp()
|
||||
self.dest = os.path.join(self.tdir, os.path.basename(self.remote_path))
|
||||
return self
|
||||
|
||||
def __exit__(self, *a: Any) -> bool:
|
||||
subprocess.Popen(
|
||||
self.batch_cmd_prefix + ['-O', 'exit', self.conn_data.hostname],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL
|
||||
).wait()
|
||||
if self.tdir:
|
||||
shutil.rmtree(self.tdir)
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_alive(self) -> bool:
|
||||
return subprocess.Popen(
|
||||
self.batch_cmd_prefix + ['-O', 'check', self.conn_data.hostname],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL
|
||||
).wait() == 0
|
||||
|
||||
def check_hostname_matches(self) -> bool:
|
||||
cp = subprocess.run(self.batch_cmd_prefix + [self.conn_data.hostname, 'hostname', '-f'], stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL)
|
||||
if cp.returncode == 0:
|
||||
q = cp.stdout.decode('utf-8').strip()
|
||||
if not hostname_matches(self.cli_opts.hostname or '', q):
|
||||
print(reset_terminal(), end='')
|
||||
print(f'The remote hostname {styled(q, fg="green")} does not match the')
|
||||
print(f'hostname in the hyperlink {styled(self.cli_opts.hostname or "", fg="red")}')
|
||||
print('This indicates that kitty has not connected to the correct remote machine.')
|
||||
print('This can happen, for example, when using nested SSH sessions.')
|
||||
print(f'The hostname kitty used to connect was: {styled(self.conn_data.hostname, fg="yellow")}', end='')
|
||||
if self.conn_data.port is not None:
|
||||
print(f' with port: {self.conn_data.port}')
|
||||
print()
|
||||
print()
|
||||
print('Do you want to continue anyway?')
|
||||
print(
|
||||
f'{styled("Y", fg="green")}es',
|
||||
f'{styled("N", fg="red")}o', sep='\t'
|
||||
)
|
||||
sys.stdout.flush()
|
||||
response = get_key_press('yn', 'n')
|
||||
print(reset_terminal(), end='')
|
||||
return response == 'y'
|
||||
return True
|
||||
|
||||
def download(self) -> bool:
|
||||
with open(self.dest, 'wb') as f:
|
||||
return subprocess.run(
|
||||
self.batch_cmd_prefix + [self.conn_data.hostname, 'cat', self.remote_path],
|
||||
stdout=f, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL
|
||||
).returncode == 0
|
||||
|
||||
def upload(self, suppress_output: bool = True) -> bool:
|
||||
cmd_prefix = self.cmd_prefix if suppress_output else self.batch_cmd_prefix
|
||||
cmd = cmd_prefix + [self.conn_data.hostname, 'cat', '>', self.remote_path]
|
||||
if not suppress_output:
|
||||
print(' '.join(map(shlex.quote, cmd)))
|
||||
redirect = subprocess.DEVNULL if suppress_output else None
|
||||
with open(self.dest, 'rb') as f:
|
||||
return subprocess.run(cmd, stdout=redirect, stderr=redirect, stdin=f).returncode == 0
|
||||
|
||||
|
||||
Result = Optional[str]
|
||||
|
||||
|
||||
def main(args: List[str]) -> Result:
|
||||
msg = 'Ask the user what to do with the remote file'
|
||||
try:
|
||||
cli_opts, items = parse_args(args[1:], option_text, '', msg, 'kitty remote_file', result_class=RemoteFileCLIOptions)
|
||||
except SystemExit as e:
|
||||
if e.code != 0:
|
||||
print(e.args[0])
|
||||
input('Press enter to quit...')
|
||||
raise SystemExit(e.code)
|
||||
|
||||
try:
|
||||
action = ask_action(cli_opts)
|
||||
finally:
|
||||
print(reset_terminal(), end='', flush=True)
|
||||
try:
|
||||
return handle_action(action, cli_opts)
|
||||
except Exception:
|
||||
print(reset_terminal(), end='', flush=True)
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
show_error('Failed with unhandled exception')
|
||||
|
||||
|
||||
def save_as(conn_data: SSHConnectionData, remote_path: str, cli_opts: RemoteFileCLIOptions) -> None:
|
||||
ddir = cache_dir()
|
||||
os.makedirs(ddir, exist_ok=True)
|
||||
last_used_store_path = os.path.join(ddir, 'remote-file-last-used.txt')
|
||||
try:
|
||||
with open(last_used_store_path) as f:
|
||||
last_used_path = f.read()
|
||||
except FileNotFoundError:
|
||||
last_used_path = tempfile.gettempdir()
|
||||
last_used_file = os.path.join(last_used_path, os.path.basename(remote_path))
|
||||
print(
|
||||
'Where do you wish to save the file? Leaving it blank will save it as:',
|
||||
styled(last_used_file, fg='yellow')
|
||||
)
|
||||
print('Relative paths will be resolved from:', styled(os.getcwd(), fg_intense=True, bold=True))
|
||||
print()
|
||||
from ..tui.path_completer import PathCompleter
|
||||
try:
|
||||
dest = PathCompleter().input()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
return
|
||||
if dest:
|
||||
dest = os.path.expandvars(os.path.expanduser(dest))
|
||||
if os.path.isdir(dest):
|
||||
dest = os.path.join(dest, os.path.basename(remote_path))
|
||||
with open(last_used_store_path, 'w') as f:
|
||||
f.write(os.path.dirname(os.path.abspath(dest)))
|
||||
else:
|
||||
dest = last_used_file
|
||||
if os.path.exists(dest):
|
||||
print(reset_terminal(), end='')
|
||||
print(f'The file {styled(dest, fg="yellow")} already exists. What would you like to do?')
|
||||
print(f'{key("O")}verwrite {key("A")}bort Auto {key("R")}ename {key("N")}ew name')
|
||||
response = get_key_press('anor', 'a')
|
||||
if response == 'a':
|
||||
return
|
||||
if response == 'n':
|
||||
print(reset_terminal(), end='')
|
||||
return save_as(conn_data, remote_path, cli_opts)
|
||||
|
||||
if response == 'r':
|
||||
q = dest
|
||||
c = 0
|
||||
while os.path.exists(q):
|
||||
c += 1
|
||||
b, ext = os.path.splitext(dest)
|
||||
q = f'{b}-{c}{ext}'
|
||||
dest = q
|
||||
if os.path.dirname(dest):
|
||||
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
||||
with ControlMaster(conn_data, remote_path, cli_opts, dest=dest) as master:
|
||||
if master.check_hostname_matches():
|
||||
if not master.download():
|
||||
show_error('Failed to copy file from remote machine')
|
||||
|
||||
|
||||
def handle_action(action: str, cli_opts: RemoteFileCLIOptions) -> Result:
|
||||
conn_data = SSHConnectionData(*json.loads(cli_opts.ssh_connection_data or ''))
|
||||
remote_path = cli_opts.path or ''
|
||||
if action == 'open':
|
||||
print('Opening', cli_opts.path, 'from', cli_opts.hostname)
|
||||
dest = os.path.join(tempfile.mkdtemp(), os.path.basename(remote_path))
|
||||
with ControlMaster(conn_data, remote_path, cli_opts, dest=dest) as master:
|
||||
if master.check_hostname_matches():
|
||||
if master.download():
|
||||
return dest
|
||||
show_error('Failed to copy file from remote machine')
|
||||
elif action == 'edit':
|
||||
print('Editing', cli_opts.path, 'from', cli_opts.hostname)
|
||||
with ControlMaster(conn_data, remote_path, cli_opts) as master:
|
||||
if not master.check_hostname_matches():
|
||||
return None
|
||||
if not master.download():
|
||||
show_error(f'Failed to download {remote_path}')
|
||||
return None
|
||||
mtime = os.path.getmtime(master.dest)
|
||||
print(reset_terminal(), end='', flush=True)
|
||||
editor = get_editor()
|
||||
editor_process = subprocess.Popen(editor + [master.dest])
|
||||
while editor_process.poll() is None:
|
||||
time.sleep(0.1)
|
||||
newmtime = os.path.getmtime(master.dest)
|
||||
if newmtime > mtime:
|
||||
mtime = newmtime
|
||||
if master.is_alive:
|
||||
master.upload()
|
||||
print(reset_terminal(), end='', flush=True)
|
||||
if master.is_alive:
|
||||
if not master.upload(suppress_output=False):
|
||||
show_error(f'Failed to upload {remote_path}')
|
||||
else:
|
||||
show_error(f'Failed to upload {remote_path}, SSH master process died')
|
||||
elif action == 'save':
|
||||
print('Saving', cli_opts.path, 'from', cli_opts.hostname)
|
||||
save_as(conn_data, remote_path, cli_opts)
|
||||
|
||||
|
||||
@result_handler()
|
||||
def handle_result(args: List[str], data: Result, target_window_id: int, boss: BossType) -> None:
|
||||
if data:
|
||||
cmd = command_for_open(boss.opts.open_url_with)
|
||||
open_cmd(cmd, data)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
@@ -7,7 +7,10 @@ import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import List, NoReturn, Set, Tuple
|
||||
from contextlib import suppress
|
||||
from typing import List, NoReturn, Optional, Set, Tuple
|
||||
|
||||
from kitty.utils import SSHConnectionData
|
||||
|
||||
SHELL_SCRIPT = '''\
|
||||
#!/bin/sh
|
||||
@@ -60,6 +63,41 @@ def get_ssh_cli() -> Tuple[Set[str], Set[str]]:
|
||||
return set('-' + x for x in boolean_ssh_args), set('-' + x for x in other_ssh_args)
|
||||
|
||||
|
||||
def get_connection_data(args: List[str]) -> Optional[SSHConnectionData]:
|
||||
boolean_ssh_args, other_ssh_args = get_ssh_cli()
|
||||
found_ssh = ''
|
||||
port: Optional[int] = None
|
||||
expecting_port = False
|
||||
expecting_option_val = False
|
||||
|
||||
for i, arg in enumerate(args):
|
||||
if not found_ssh:
|
||||
if os.path.basename(arg).lower() in ('ssh', 'ssh.exe'):
|
||||
found_ssh = arg
|
||||
continue
|
||||
if arg.startswith('-') and not expecting_option_val:
|
||||
if arg in boolean_ssh_args:
|
||||
continue
|
||||
if arg.startswith('-p'):
|
||||
if arg[2:].isdigit():
|
||||
with suppress(Exception):
|
||||
port = int(arg[2:])
|
||||
elif arg == '-p':
|
||||
expecting_port = True
|
||||
expecting_option_val = True
|
||||
continue
|
||||
|
||||
if expecting_option_val:
|
||||
if expecting_port:
|
||||
with suppress(Exception):
|
||||
port = int(arg)
|
||||
expecting_port = False
|
||||
expecting_option_val = False
|
||||
continue
|
||||
|
||||
return SSHConnectionData(found_ssh, arg, port)
|
||||
|
||||
|
||||
def parse_ssh_args(args: List[str]) -> Tuple[List[str], List[str], bool]:
|
||||
boolean_ssh_args, other_ssh_args = get_ssh_cli()
|
||||
passthrough_args = {'-' + x for x in 'Nnf'}
|
||||
|
||||
385
kittens/tui/dircolors.py
Normal file
385
kittens/tui/dircolors.py
Normal file
@@ -0,0 +1,385 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import os
|
||||
import stat
|
||||
from contextlib import suppress
|
||||
from typing import Dict, Generator, Optional, Tuple, Union
|
||||
|
||||
DEFAULT_DIRCOLORS = r"""# {{{
|
||||
# Configuration file for dircolors, a utility to help you set the
|
||||
# LS_COLORS environment variable used by GNU ls with the --color option.
|
||||
# Copyright (C) 1996-2019 Free Software Foundation, Inc.
|
||||
# Copying and distribution of this file, with or without modification,
|
||||
# are permitted provided the copyright notice and this notice are preserved.
|
||||
# The keywords COLOR, OPTIONS, and EIGHTBIT (honored by the
|
||||
# slackware version of dircolors) are recognized but ignored.
|
||||
# Below are TERM entries, which can be a glob patterns, to match
|
||||
# against the TERM environment variable to determine if it is colorizable.
|
||||
TERM Eterm
|
||||
TERM ansi
|
||||
TERM *color*
|
||||
TERM con[0-9]*x[0-9]*
|
||||
TERM cons25
|
||||
TERM console
|
||||
TERM cygwin
|
||||
TERM dtterm
|
||||
TERM gnome
|
||||
TERM hurd
|
||||
TERM jfbterm
|
||||
TERM konsole
|
||||
TERM kterm
|
||||
TERM linux
|
||||
TERM linux-c
|
||||
TERM mlterm
|
||||
TERM putty
|
||||
TERM rxvt*
|
||||
TERM screen*
|
||||
TERM st
|
||||
TERM terminator
|
||||
TERM tmux*
|
||||
TERM vt100
|
||||
TERM xterm*
|
||||
# Below are the color init strings for the basic file types.
|
||||
# One can use codes for 256 or more colors supported by modern terminals.
|
||||
# The default color codes use the capabilities of an 8 color terminal
|
||||
# with some additional attributes as per the following codes:
|
||||
# Attribute codes:
|
||||
# 00=none 01=bold 04=underscore 05=blink 07=reverse 08=concealed
|
||||
# Text color codes:
|
||||
# 30=black 31=red 32=green 33=yellow 34=blue 35=magenta 36=cyan 37=white
|
||||
# Background color codes:
|
||||
# 40=black 41=red 42=green 43=yellow 44=blue 45=magenta 46=cyan 47=white
|
||||
#NORMAL 00 # no color code at all
|
||||
#FILE 00 # regular file: use no color at all
|
||||
RESET 0 # reset to "normal" color
|
||||
DIR 01;34 # directory
|
||||
LINK 01;36 # symbolic link. (If you set this to 'target' instead of a
|
||||
# numerical value, the color is as for the file pointed to.)
|
||||
MULTIHARDLINK 00 # regular file with more than one link
|
||||
FIFO 40;33 # pipe
|
||||
SOCK 01;35 # socket
|
||||
DOOR 01;35 # door
|
||||
BLK 40;33;01 # block device driver
|
||||
CHR 40;33;01 # character device driver
|
||||
ORPHAN 40;31;01 # symlink to nonexistent file, or non-stat'able file ...
|
||||
MISSING 00 # ... and the files they point to
|
||||
SETUID 37;41 # file that is setuid (u+s)
|
||||
SETGID 30;43 # file that is setgid (g+s)
|
||||
CAPABILITY 30;41 # file with capability
|
||||
STICKY_OTHER_WRITABLE 30;42 # dir that is sticky and other-writable (+t,o+w)
|
||||
OTHER_WRITABLE 34;42 # dir that is other-writable (o+w) and not sticky
|
||||
STICKY 37;44 # dir with the sticky bit set (+t) and not other-writable
|
||||
# This is for files with execute permission:
|
||||
EXEC 01;32
|
||||
# List any file extensions like '.gz' or '.tar' that you would like ls
|
||||
# to colorize below. Put the extension, a space, and the color init string.
|
||||
# (and any comments you want to add after a '#')
|
||||
# If you use DOS-style suffixes, you may want to uncomment the following:
|
||||
#.cmd 01;32 # executables (bright green)
|
||||
#.exe 01;32
|
||||
#.com 01;32
|
||||
#.btm 01;32
|
||||
#.bat 01;32
|
||||
# Or if you want to colorize scripts even if they do not have the
|
||||
# executable bit actually set.
|
||||
#.sh 01;32
|
||||
#.csh 01;32
|
||||
# archives or compressed (bright red)
|
||||
.tar 01;31
|
||||
.tgz 01;31
|
||||
.arc 01;31
|
||||
.arj 01;31
|
||||
.taz 01;31
|
||||
.lha 01;31
|
||||
.lz4 01;31
|
||||
.lzh 01;31
|
||||
.lzma 01;31
|
||||
.tlz 01;31
|
||||
.txz 01;31
|
||||
.tzo 01;31
|
||||
.t7z 01;31
|
||||
.zip 01;31
|
||||
.z 01;31
|
||||
.dz 01;31
|
||||
.gz 01;31
|
||||
.lrz 01;31
|
||||
.lz 01;31
|
||||
.lzo 01;31
|
||||
.xz 01;31
|
||||
.zst 01;31
|
||||
.tzst 01;31
|
||||
.bz2 01;31
|
||||
.bz 01;31
|
||||
.tbz 01;31
|
||||
.tbz2 01;31
|
||||
.tz 01;31
|
||||
.deb 01;31
|
||||
.rpm 01;31
|
||||
.jar 01;31
|
||||
.war 01;31
|
||||
.ear 01;31
|
||||
.sar 01;31
|
||||
.rar 01;31
|
||||
.alz 01;31
|
||||
.ace 01;31
|
||||
.zoo 01;31
|
||||
.cpio 01;31
|
||||
.7z 01;31
|
||||
.rz 01;31
|
||||
.cab 01;31
|
||||
.wim 01;31
|
||||
.swm 01;31
|
||||
.dwm 01;31
|
||||
.esd 01;31
|
||||
# image formats
|
||||
.jpg 01;35
|
||||
.jpeg 01;35
|
||||
.mjpg 01;35
|
||||
.mjpeg 01;35
|
||||
.gif 01;35
|
||||
.bmp 01;35
|
||||
.pbm 01;35
|
||||
.pgm 01;35
|
||||
.ppm 01;35
|
||||
.tga 01;35
|
||||
.xbm 01;35
|
||||
.xpm 01;35
|
||||
.tif 01;35
|
||||
.tiff 01;35
|
||||
.png 01;35
|
||||
.svg 01;35
|
||||
.svgz 01;35
|
||||
.mng 01;35
|
||||
.pcx 01;35
|
||||
.mov 01;35
|
||||
.mpg 01;35
|
||||
.mpeg 01;35
|
||||
.m2v 01;35
|
||||
.mkv 01;35
|
||||
.webm 01;35
|
||||
.ogm 01;35
|
||||
.mp4 01;35
|
||||
.m4v 01;35
|
||||
.mp4v 01;35
|
||||
.vob 01;35
|
||||
.qt 01;35
|
||||
.nuv 01;35
|
||||
.wmv 01;35
|
||||
.asf 01;35
|
||||
.rm 01;35
|
||||
.rmvb 01;35
|
||||
.flc 01;35
|
||||
.avi 01;35
|
||||
.fli 01;35
|
||||
.flv 01;35
|
||||
.gl 01;35
|
||||
.dl 01;35
|
||||
.xcf 01;35
|
||||
.xwd 01;35
|
||||
.yuv 01;35
|
||||
.cgm 01;35
|
||||
.emf 01;35
|
||||
# https://wiki.xiph.org/MIME_Types_and_File_Extensions
|
||||
.ogv 01;35
|
||||
.ogx 01;35
|
||||
# audio formats
|
||||
.aac 00;36
|
||||
.au 00;36
|
||||
.flac 00;36
|
||||
.m4a 00;36
|
||||
.mid 00;36
|
||||
.midi 00;36
|
||||
.mka 00;36
|
||||
.mp3 00;36
|
||||
.mpc 00;36
|
||||
.ogg 00;36
|
||||
.ra 00;36
|
||||
.wav 00;36
|
||||
# https://wiki.xiph.org/MIME_Types_and_File_Extensions
|
||||
.oga 00;36
|
||||
.opus 00;36
|
||||
.spx 00;36
|
||||
.xspf 00;36
|
||||
""" # }}}
|
||||
|
||||
# special file?
|
||||
special_types = (
|
||||
(stat.S_IFLNK, 'ln'), # symlink
|
||||
(stat.S_IFIFO, 'pi'), # pipe (FIFO)
|
||||
(stat.S_IFSOCK, 'so'), # socket
|
||||
(stat.S_IFBLK, 'bd'), # block device
|
||||
(stat.S_IFCHR, 'cd'), # character device
|
||||
(stat.S_ISUID, 'su'), # setuid
|
||||
(stat.S_ISGID, 'sg'), # setgid
|
||||
)
|
||||
|
||||
CODE_MAP = {
|
||||
'RESET': 'rs',
|
||||
'DIR': 'di',
|
||||
'LINK': 'ln',
|
||||
'MULTIHARDLINK': 'mh',
|
||||
'FIFO': 'pi',
|
||||
'SOCK': 'so',
|
||||
'DOOR': 'do',
|
||||
'BLK': 'bd',
|
||||
'CHR': 'cd',
|
||||
'ORPHAN': 'or',
|
||||
'MISSING': 'mi',
|
||||
'SETUID': 'su',
|
||||
'SETGID': 'sg',
|
||||
'CAPABILITY': 'ca',
|
||||
'STICKY_OTHER_WRITABLE': 'tw',
|
||||
'OTHER_WRITABLE': 'ow',
|
||||
'STICKY': 'st',
|
||||
'EXEC': 'ex',
|
||||
}
|
||||
|
||||
|
||||
def stat_at(file: str, cwd: Optional[Union[int, str]] = None, follow_symlinks: bool = False) -> os.stat_result:
|
||||
dirfd: Optional[int] = None
|
||||
need_to_close = False
|
||||
if isinstance(cwd, str):
|
||||
dirfd = os.open(cwd, os.O_RDONLY)
|
||||
need_to_close = True
|
||||
elif isinstance(cwd, int):
|
||||
dirfd = cwd
|
||||
|
||||
try:
|
||||
return os.stat(file, dir_fd=dirfd, follow_symlinks=follow_symlinks)
|
||||
finally:
|
||||
if need_to_close and dirfd is not None:
|
||||
os.close(dirfd)
|
||||
|
||||
|
||||
class Dircolors:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.codes: Dict[str, str] = {}
|
||||
self.extensions: Dict[str, str] = {}
|
||||
if not self.load_from_environ() and not self.load_from_file():
|
||||
self.load_defaults()
|
||||
|
||||
def clear(self) -> None:
|
||||
self.codes.clear()
|
||||
self.extensions.clear()
|
||||
|
||||
def load_from_file(self) -> bool:
|
||||
for candidate in (os.path.expanduser('~/.dir_colors'), '/etc/DIR_COLORS'):
|
||||
with suppress(Exception):
|
||||
with open(candidate) as f:
|
||||
return self.load_from_dircolors(f.read())
|
||||
return False
|
||||
|
||||
def load_from_lscolors(self, lscolors: str) -> bool:
|
||||
self.clear()
|
||||
if not lscolors:
|
||||
return False
|
||||
|
||||
for item in lscolors.split(':'):
|
||||
try:
|
||||
code, color = item.split('=', 1)
|
||||
except ValueError:
|
||||
continue
|
||||
if code.startswith('*.'):
|
||||
self.extensions[code[1:]] = color
|
||||
else:
|
||||
self.codes[code] = color
|
||||
|
||||
return bool(self.codes or self.extensions)
|
||||
|
||||
def load_from_environ(self, envvar: str = 'LS_COLORS') -> bool:
|
||||
return self.load_from_lscolors(os.environ.get(envvar) or '')
|
||||
|
||||
def load_from_dircolors(self, database: str, strict: bool = False) -> bool:
|
||||
self.clear()
|
||||
|
||||
for line in database.splitlines():
|
||||
line = line.split('#')[0].strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
split = line.split()
|
||||
if len(split) != 2:
|
||||
if strict:
|
||||
raise ValueError(f'Warning: unable to parse dircolors line "{line}"')
|
||||
continue
|
||||
|
||||
key, val = split
|
||||
if key == 'TERM':
|
||||
continue
|
||||
if key in CODE_MAP:
|
||||
self.codes[CODE_MAP[key]] = val
|
||||
elif key.startswith('.'):
|
||||
self.extensions[key] = val
|
||||
elif strict:
|
||||
raise ValueError(f'Warning: unable to parse dircolors line "{line}"')
|
||||
|
||||
return bool(self.codes or self.extensions)
|
||||
|
||||
def load_defaults(self) -> bool:
|
||||
self.clear()
|
||||
return self.load_from_dircolors(DEFAULT_DIRCOLORS, True)
|
||||
|
||||
def generate_lscolors(self) -> str:
|
||||
""" Output the database in the format used by the LS_COLORS environment variable. """
|
||||
|
||||
def gen_pairs() -> Generator[Tuple[str, str], None, None]:
|
||||
for pair in self.codes.items():
|
||||
yield pair
|
||||
for pair in self.extensions.items():
|
||||
# change .xyz to *.xyz
|
||||
yield '*' + pair[0], pair[1]
|
||||
|
||||
return ':'.join('%s=%s' % pair for pair in gen_pairs())
|
||||
|
||||
def _format_code(self, text: str, code: str) -> str:
|
||||
val = self.codes.get(code)
|
||||
return '\033[%sm%s\033[%sm' % (val, text, self.codes.get('rs', '0')) if val else text
|
||||
|
||||
def _format_ext(self, text: str, ext: str) -> str:
|
||||
val = self.extensions.get(ext, '0')
|
||||
return '\033[%sm%s\033[%sm' % (val, text, self.codes.get('rs', '0')) if val else text
|
||||
|
||||
def format_mode(self, text: str, sr: os.stat_result) -> str:
|
||||
mode = sr.st_mode
|
||||
if stat.S_ISDIR(mode):
|
||||
if (mode & (stat.S_ISVTX | stat.S_IWOTH)) == (stat.S_ISVTX | stat.S_IWOTH):
|
||||
# sticky and world-writable
|
||||
return self._format_code(text, 'tw')
|
||||
if mode & stat.S_ISVTX:
|
||||
# sticky but not world-writable
|
||||
return self._format_code(text, 'st')
|
||||
if mode & stat.S_IWOTH:
|
||||
# world-writable but not sticky
|
||||
return self._format_code(text, 'ow')
|
||||
# normal directory
|
||||
return self._format_code(text, 'di')
|
||||
|
||||
for mask, code in special_types:
|
||||
if (mode & mask) == mask:
|
||||
return self._format_code(text, code)
|
||||
|
||||
# executable file?
|
||||
if mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH):
|
||||
return self._format_code(text, 'ex')
|
||||
|
||||
# regular file, format according to its extension
|
||||
ext = os.path.splitext(text)[1]
|
||||
if ext:
|
||||
return self._format_ext(text, ext)
|
||||
return text
|
||||
|
||||
def __call__(self, path: str, text: str, cwd: Optional[Union[int, str]] = None) -> str:
|
||||
follow_symlinks = self.codes.get('ln') == 'target'
|
||||
try:
|
||||
sr = stat_at(path, cwd, follow_symlinks)
|
||||
except OSError:
|
||||
return text
|
||||
return self.format_mode(text, sr)
|
||||
|
||||
|
||||
def develop() -> None:
|
||||
import sys
|
||||
print(Dircolors()(sys.argv[-1], sys.argv[-1]))
|
||||
@@ -73,6 +73,11 @@ def clear_to_eol() -> str:
|
||||
return '\033[K'
|
||||
|
||||
|
||||
@cmd
|
||||
def reset_terminal() -> str:
|
||||
return '\033]\033\\\033c'
|
||||
|
||||
|
||||
@cmd
|
||||
def bell() -> str:
|
||||
return '\a'
|
||||
@@ -288,6 +293,20 @@ def alternate_screen(f: Optional[IO[str]] = None) -> Generator[None, None, None]
|
||||
print(reset_mode('ALTERNATE_SCREEN'), end='', file=f)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def raw_mode(fd: Optional[int] = None) -> Generator[None, None, None]:
|
||||
import tty
|
||||
import termios
|
||||
if fd is None:
|
||||
fd = sys.stdin.fileno()
|
||||
old = termios.tcgetattr(fd)
|
||||
try:
|
||||
tty.setraw(fd)
|
||||
yield
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
||||
|
||||
|
||||
@cmd
|
||||
def set_default_colors(
|
||||
fg: Optional[Union[Color, str]] = None,
|
||||
|
||||
150
kittens/tui/path_completer.py
Normal file
150
kittens/tui/path_completer.py
Normal file
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
import os
|
||||
from typing import Any, Callable, Dict, Generator, Optional, Sequence, Tuple
|
||||
|
||||
from kitty.fast_data_types import wcswidth
|
||||
from kitty.utils import ScreenSize, screen_size_function
|
||||
|
||||
from .operations import styled
|
||||
|
||||
|
||||
def directory_completions(path: str, qpath: str, prefix: str = '') -> Generator[str, None, None]:
|
||||
try:
|
||||
entries = os.scandir(qpath)
|
||||
except OSError:
|
||||
return
|
||||
for x in entries:
|
||||
try:
|
||||
is_dir = x.is_dir()
|
||||
except OSError:
|
||||
is_dir = False
|
||||
name = x.name + (os.sep if is_dir else '')
|
||||
if not prefix or name.startswith(prefix):
|
||||
if path:
|
||||
yield os.path.join(path, name)
|
||||
else:
|
||||
yield name
|
||||
|
||||
|
||||
def expand_path(path: str) -> str:
|
||||
return os.path.abspath(os.path.expandvars(os.path.expanduser(path)))
|
||||
|
||||
|
||||
def find_completions(path: str) -> Generator[str, None, None]:
|
||||
if path and path[0] == '~':
|
||||
if path == '~':
|
||||
yield '~' + os.sep
|
||||
return
|
||||
if os.sep not in path:
|
||||
qpath = os.path.expanduser(path)
|
||||
if qpath != path:
|
||||
yield path + os.sep
|
||||
return
|
||||
qpath = expand_path(path)
|
||||
if not path or path.endswith(os.sep):
|
||||
yield from directory_completions(path, qpath)
|
||||
else:
|
||||
yield from directory_completions(os.path.dirname(path), os.path.dirname(qpath), os.path.basename(qpath))
|
||||
|
||||
|
||||
def print_table(items: Sequence[str], screen_size: ScreenSize, dir_colors: Callable[[str, str], str]) -> None:
|
||||
max_width = 0
|
||||
item_widths = {}
|
||||
for item in items:
|
||||
item_widths[item] = w = wcswidth(item)
|
||||
max_width = max(w, max_width)
|
||||
col_width = max_width + 2
|
||||
num_of_cols = max(1, screen_size.cols // col_width)
|
||||
cr = 0
|
||||
at_start = False
|
||||
for item in items:
|
||||
w = item_widths[item]
|
||||
left = col_width - w
|
||||
print(dir_colors(expand_path(item), item), ' ' * left, sep='', end='')
|
||||
at_start = False
|
||||
cr = (cr + 1) % num_of_cols
|
||||
if not cr:
|
||||
print()
|
||||
at_start = True
|
||||
if not at_start:
|
||||
print()
|
||||
|
||||
|
||||
class PathCompleter:
|
||||
|
||||
def __init__(self, prompt: str = '> '):
|
||||
self.prompt = prompt
|
||||
self.prompt_len = wcswidth(self.prompt)
|
||||
|
||||
def __enter__(self) -> 'PathCompleter':
|
||||
import readline
|
||||
|
||||
from .dircolors import Dircolors
|
||||
if 'libedit' in readline.__doc__:
|
||||
readline.parse_and_bind("bind -e")
|
||||
readline.parse_and_bind("bind '\t' rl_complete")
|
||||
else:
|
||||
readline.parse_and_bind('tab: complete')
|
||||
readline.parse_and_bind('set colored-stats on')
|
||||
readline.set_completer_delims(' \t\n`!@#$%^&*()-=+[{]}\\|;:\'",<>?')
|
||||
readline.set_completion_display_matches_hook(self.format_completions)
|
||||
self.original_completer = readline.get_completer()
|
||||
readline.set_completer(self)
|
||||
self.cache: Dict[str, Tuple[str, ...]] = {}
|
||||
self.dircolors = Dircolors()
|
||||
return self
|
||||
|
||||
def format_completions(self, substitution: str, matches: Sequence[str], longest_match_length: int) -> None:
|
||||
import readline
|
||||
print()
|
||||
files, dirs = [], []
|
||||
for m in matches:
|
||||
if m.endswith('/'):
|
||||
if len(m) > 1:
|
||||
m = m[:-1]
|
||||
dirs.append(m)
|
||||
else:
|
||||
files.append(m)
|
||||
|
||||
ss = screen_size_function()()
|
||||
if dirs:
|
||||
print(styled('Directories', bold=True, fg_intense=True))
|
||||
print_table(dirs, ss, self.dircolors)
|
||||
if files:
|
||||
print(styled('Files', bold=True, fg_intense=True))
|
||||
print_table(files, ss, self.dircolors)
|
||||
|
||||
buf = readline.get_line_buffer()
|
||||
x = readline.get_endidx()
|
||||
buflen = wcswidth(buf)
|
||||
print(self.prompt, buf, sep='', end='')
|
||||
if x < buflen:
|
||||
pos = x + self.prompt_len
|
||||
print(f"\r\033[{pos}C", end='')
|
||||
print(sep='', end='', flush=True)
|
||||
|
||||
def __call__(self, text: str, state: int) -> Optional[str]:
|
||||
options = self.cache.get(text)
|
||||
if options is None:
|
||||
options = self.cache[text] = tuple(find_completions(text))
|
||||
if options and state < len(options):
|
||||
return options[state]
|
||||
|
||||
def __exit__(self, *a: Any) -> bool:
|
||||
import readline
|
||||
del self.cache
|
||||
readline.set_completer(self.original_completer)
|
||||
readline.set_completion_display_matches_hook()
|
||||
return True
|
||||
|
||||
def input(self) -> str:
|
||||
with self:
|
||||
return input(self.prompt)
|
||||
|
||||
|
||||
def develop() -> None:
|
||||
PathCompleter().input()
|
||||
29
kittens/tui/utils.py
Normal file
29
kittens/tui/utils.py
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import sys
|
||||
from contextlib import suppress
|
||||
|
||||
from .operations import raw_mode, set_cursor_visible
|
||||
|
||||
|
||||
def get_key_press(allowed: str, default: str) -> str:
|
||||
response = default
|
||||
with raw_mode():
|
||||
print(set_cursor_visible(False), end='', flush=True)
|
||||
try:
|
||||
while True:
|
||||
q = sys.stdin.buffer.read(1)
|
||||
if q:
|
||||
if q in b'\x1b\x03':
|
||||
break
|
||||
with suppress(Exception):
|
||||
response = q.decode('utf-8').lower()
|
||||
if response in allowed:
|
||||
break
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
pass
|
||||
finally:
|
||||
print(set_cursor_visible(True), end='', flush=True)
|
||||
return response
|
||||
61717
kittens/unicode_input/names.h
generated
61717
kittens/unicode_input/names.h
generated
File diff suppressed because one or more lines are too long
@@ -69,6 +69,7 @@ class Borders:
|
||||
self.os_window_id = os_window_id
|
||||
self.tab_id = tab_id
|
||||
self.draw_active_borders = opts.active_border_color is not None
|
||||
self.draw_minimal_borders = opts.draw_minimal_borders
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
@@ -88,29 +89,24 @@ class Borders:
|
||||
if groups:
|
||||
bw = groups[0].effective_border()
|
||||
draw_borders = bw > 0 and draw_window_borders
|
||||
if draw_borders:
|
||||
border_data = current_layout.resolve_borders(all_windows)
|
||||
active_group = all_windows.active_group
|
||||
|
||||
for i, wg in enumerate(groups):
|
||||
window_bg = wg.default_bg
|
||||
window_bg = (window_bg << 8) | BorderColor.window_bg
|
||||
if draw_borders:
|
||||
if draw_borders and not self.draw_minimal_borders:
|
||||
# Draw the border rectangles
|
||||
if wg is active_group and self.draw_active_borders:
|
||||
color = BorderColor.active
|
||||
else:
|
||||
color = BorderColor.bell if wg.needs_attention else BorderColor.inactive
|
||||
try:
|
||||
colors = tuple(color if needed else window_bg for needed in next(border_data))
|
||||
draw_edges(self.os_window_id, self.tab_id, colors, wg, borders=True)
|
||||
except StopIteration:
|
||||
pass
|
||||
draw_edges(self.os_window_id, self.tab_id, (color, color, color, color), wg, borders=True)
|
||||
if not has_background_image:
|
||||
# Draw the background rectangles over the padding region
|
||||
colors = window_bg, window_bg, window_bg, window_bg
|
||||
draw_edges(self.os_window_id, self.tab_id, colors, wg)
|
||||
|
||||
color = BorderColor.inactive
|
||||
for (left, top, right, bottom) in current_layout.window_independent_borders(all_windows):
|
||||
add_borders_rect(self.os_window_id, self.tab_id, left, top, right, bottom, color)
|
||||
if self.draw_minimal_borders:
|
||||
for border_line in current_layout.minimal_borders(all_windows):
|
||||
left, top, right, bottom = border_line.edges
|
||||
add_borders_rect(self.os_window_id, self.tab_id, left, top, right, bottom, border_line.color)
|
||||
|
||||
@@ -19,7 +19,7 @@ from .cli import create_opts, parse_args
|
||||
from .cli_stub import CLIOptions
|
||||
from .conf.utils import BadLine, to_cmdline
|
||||
from .config import (
|
||||
KeyAction, SubSequenceMap, common_opts_as_dict, initial_window_size_func,
|
||||
KeyAction, SubSequenceMap, common_opts_as_dict,
|
||||
prepare_config_file_for_editing
|
||||
)
|
||||
from .config_data import MINIMUM_FONT_SIZE
|
||||
@@ -28,7 +28,7 @@ from .constants import (
|
||||
)
|
||||
from .fast_data_types import (
|
||||
CLOSE_BEING_CONFIRMED, IMPERATIVE_CLOSE_REQUESTED, NO_CLOSE_REQUESTED,
|
||||
ChildMonitor, background_opacity_of, change_background_opacity,
|
||||
ChildMonitor, add_timer, background_opacity_of, change_background_opacity,
|
||||
change_os_window_state, cocoa_set_menubar_title, create_os_window,
|
||||
current_application_quit_request, current_os_window, destroy_global_data,
|
||||
focus_os_window, get_clipboard_string, global_font_size,
|
||||
@@ -39,9 +39,11 @@ from .fast_data_types import (
|
||||
)
|
||||
from .keys import get_shortcut, shortcut_matches
|
||||
from .layout.base import set_layout_options
|
||||
from .notify import notification_activated
|
||||
from .options_stub import Options
|
||||
from .os_window_size import initial_window_size_func
|
||||
from .rgb import Color, color_from_int
|
||||
from .session import Session, create_sessions
|
||||
from .session import Session, create_sessions, get_os_window_sizing_data
|
||||
from .tabs import (
|
||||
SpecialWindow, SpecialWindowInstance, Tab, TabDict, TabManager
|
||||
)
|
||||
@@ -62,12 +64,6 @@ class OSWindowDict(TypedDict):
|
||||
tabs: List[TabDict]
|
||||
|
||||
|
||||
def notification_activated(identifier: str) -> None:
|
||||
if identifier == 'new-version':
|
||||
from .update_check import notification_activated as do
|
||||
do()
|
||||
|
||||
|
||||
def listen_on(spec: str) -> int:
|
||||
import socket
|
||||
family, address, socket_path = parse_address_spec(spec)
|
||||
@@ -171,12 +167,18 @@ class Boss:
|
||||
if new_os_window_trigger is not None:
|
||||
self.keymap.pop(new_os_window_trigger, None)
|
||||
if is_macos:
|
||||
from .fast_data_types import cocoa_set_notification_activated_callback
|
||||
from .fast_data_types import (
|
||||
cocoa_set_notification_activated_callback
|
||||
)
|
||||
cocoa_set_notification_activated_callback(notification_activated)
|
||||
|
||||
def startup_first_child(self, os_window_id: Optional[int]) -> None:
|
||||
from kitty.launch import load_watch_modules
|
||||
startup_sessions = create_sessions(self.opts, self.args, default_session=self.opts.startup_session)
|
||||
watchers = load_watch_modules(self.args.watcher)
|
||||
for startup_session in startup_sessions:
|
||||
if watchers is not None and watchers.has_watchers:
|
||||
startup_session.add_watchers_to_all_windows(watchers)
|
||||
self.add_os_window(startup_session, os_window_id=os_window_id)
|
||||
os_window_id = None
|
||||
if self.args.start_as != 'normal':
|
||||
@@ -195,11 +197,11 @@ class Boss:
|
||||
startup_id: Optional[str] = None
|
||||
) -> int:
|
||||
if os_window_id is None:
|
||||
opts_for_size = opts_for_size or getattr(startup_session, 'os_window_size', None) or self.opts
|
||||
size_data = get_os_window_sizing_data(opts_for_size or self.opts, startup_session)
|
||||
wclass = wclass or getattr(startup_session, 'os_window_class', None) or self.args.cls or appname
|
||||
with startup_notification_handler(do_notify=startup_id is not None, startup_id=startup_id) as pre_show_callback:
|
||||
os_window_id = create_os_window(
|
||||
initial_window_size_func(opts_for_size, self.cached_values),
|
||||
initial_window_size_func(size_data, self.cached_values),
|
||||
pre_show_callback,
|
||||
self.args.title or appname, wname or self.args.name or wclass, wclass)
|
||||
tm = TabManager(os_window_id, self.opts, self.args, startup_session)
|
||||
@@ -349,8 +351,10 @@ class Boss:
|
||||
return response
|
||||
|
||||
def remote_control(self, *args: str) -> None:
|
||||
from .rc.base import (
|
||||
PayloadGetter, command_for_name, parse_subcommand_cli
|
||||
)
|
||||
from .remote_control import parse_rc_args
|
||||
from .rc.base import command_for_name, parse_subcommand_cli, PayloadGetter
|
||||
try:
|
||||
global_opts, items = parse_rc_args(['@'] + list(args))
|
||||
if not items:
|
||||
@@ -930,6 +934,9 @@ class Boss:
|
||||
kargs = shlex.split(cmdline) if cmdline else []
|
||||
self._run_kitten(kitten, kargs)
|
||||
|
||||
def run_kitten(self, kitten: str, *args: str) -> None:
|
||||
self._run_kitten(kitten, args)
|
||||
|
||||
def on_kitten_finish(self, target_window_id: str, end_kitten: Callable, source_window: Window) -> None:
|
||||
output = self.get_output(source_window, num_lines=None)
|
||||
from kittens.runner import deserialize
|
||||
@@ -1013,13 +1020,29 @@ class Boss:
|
||||
tab.set_active_window(window_id)
|
||||
|
||||
def open_url(self, url: str, program: Optional[Union[str, List[str]]] = None, cwd: Optional[str] = None) -> None:
|
||||
if url:
|
||||
if isinstance(program, str):
|
||||
program = to_cmdline(program)
|
||||
if not url:
|
||||
return
|
||||
if isinstance(program, str):
|
||||
program = to_cmdline(program)
|
||||
found_action = False
|
||||
if program is None:
|
||||
from .open_actions import actions_for_url
|
||||
actions = list(actions_for_url(url))
|
||||
if actions:
|
||||
found_action = True
|
||||
self.dispatch_action(actions.pop(0))
|
||||
if actions:
|
||||
self.drain_actions(actions)
|
||||
if not found_action:
|
||||
open_url(url, program or self.opts.open_url_with, cwd=cwd)
|
||||
|
||||
def open_url_lines(self, lines: Iterable[str], program: Optional[Union[str, List[str]]] = None) -> None:
|
||||
self.open_url(''.join(lines), program)
|
||||
def drain_actions(self, actions: List) -> None:
|
||||
|
||||
def callback(timer_id: Optional[int]) -> None:
|
||||
self.dispatch_action(actions.pop(0))
|
||||
if actions:
|
||||
self.drain_actions(actions)
|
||||
add_timer(callback, 0, False)
|
||||
|
||||
def destroy(self) -> None:
|
||||
self.shutting_down = True
|
||||
@@ -1283,7 +1306,7 @@ class Boss:
|
||||
self._new_window(list(args), cwd_from=cwd_from)
|
||||
|
||||
def launch(self, *args: str) -> None:
|
||||
from kitty.launch import parse_launch_args, launch
|
||||
from kitty.launch import launch, parse_launch_args
|
||||
opts, args_ = parse_launch_args(args)
|
||||
launch(self, opts, args_)
|
||||
|
||||
@@ -1356,7 +1379,9 @@ class Boss:
|
||||
log_error('Failed to process update check data {!r}, with error: {}'.format(raw, e))
|
||||
|
||||
def dbus_notification_callback(self, activated: bool, a: int, b: Union[int, str]) -> None:
|
||||
from .notify import dbus_notification_created, dbus_notification_activated
|
||||
from .notify import (
|
||||
dbus_notification_activated, dbus_notification_created
|
||||
)
|
||||
if activated:
|
||||
assert isinstance(b, str)
|
||||
dbus_notification_activated(a, b)
|
||||
@@ -1373,7 +1398,9 @@ class Boss:
|
||||
self.show_error(_('Errors in kitty.conf'), msg)
|
||||
|
||||
def set_colors(self, *args: str) -> None:
|
||||
from kitty.rc.base import parse_subcommand_cli, command_for_name, PayloadGetter
|
||||
from kitty.rc.base import (
|
||||
PayloadGetter, command_for_name, parse_subcommand_cli
|
||||
)
|
||||
from kitty.remote_control import parse_rc_args
|
||||
c = command_for_name('set_colors')
|
||||
opts, items = parse_subcommand_cli(c, ['set-colors'] + list(args))
|
||||
@@ -1535,3 +1562,21 @@ class Boss:
|
||||
set_background_image(path, os_windows, configured)
|
||||
for os_window_id in os_windows:
|
||||
self.default_bg_changed_for(os_window_id)
|
||||
|
||||
# Can be called with kitty -o "map f1 send_test_notification"
|
||||
def send_test_notification(self) -> None:
|
||||
from time import monotonic
|
||||
|
||||
from .notify import notify
|
||||
now = monotonic()
|
||||
ident = f'test-notify-{now}'
|
||||
notify(f'Test {now}', f'At: {now}', identifier=ident, subtitle=f'Test subtitle {now}')
|
||||
|
||||
def notification_activated(self, identifier: str, window_id: int, focus: bool, report: bool) -> None:
|
||||
w = self.window_id_map.get(window_id)
|
||||
if w is None:
|
||||
return
|
||||
if focus:
|
||||
self.set_active_window(w, switch_os_window_if_needed=True)
|
||||
if report:
|
||||
w.report_notification_activated(identifier)
|
||||
|
||||
@@ -236,7 +236,7 @@ schedule_write_to_child(unsigned long id, unsigned int num, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, num);
|
||||
for (unsigned int i = 0; i < num; i++) {
|
||||
data = va_arg(ap, const char*);
|
||||
va_arg(ap, const char*);
|
||||
sz += va_arg(ap, size_t);
|
||||
}
|
||||
va_end(ap);
|
||||
@@ -1539,7 +1539,7 @@ send_response(id_type peer_id, const char *msg, size_t msg_sz) {
|
||||
peer->write.capacity += msg_sz;
|
||||
} else fatal("Out of memory");
|
||||
}
|
||||
memcpy(peer->write.data + peer->write.used, msg, msg_sz);
|
||||
if (msg) memcpy(peer->write.data + peer->write.used, msg, msg_sz);
|
||||
peer->write.used += msg_sz;
|
||||
}
|
||||
wakeup = true;
|
||||
|
||||
18
kitty/cli.py
18
kitty/cli.py
@@ -544,6 +544,15 @@ def parse_cmdline(oc: Options, disabled: OptionSpecSeq, ans: Any, args: Optional
|
||||
return leftover_args
|
||||
|
||||
|
||||
WATCHER_DEFINITION = '''
|
||||
--watcher -w
|
||||
type=list
|
||||
Path to a python file. Appropriately named functions in this file will be called
|
||||
for various events, such as when the window is resized, focused or closed. See the section
|
||||
on watchers in the launch command documentation :doc:`launch`. Relative paths are
|
||||
resolved relative to the kitty config directory.'''
|
||||
|
||||
|
||||
def options_spec() -> str:
|
||||
if not hasattr(options_spec, 'ans'):
|
||||
OPTIONS = '''
|
||||
@@ -593,6 +602,11 @@ Path to a file containing the startup :italic:`session` (tabs, windows, layout,
|
||||
Use - to read from STDIN. See the README file for details and an example.
|
||||
|
||||
|
||||
{watcher}
|
||||
Note that this watcher will be added only to all initially created windows, not new windows
|
||||
created after startup.
|
||||
|
||||
|
||||
--hold
|
||||
type=bool-set
|
||||
Remain open after child process exits. Note that this only affects the first
|
||||
@@ -695,8 +709,8 @@ type=bool-set
|
||||
!
|
||||
'''
|
||||
setattr(options_spec, 'ans', OPTIONS.format(
|
||||
appname=appname, config_help=CONFIG_HELP.format(appname=appname, conf_name=appname)
|
||||
|
||||
appname=appname, config_help=CONFIG_HELP.format(appname=appname, conf_name=appname),
|
||||
watcher=WATCHER_DEFINITION
|
||||
))
|
||||
ans: str = getattr(options_spec, 'ans')
|
||||
return ans
|
||||
|
||||
@@ -12,7 +12,7 @@ class CLIOptions:
|
||||
|
||||
LaunchCLIOptions = AskCLIOptions = ClipboardCLIOptions = DiffCLIOptions = CLIOptions
|
||||
HintsCLIOptions = IcatCLIOptions = PanelCLIOptions = ResizeCLIOptions = CLIOptions
|
||||
ErrorCLIOptions = UnicodeCLIOptions = RCOptions = CLIOptions
|
||||
ErrorCLIOptions = UnicodeCLIOptions = RCOptions = RemoteFileCLIOptions = CLIOptions
|
||||
|
||||
|
||||
def generate_stub() -> None:
|
||||
@@ -35,6 +35,9 @@ def generate_stub() -> None:
|
||||
from kittens.ask.main import option_text
|
||||
do(option_text(), 'AskCLIOptions')
|
||||
|
||||
from kittens.remote_file.main import option_text
|
||||
do(option_text(), 'RemoteFileCLIOptions')
|
||||
|
||||
from kittens.clipboard.main import OPTIONS
|
||||
do(OPTIONS(), 'ClipboardCLIOptions')
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
#include "state.h"
|
||||
#include "monotonic.h"
|
||||
#include <Cocoa/Cocoa.h>
|
||||
#ifndef KITTY_USE_DEPRECATED_MACOS_NOTIFICATION_API
|
||||
#include <UserNotifications/UserNotifications.h>
|
||||
#endif
|
||||
|
||||
#include <AvailabilityMacros.h>
|
||||
// Needed for _NSGetProgname
|
||||
@@ -133,6 +136,7 @@ get_dock_menu(id self UNUSED, SEL _cmd UNUSED, NSApplication *sender UNUSED) {
|
||||
}
|
||||
|
||||
static PyObject *notification_activated_callback = NULL;
|
||||
|
||||
static PyObject*
|
||||
set_notification_activated_callback(PyObject *self UNUSED, PyObject *callback) {
|
||||
if (notification_activated_callback) Py_DECREF(notification_activated_callback);
|
||||
@@ -141,6 +145,8 @@ set_notification_activated_callback(PyObject *self UNUSED, PyObject *callback) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
#ifdef KITTY_USE_DEPRECATED_MACOS_NOTIFICATION_API
|
||||
|
||||
@interface NotificationDelegate : NSObject <NSUserNotificationCenterDelegate>
|
||||
@end
|
||||
|
||||
@@ -170,24 +176,12 @@ set_notification_activated_callback(PyObject *self UNUSED, PyObject *callback) {
|
||||
|
||||
static PyObject*
|
||||
cocoa_send_notification(PyObject *self UNUSED, PyObject *args) {
|
||||
char *identifier = NULL, *title = NULL, *subtitle = NULL, *informativeText = NULL, *path_to_image = NULL;
|
||||
if (!PyArg_ParseTuple(args, "zssz|z", &identifier, &title, &informativeText, &path_to_image, &subtitle)) return NULL;
|
||||
char *identifier = NULL, *title = NULL, *informativeText = NULL, *subtitle = NULL;
|
||||
if (!PyArg_ParseTuple(args, "zsz|z", &identifier, &title, &informativeText, &subtitle)) return NULL;
|
||||
NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
|
||||
if (!center) {PyErr_SetString(PyExc_RuntimeError, "Failed to get the user notification center"); return NULL; }
|
||||
if (!center.delegate) center.delegate = [[NotificationDelegate alloc] init];
|
||||
NSUserNotification *n = [NSUserNotification new];
|
||||
NSImage *img = nil;
|
||||
if (path_to_image) {
|
||||
NSString *p = @(path_to_image);
|
||||
NSURL *url = [NSURL fileURLWithPath:p];
|
||||
img = [[NSImage alloc] initWithContentsOfURL:url];
|
||||
[url release]; [p release];
|
||||
if (img) {
|
||||
[n setValue:img forKey:@"_identityImage"];
|
||||
[n setValue:@(false) forKey:@"_identityImageHasBorder"];
|
||||
}
|
||||
[img release];
|
||||
}
|
||||
#define SET(x) { \
|
||||
if (x) { \
|
||||
NSString *t = @(x); \
|
||||
@@ -203,6 +197,111 @@ cocoa_send_notification(PyObject *self UNUSED, PyObject *args) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
@interface NotificationDelegate : NSObject <UNUserNotificationCenterDelegate>
|
||||
@end
|
||||
|
||||
@implementation NotificationDelegate
|
||||
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
|
||||
didReceiveNotificationResponse:(UNNotificationResponse *)response
|
||||
withCompletionHandler:(void (^)(void))completionHandler {
|
||||
(void)(center);
|
||||
if (notification_activated_callback) {
|
||||
NSString *identifier = [[[response notification] request] identifier];
|
||||
PyObject *ret = PyObject_CallFunction(notification_activated_callback, "z",
|
||||
identifier ? [identifier UTF8String] : NULL);
|
||||
if (ret == NULL) PyErr_Print();
|
||||
else Py_DECREF(ret);
|
||||
}
|
||||
completionHandler();
|
||||
}
|
||||
@end
|
||||
|
||||
|
||||
static void
|
||||
schedule_notification(const char *identifier, const char *title, const char *body, const char *subtitle) {
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
if (!center) return;
|
||||
// Configure the notification's payload.
|
||||
UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
|
||||
if (title) content.title = @(title);
|
||||
if (body) content.body = @(body);
|
||||
if (subtitle) content.subtitle = @(subtitle);
|
||||
// Deliver the notification
|
||||
static unsigned long counter = 1;
|
||||
UNNotificationRequest* request = [
|
||||
UNNotificationRequest requestWithIdentifier:(identifier ? @(identifier) : [NSString stringWithFormat:@"Id_%lu", counter++])
|
||||
content:content trigger:nil];
|
||||
[center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
|
||||
if (error != nil) {
|
||||
log_error("Failed to show notification: %s", [[error localizedDescription] UTF8String]);
|
||||
}
|
||||
}];
|
||||
[content release];
|
||||
}
|
||||
|
||||
|
||||
typedef struct {
|
||||
char *identifier, *title, *body, *subtitle;
|
||||
} QueuedNotification;
|
||||
|
||||
typedef struct {
|
||||
QueuedNotification *notifications;
|
||||
size_t count, capacity;
|
||||
} NotificationQueue;
|
||||
static NotificationQueue notification_queue = {0};
|
||||
|
||||
static void
|
||||
queue_notification(const char *identifier, const char *title, const char* body, const char* subtitle) {
|
||||
ensure_space_for((¬ification_queue), notifications, QueuedNotification, notification_queue.count + 16, capacity, 16, true);
|
||||
QueuedNotification *n = notification_queue.notifications + notification_queue.count++;
|
||||
n->identifier = identifier ? strdup(identifier) : NULL;
|
||||
n->title = title ? strdup(title) : NULL;
|
||||
n->body = body ? strdup(body) : NULL;
|
||||
n->subtitle = subtitle ? strdup(subtitle) : NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
drain_pending_notifications(BOOL granted) {
|
||||
if (granted) {
|
||||
for (size_t i = 0; i < notification_queue.count; i++) {
|
||||
QueuedNotification *n = notification_queue.notifications + i;
|
||||
schedule_notification(n->identifier, n->title, n->body, n->subtitle);
|
||||
}
|
||||
}
|
||||
while(notification_queue.count) {
|
||||
QueuedNotification *n = notification_queue.notifications + --notification_queue.count;
|
||||
free(n->identifier); free(n->title); free(n->body); free(n->subtitle);
|
||||
n->identifier = NULL; n->title = NULL; n->body = NULL; n->subtitle = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
cocoa_send_notification(PyObject *self UNUSED, PyObject *args) {
|
||||
char *identifier = NULL, *title = NULL, *body = NULL, *subtitle = NULL;
|
||||
if (!PyArg_ParseTuple(args, "zsz|z", &identifier, &title, &body, &subtitle)) return NULL;
|
||||
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
if (!center) Py_RETURN_NONE;
|
||||
if (!center.delegate) center.delegate = [[NotificationDelegate alloc] init];
|
||||
queue_notification(identifier, title, body, subtitle);
|
||||
|
||||
[center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert)
|
||||
completionHandler:^(BOOL granted, NSError * _Nullable error) {
|
||||
if (error != nil) {
|
||||
log_error("Failed to request permission for showing notification: %s", [[error localizedDescription] UTF8String]);
|
||||
}
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
drain_pending_notifications(granted);
|
||||
});
|
||||
}
|
||||
];
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@interface ServiceProvider : NSObject
|
||||
@end
|
||||
|
||||
@@ -497,8 +596,15 @@ cleanup() {
|
||||
|
||||
if (dockMenu) [dockMenu release];
|
||||
dockMenu = nil;
|
||||
if (notification_activated_callback) Py_DECREF(notification_activated_callback);
|
||||
notification_activated_callback = NULL;
|
||||
Py_CLEAR(notification_activated_callback);
|
||||
|
||||
#ifndef KITTY_USE_DEPRECATED_MACOS_NOTIFICATION_API
|
||||
drain_pending_notifications(NO);
|
||||
free(notification_queue.notifications);
|
||||
notification_queue.notifications = NULL;
|
||||
notification_queue.capacity = 0;
|
||||
#endif
|
||||
|
||||
} // autoreleasepool
|
||||
}
|
||||
|
||||
@@ -537,6 +643,11 @@ cocoa_hide_titlebar(void *w)
|
||||
} // autoreleasepool
|
||||
}
|
||||
|
||||
void
|
||||
cocoa_system_beep(void) {
|
||||
NSBeep();
|
||||
}
|
||||
|
||||
static PyMethodDef module_methods[] = {
|
||||
{"cocoa_get_lang", (PyCFunction)cocoa_get_lang, METH_NOARGS, ""},
|
||||
{"cocoa_set_new_window_trigger", (PyCFunction)cocoa_set_new_window_trigger, METH_VARARGS, ""},
|
||||
|
||||
@@ -254,6 +254,8 @@ def complete_kitty_cli_arg(ans: Completions, opt: Optional[OptionDict], prefix:
|
||||
complete_files_and_dirs(ans, prefix, files_group_name='Config files', predicate=is_conf_file)
|
||||
elif dest == 'session':
|
||||
complete_files_and_dirs(ans, prefix, files_group_name='Session files')
|
||||
elif dest == 'watcher':
|
||||
complete_files_and_dirs(ans, prefix, files_group_name='Watcher files')
|
||||
elif dest == 'directory':
|
||||
complete_files_and_dirs(ans, prefix, files_group_name='Directories', predicate=os.path.isdir)
|
||||
elif dest == 'start_as':
|
||||
|
||||
@@ -11,7 +11,7 @@ from typing import (
|
||||
)
|
||||
|
||||
from ..rgb import Color, to_color as as_color
|
||||
from ..utils import log_error
|
||||
from ..utils import log_error, expandvars
|
||||
|
||||
key_pat = re.compile(r'([a-zA-Z][a-zA-Z0-9_-]*)\s+(.+)$')
|
||||
T = TypeVar('T')
|
||||
@@ -53,13 +53,36 @@ def to_bool(x: str) -> bool:
|
||||
return x.lower() in ('y', 'yes', 'true')
|
||||
|
||||
|
||||
def to_cmdline(x: str) -> List[str]:
|
||||
return list(
|
||||
map(
|
||||
lambda y: os.path.expandvars(os.path.expanduser(y)),
|
||||
shlex.split(x)
|
||||
class ToCmdline:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.override_env: Optional[Dict[str, str]] = None
|
||||
|
||||
def __enter__(self) -> 'ToCmdline':
|
||||
return self
|
||||
|
||||
def __exit__(self, *a: Any) -> None:
|
||||
self.override_env = None
|
||||
|
||||
def filter_env_vars(self, *a: str) -> 'ToCmdline':
|
||||
remove = frozenset(a)
|
||||
self.override_env = {k: v for k, v in os.environ.items() if k not in remove}
|
||||
return self
|
||||
|
||||
def __call__(self, x: str) -> List[str]:
|
||||
return list(
|
||||
map(
|
||||
lambda y: expandvars(
|
||||
os.path.expanduser(y),
|
||||
os.environ if self.override_env is None else self.override_env,
|
||||
fallback_to_os_env=False
|
||||
),
|
||||
shlex.split(x)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
to_cmdline = ToCmdline()
|
||||
|
||||
|
||||
def python_string(text: str) -> str:
|
||||
|
||||
@@ -23,7 +23,7 @@ from .config_data import all_options, parse_mods, type_convert
|
||||
from .constants import cache_dir, defconf, is_macos
|
||||
from .key_names import get_key_name_lookup, key_name_aliases
|
||||
from .options_stub import Options as OptionsStub
|
||||
from .typing import EdgeLiteral, TypedDict
|
||||
from .typing import TypedDict
|
||||
from .utils import expandvars, log_error
|
||||
|
||||
KeySpec = Tuple[int, bool, int]
|
||||
@@ -393,7 +393,7 @@ class KeyDefinition:
|
||||
if len(expanded) > 1:
|
||||
rest = expanded[1] + ' ' + rest
|
||||
if changed:
|
||||
self.action = self.action._replace(args=[kitten + (' ' + rest).rstrip()])
|
||||
self.action = self.action._replace(args=[kitten, rest.rstrip()])
|
||||
|
||||
|
||||
def parse_key(val: str, key_definitions: List[KeyDefinition]) -> None:
|
||||
@@ -718,51 +718,6 @@ def cached_values_for(name: str) -> Generator[Dict, None, None]:
|
||||
err))
|
||||
|
||||
|
||||
def initial_window_size_func(opts: OptionsStub, cached_values: Dict) -> Callable[[int, int, float, float, float, float], Tuple[int, int]]:
|
||||
|
||||
if 'window-size' in cached_values and opts.remember_window_size:
|
||||
ws = cached_values['window-size']
|
||||
try:
|
||||
w, h = map(int, ws)
|
||||
|
||||
def initial_window_size(*a: Any) -> Tuple[int, int]:
|
||||
return w, h
|
||||
return initial_window_size
|
||||
except Exception:
|
||||
log_error('Invalid cached window size, ignoring')
|
||||
|
||||
w, w_unit = opts.initial_window_width
|
||||
h, h_unit = opts.initial_window_height
|
||||
|
||||
def get_window_size(cell_width: int, cell_height: int, dpi_x: float, dpi_y: float, xscale: float, yscale: float) -> Tuple[int, int]:
|
||||
if not is_macos:
|
||||
# scaling is not needed on Wayland, but is needed on macOS. Not
|
||||
# sure about X11.
|
||||
xscale = yscale = 1
|
||||
|
||||
def effective_margin(which: EdgeLiteral) -> float:
|
||||
ans: float = getattr(opts.single_window_margin_width, which)
|
||||
if ans < 0:
|
||||
ans = getattr(opts.window_margin_width, which)
|
||||
return ans
|
||||
|
||||
if w_unit == 'cells':
|
||||
spacing = effective_margin('left') + effective_margin('right')
|
||||
spacing += opts.window_padding_width.left + opts.window_padding_width.right
|
||||
width = cell_width * w / xscale + (dpi_x / 72) * spacing + 1
|
||||
else:
|
||||
width = w
|
||||
if h_unit == 'cells':
|
||||
spacing = effective_margin('top') + effective_margin('bottom')
|
||||
spacing += opts.window_padding_width.top + opts.window_padding_width.bottom
|
||||
height = cell_height * h / yscale + (dpi_y / 72) * spacing + 1
|
||||
else:
|
||||
height = h
|
||||
return int(width), int(height)
|
||||
|
||||
return get_window_size
|
||||
|
||||
|
||||
def commented_out_default_config() -> str:
|
||||
ans = []
|
||||
for line in as_conf_file(all_options.values()):
|
||||
|
||||
@@ -13,7 +13,7 @@ from typing import (
|
||||
from . import fast_data_types as defines
|
||||
from .conf.definition import Option, Shortcut, option_func
|
||||
from .conf.utils import (
|
||||
choices, positive_float, positive_int, to_bool, to_cmdline, to_color,
|
||||
choices, positive_float, positive_int, to_bool, to_cmdline as tc, to_color,
|
||||
to_color_or_none, unit_float
|
||||
)
|
||||
from .constants import FloatEdges, config_dir, is_macos
|
||||
@@ -29,6 +29,10 @@ mod_map = {'CTRL': 'CONTROL', 'CMD': 'SUPER', '⌘': 'SUPER',
|
||||
'⌥': 'ALT', 'OPTION': 'ALT', 'KITTY_MOD': 'KITTY'}
|
||||
|
||||
|
||||
def to_cmdline(x: str) -> List[str]:
|
||||
return tc(x)
|
||||
|
||||
|
||||
def parse_mods(parts: Iterable[str], sc: str) -> Optional[int]:
|
||||
|
||||
def map_mod(m: str) -> str:
|
||||
@@ -321,7 +325,9 @@ Note that this code is indexed by PostScript name, and not the font
|
||||
family. This allows you to define very precise feature settings; e.g. you can
|
||||
disable a feature in the italic font but not in the regular font.
|
||||
|
||||
To get the PostScript name for a font, use :code:`kitty + list-fonts --psnames`::
|
||||
To get the PostScript name for a font, use :code:`kitty + list-fonts --psnames`:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
$ kitty + list-fonts --psnames | grep Fira
|
||||
Fira Code
|
||||
@@ -440,8 +446,8 @@ def scrollback_pager_history_size(x: str) -> int:
|
||||
o('scrollback_lines', 2000, option_type=scrollback_lines, long_text=_('''
|
||||
Number of lines of history to keep in memory for scrolling back. Memory is allocated
|
||||
on demand. Negative numbers are (effectively) infinite scrollback. Note that using
|
||||
very large scrollback is not recommended as it can slow down resizing of the terminal
|
||||
and also use large amounts of RAM.'''))
|
||||
very large scrollback is not recommended as it can slow down performance of the terminal
|
||||
and also use large amounts of RAM. Instead, consider using :opt:`scrollback_pager_history_size`.'''))
|
||||
|
||||
o('scrollback_pager', 'less --chop-long-lines --RAW-CONTROL-CHARS +INPUT_LINE_NUMBER', option_type=to_cmdline, long_text=_('''
|
||||
Program with which to view scrollback in a new window. The scrollback buffer is
|
||||
@@ -455,9 +461,9 @@ o('scrollback_pager_history_size', 0, option_type=scrollback_pager_history_size,
|
||||
Separate scrollback history size, used only for browsing the scrollback buffer (in MB).
|
||||
This separate buffer is not available for interactive scrolling but will be
|
||||
piped to the pager program when viewing scrollback buffer in a separate window.
|
||||
The current implementation stores one character in 4 bytes, so approximatively
|
||||
2500 lines per megabyte at 100 chars per line. A value of zero or less disables
|
||||
this feature. The maximum allowed size is 4GB.'''))
|
||||
The current implementation stores the data in UTF-8, so approximatively
|
||||
10000 lines per megabyte at 100 chars per line, for pure ASCII text, unformatted text.
|
||||
A value of zero or less disables this feature. The maximum allowed size is 4GB.'''))
|
||||
|
||||
o('wheel_scroll_multiplier', 5.0, long_text=_('''
|
||||
Modify the amount scrolled by the mouse wheel. Note this is only used for low
|
||||
@@ -668,9 +674,27 @@ for vertical resizing.
|
||||
'''))
|
||||
o('window_resize_step_lines', 2, option_type=positive_int)
|
||||
|
||||
o('window_border_width', 1.0, option_type=positive_float, long_text=_('''
|
||||
The width (in pts) of window borders. Will be rounded to the nearest number of pixels based on screen resolution.
|
||||
Note that borders are displayed only when more than one window is visible. They are meant to separate multiple windows.'''))
|
||||
|
||||
def window_border_width(x: Union[str, int, float]) -> Tuple[float, str]:
|
||||
unit = 'pt'
|
||||
if isinstance(x, str):
|
||||
trailer = x[-2:]
|
||||
if trailer in ('px', 'pt'):
|
||||
unit = trailer
|
||||
val = float(x[:-2])
|
||||
else:
|
||||
val = float(x)
|
||||
else:
|
||||
val = float(x)
|
||||
return max(0, val), unit
|
||||
|
||||
|
||||
o('window_border_width', '0.5pt', option_type=window_border_width, long_text=_('''
|
||||
The width of window borders. Can be either in pixels (px) or pts (pt). Values
|
||||
in pts will be rounded to the nearest number of pixels based on screen
|
||||
resolution. If not specified the unit is assumed to be pts.
|
||||
Note that borders are displayed only when more than one window
|
||||
is visible. They are meant to separate multiple windows.'''))
|
||||
|
||||
o('draw_minimal_borders', True, long_text=_('''
|
||||
Draw only the minimum borders needed. This means that only the minimum
|
||||
@@ -863,6 +887,17 @@ o('tab_separator', '"{}"'.format(default_tab_separator), option_type=tab_separat
|
||||
The separator between tabs in the tab bar when using :code:`separator` as the :opt:`tab_bar_style`.'''))
|
||||
|
||||
|
||||
def tab_activity_symbol(x: str) -> Optional[str]:
|
||||
if x == 'none':
|
||||
return None
|
||||
return x or None
|
||||
|
||||
|
||||
o('tab_activity_symbol', 'none', option_type=tab_activity_symbol, long_text=_('''
|
||||
Some text or a unicode symbol to show on the tab if a window in the tab that does
|
||||
not have focus has some activity.'''))
|
||||
|
||||
|
||||
def tab_title_template(x: str) -> str:
|
||||
if x:
|
||||
for q in '\'"':
|
||||
@@ -1113,6 +1148,20 @@ that enabling the read functionality is a security risk as it means that any
|
||||
program, even one running on a remote server via SSH can read your clipboard.
|
||||
'''))
|
||||
|
||||
|
||||
def allow_hyperlinks(x: str) -> int:
|
||||
if x == 'ask':
|
||||
return 0b11
|
||||
return 1 if to_bool(x) else 0
|
||||
|
||||
|
||||
o('allow_hyperlinks', 'yes', option_type=allow_hyperlinks, long_text=_('''
|
||||
Process hyperlink (OSC 8) escape sequences. If disabled OSC 8 escape
|
||||
sequences are ignored. Otherwise they become clickable links, that you
|
||||
can click by holding down ctrl+shift and clicking with the mouse. The special
|
||||
value of ``ask`` means that kitty will ask before opening the link.'''))
|
||||
|
||||
|
||||
o('term', 'xterm-kitty', long_text=_('''
|
||||
The value of the TERM environment variable to set. Changing this can break many
|
||||
terminal programs, only change it if you know what you are doing, not because
|
||||
@@ -1438,6 +1487,9 @@ k('goto_file_line', 'kitty_mod+p>n', 'kitten hints --type linenum', _('Open the
|
||||
Select something that looks like :code:`filename:linenum` and open it in vim at
|
||||
the specified line number.'''))
|
||||
|
||||
k('open_selected_hyperlink', 'kitty_mod+p>y', 'kitten hints --type hyperlink', _('Open the selected hyperlink'), long_text=_('''
|
||||
Select a hyperlink (i.e. a URL that has been marked as such by the terminal program, for example, by ls --hyperlink=auto).
|
||||
'''))
|
||||
|
||||
# }}}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class Version(NamedTuple):
|
||||
|
||||
|
||||
appname: str = 'kitty'
|
||||
version: Version = Version(0, 18, 2)
|
||||
version: Version = Version(0, 19, 1)
|
||||
str_version: str = '.'.join(map(str, version))
|
||||
_plat = sys.platform.lower()
|
||||
is_macos: bool = 'darwin' in _plat
|
||||
@@ -64,16 +64,16 @@ class WindowGeometry(NamedTuple):
|
||||
def kitty_exe() -> str:
|
||||
rpath = sys._xoptions.get('bundle_exe_dir')
|
||||
if not rpath:
|
||||
items = filter(None, os.environ.get('PATH', '').split(os.pathsep))
|
||||
items = os.environ.get('PATH', '').split(os.pathsep) + [os.path.join(base, 'launcher')]
|
||||
seen: Set[str] = set()
|
||||
for candidate in items:
|
||||
for candidate in filter(None, items):
|
||||
if candidate not in seen:
|
||||
seen.add(candidate)
|
||||
if os.access(os.path.join(candidate, 'kitty'), os.X_OK):
|
||||
rpath = candidate
|
||||
break
|
||||
else:
|
||||
rpath = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'launcher')
|
||||
raise RuntimeError('kitty binary not found')
|
||||
return os.path.join(rpath, 'kitty')
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ def wakeup() -> None:
|
||||
b.child_monitor.wakeup()
|
||||
|
||||
|
||||
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
base_dir = os.path.dirname(base)
|
||||
terminfo_dir = os.path.join(base_dir, 'terminfo')
|
||||
logo_data_file = os.path.join(base_dir, 'logo', 'kitty.rgba')
|
||||
logo_png_file = os.path.join(base_dir, 'logo', 'kitty.png')
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
#endif
|
||||
#include "data-types.h"
|
||||
#include "control-codes.h"
|
||||
#include "wcwidth-std.h"
|
||||
#include "wcswidth.h"
|
||||
#include "modes.h"
|
||||
#include <stddef.h>
|
||||
#include <termios.h>
|
||||
@@ -148,7 +150,15 @@ close_tty(PyObject *self UNUSED, PyObject *args) {
|
||||
|
||||
#undef TTY_ARGS
|
||||
|
||||
static PyObject*
|
||||
wcwidth_wrap(PyObject UNUSED *self, PyObject *chr) {
|
||||
return PyLong_FromLong(wcwidth_std(PyLong_AsLong(chr)));
|
||||
}
|
||||
|
||||
|
||||
static PyMethodDef module_methods[] = {
|
||||
{"wcwidth", (PyCFunction)wcwidth_wrap, METH_O, ""},
|
||||
{"wcswidth", (PyCFunction)wcswidth_std, METH_O, ""},
|
||||
{"open_tty", open_tty, METH_VARARGS, ""},
|
||||
{"normal_tty", normal_tty, METH_VARARGS, ""},
|
||||
{"raw_tty", raw_tty, METH_VARARGS, ""},
|
||||
|
||||
@@ -41,6 +41,8 @@ void log_error(const char *fmt, ...) __attribute__ ((format (printf, 1, 2)));
|
||||
typedef unsigned long long id_type;
|
||||
typedef uint32_t char_type;
|
||||
typedef uint32_t color_type;
|
||||
typedef uint16_t hyperlink_id_type;
|
||||
#define HYPERLINK_MAX_NUMBER UINT16_MAX
|
||||
typedef uint16_t combining_type;
|
||||
typedef uint32_t pixel;
|
||||
typedef unsigned int index_type;
|
||||
@@ -160,6 +162,7 @@ typedef struct {
|
||||
typedef struct {
|
||||
char_type ch;
|
||||
combining_type cc_idx[2];
|
||||
hyperlink_id_type hyperlink_id;
|
||||
} CPUCell;
|
||||
|
||||
|
||||
@@ -190,13 +193,20 @@ typedef struct {
|
||||
} HistoryBufSegment;
|
||||
|
||||
typedef struct {
|
||||
index_type bufsize, maxsz;
|
||||
Py_UCS4 *buffer;
|
||||
index_type start, end;
|
||||
index_type bufend;
|
||||
uint8_t *buffer;
|
||||
size_t buffer_size, max_sz;
|
||||
size_t start, length;
|
||||
bool rewrap_needed;
|
||||
} PagerHistoryBuf;
|
||||
|
||||
typedef struct {int x;} *HYPERLINK_POOL_HANDLE;
|
||||
typedef struct {
|
||||
Py_UCS4 *buf;
|
||||
size_t len, capacity;
|
||||
HYPERLINK_POOL_HANDLE hyperlink_pool;
|
||||
hyperlink_id_type active_hyperlink_id;
|
||||
} ANSIBuf;
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
|
||||
@@ -311,6 +321,7 @@ void play_canberra_sound(const char *which_sound, const char *event_id);
|
||||
#endif
|
||||
SPRITE_MAP_HANDLE alloc_sprite_map(unsigned int, unsigned int);
|
||||
SPRITE_MAP_HANDLE free_sprite_map(SPRITE_MAP_HANDLE);
|
||||
const char* get_hyperlink_for_id(const HYPERLINK_POOL_HANDLE, hyperlink_id_type id, bool only_url);
|
||||
|
||||
static inline void safe_close(int fd, const char* file UNUSED, const int line UNUSED) {
|
||||
#if 0
|
||||
|
||||
2
kitty/emoji.h
generated
2
kitty/emoji.h
generated
@@ -1,4 +1,4 @@
|
||||
// unicode data, built from the unicode standard on: 2020-04-06
|
||||
// unicode data, built from the unicode standard on: 2020-09-22
|
||||
// see gen-wcwidth.py
|
||||
#pragma once
|
||||
#include "data-types.h"
|
||||
|
||||
@@ -461,7 +461,7 @@ def coretext_all_fonts() -> Tuple[CoreTextFont, ...]:
|
||||
|
||||
|
||||
def add_timer(
|
||||
callback: Callable[[Optional[int]], bool],
|
||||
callback: Callable[[Optional[int]], None],
|
||||
interval: float,
|
||||
repeats: bool = True
|
||||
) -> int:
|
||||
@@ -519,9 +519,8 @@ def dbus_send_notification(
|
||||
def cocoa_send_notification(
|
||||
identifier: Optional[str],
|
||||
title: str,
|
||||
informative_text: str,
|
||||
path_to_img: Optional[str],
|
||||
subtitle: Optional[str] = None
|
||||
body: Optional[str],
|
||||
subtitle: Optional[str],
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@@ -945,7 +944,10 @@ class HistoryBuf:
|
||||
def as_text(self, callback: Callable[[str], None], as_ansi: bool, insert_wrap_markers: bool) -> None:
|
||||
pass
|
||||
|
||||
def pagerhist_as_text(self, callback: Callable[[str], None]) -> None:
|
||||
def pagerhist_as_text(self) -> str:
|
||||
pass
|
||||
|
||||
def pagerhist_as_bytes(self) -> bytes:
|
||||
pass
|
||||
|
||||
|
||||
@@ -1065,6 +1067,15 @@ class Screen:
|
||||
def erase_in_display(self, how: int = 0, private: bool = False) -> None:
|
||||
pass
|
||||
|
||||
def focus_changed(self, focused: bool) -> bool:
|
||||
pass
|
||||
|
||||
def has_focus(self) -> bool:
|
||||
pass
|
||||
|
||||
def has_activity_since_last_focus(self) -> bool:
|
||||
pass
|
||||
|
||||
|
||||
def set_tab_bar_render_data(
|
||||
os_window_id: int, xstart: float, ystart: float, dx: float, dy: float,
|
||||
|
||||
@@ -130,12 +130,48 @@ font_group_is_unused(FontGroup *fg) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
free_maps(Font *font) {
|
||||
#define free_a_map(type, attr) {\
|
||||
type *s, *t; \
|
||||
for (size_t i = 0; i < sizeof(font->attr)/sizeof(font->attr[0]); i++) { \
|
||||
s = font->attr[i].next; \
|
||||
while (s) { \
|
||||
t = s; \
|
||||
s = s->next; \
|
||||
free(t); \
|
||||
} \
|
||||
}\
|
||||
memset(font->attr, 0, sizeof(font->attr)); \
|
||||
}
|
||||
free_a_map(SpritePosition, sprite_map);
|
||||
free_a_map(SpecialGlyphCache, special_glyph_cache);
|
||||
#undef free_a_map
|
||||
}
|
||||
|
||||
static inline void
|
||||
del_font(Font *f) {
|
||||
Py_CLEAR(f->face);
|
||||
free(f->ffs_hb_features); f->ffs_hb_features = NULL;
|
||||
free_maps(f);
|
||||
f->bold = false; f->italic = false;
|
||||
}
|
||||
|
||||
static inline void
|
||||
del_font_group(FontGroup *fg) {
|
||||
free(fg->canvas); fg->canvas = NULL;
|
||||
fg->sprite_map = free_sprite_map(fg->sprite_map);
|
||||
for (size_t i = 0; i < fg->fonts_count; i++) del_font(fg->fonts + i);
|
||||
free(fg->fonts); fg->fonts = NULL;
|
||||
}
|
||||
|
||||
static inline void
|
||||
trim_unused_font_groups(void) {
|
||||
save_window_font_groups();
|
||||
size_t i = 0;
|
||||
while (i < num_font_groups) {
|
||||
if (font_group_is_unused(font_groups + i)) {
|
||||
del_font_group(font_groups + i);
|
||||
size_t num_to_right = (--num_font_groups) - i;
|
||||
if (!num_to_right) break;
|
||||
memmove(font_groups + i, font_groups + 1 + i, num_to_right * sizeof(FontGroup));
|
||||
@@ -282,25 +318,6 @@ sprite_tracker_current_layout(FONTS_DATA_HANDLE data, unsigned int *x, unsigned
|
||||
*x = fg->sprite_tracker.xnum; *y = fg->sprite_tracker.ynum; *z = fg->sprite_tracker.z;
|
||||
}
|
||||
|
||||
void
|
||||
free_maps(Font *font) {
|
||||
#define free_a_map(type, attr) {\
|
||||
type *s, *t; \
|
||||
for (size_t i = 0; i < sizeof(font->attr)/sizeof(font->attr[0]); i++) { \
|
||||
s = font->attr[i].next; \
|
||||
while (s) { \
|
||||
t = s; \
|
||||
s = s->next; \
|
||||
free(t); \
|
||||
} \
|
||||
}\
|
||||
memset(font->attr, 0, sizeof(font->attr)); \
|
||||
}
|
||||
free_a_map(SpritePosition, sprite_map);
|
||||
free_a_map(SpecialGlyphCache, special_glyph_cache);
|
||||
#undef free_a_map
|
||||
}
|
||||
|
||||
void
|
||||
clear_sprite_map(Font *font) {
|
||||
#define CLEAR(s) s->filled = false; s->rendered = false; s->colored = false; s->glyph = 0; zero_at_ptr(&s->extra_glyphs); s->x = 0; s->y = 0; s->z = 0; s->ligature_index = 0;
|
||||
@@ -384,22 +401,6 @@ init_font(Font *f, PyObject *face, bool bold, bool italic, bool emoji_presentati
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline void
|
||||
del_font(Font *f) {
|
||||
Py_CLEAR(f->face);
|
||||
free(f->ffs_hb_features); f->ffs_hb_features = NULL;
|
||||
free_maps(f);
|
||||
f->bold = false; f->italic = false;
|
||||
}
|
||||
|
||||
static inline void
|
||||
del_font_group(FontGroup *fg) {
|
||||
free(fg->canvas); fg->canvas = NULL;
|
||||
fg->sprite_map = free_sprite_map(fg->sprite_map);
|
||||
for (size_t i = 0; i < fg->fonts_count; i++) del_font(fg->fonts + i);
|
||||
free(fg->fonts); fg->fonts = NULL;
|
||||
}
|
||||
|
||||
static inline void
|
||||
free_font_groups(void) {
|
||||
if (font_groups) {
|
||||
@@ -600,6 +601,8 @@ START_ALLOW_CASE_RANGE
|
||||
case 0xe0ba: //
|
||||
case 0xe0bc: //
|
||||
case 0xe0be: //
|
||||
case 0x1fb00 ... 0x1fb8b: // symbols for legacy computing
|
||||
case 0x1fba0 ... 0x1fbae:
|
||||
return BOX_FONT;
|
||||
default:
|
||||
ans = in_symbol_maps(fg, cpu_cell->ch);
|
||||
@@ -637,8 +640,12 @@ START_ALLOW_CASE_RANGE
|
||||
return ch - 0x2500; // IDs from 0x00 to 0x9f
|
||||
case 0xe0b0 ... 0xe0d4:
|
||||
return 0xa0 + ch - 0xe0b0; // IDs from 0xa0 to 0xc4
|
||||
case 0x1fb00 ... 0x1fb8b:
|
||||
return 0xc5 + ch - 0x1fb00; // IDs from 0xc5 to 0x150
|
||||
case 0x1fba0 ... 0x1fbae:
|
||||
return 0x151 + ch - 0x1fba0;
|
||||
default:
|
||||
return 0xff;
|
||||
return 0xffff;
|
||||
}
|
||||
END_ALLOW_CASE_RANGE
|
||||
}
|
||||
|
||||
@@ -184,11 +184,12 @@ def supersampled(supersample_factor: int = 4) -> Callable:
|
||||
return create_wrapper
|
||||
|
||||
|
||||
def fill_region(buf: BufType, width: int, height: int, xlimits: Iterable[Iterable[float]]) -> None:
|
||||
def fill_region(buf: BufType, width: int, height: int, xlimits: Iterable[Iterable[float]], inverted: bool = False) -> None:
|
||||
full, empty = (0, 255) if inverted else (255, 0)
|
||||
for y in range(height):
|
||||
offset = y * width
|
||||
for x, (upper, lower) in enumerate(xlimits):
|
||||
buf[x + offset] = 255 if upper <= y <= lower else 0
|
||||
buf[x + offset] = full if upper <= y <= lower else empty
|
||||
|
||||
|
||||
def line_equation(x1: int, y1: int, x2: int, y2: int) -> Callable[[int], float]:
|
||||
@@ -231,6 +232,32 @@ def corner_triangle(buf: BufType, width: int, height: int, corner: str) -> None:
|
||||
fill_region(buf, width, height, xlimits)
|
||||
|
||||
|
||||
@supersampled()
|
||||
def half_triangle(buf: BufType, width: int, height: int, which: str = 'left', inverted: bool = False) -> None:
|
||||
mid_x, mid_y = width // 2, height // 2
|
||||
if which == 'left':
|
||||
upper_y = line_equation(0, 0, mid_x, mid_y)
|
||||
lower_y = line_equation(0, height - 1, mid_x, mid_y)
|
||||
limits = tuple((upper_y(x), lower_y(x)) for x in range(width))
|
||||
elif which == 'top':
|
||||
first_y = line_equation(0, 0, mid_x, mid_y)
|
||||
first = tuple((0, first_y(x)) for x in range(mid_x))
|
||||
second_y = line_equation(mid_x, mid_y, width - 1, 0)
|
||||
second = tuple((0, second_y(x)) for x in range(mid_x, width))
|
||||
limits = first + second
|
||||
elif which == 'right':
|
||||
upper_y = line_equation(mid_x, mid_y, width - 1, 0)
|
||||
lower_y = line_equation(mid_x, mid_y, width - 1, height - 1)
|
||||
limits = tuple((upper_y(x), lower_y(x)) for x in range(width))
|
||||
elif which == 'bottom':
|
||||
first_y = line_equation(0, height - 1, mid_x, mid_y)
|
||||
first_ = tuple((first_y(x), height - 1) for x in range(mid_x))
|
||||
second_y = line_equation(mid_x, mid_y, width - 1, height - 1)
|
||||
second_ = tuple((second_y(x), height - 1) for x in range(mid_x, width))
|
||||
limits = first_ + second_
|
||||
fill_region(buf, width, height, limits, inverted)
|
||||
|
||||
|
||||
def thick_line(buf: BufType, width: int, height: int, thickness_in_pixels: int, p1: Tuple[int, int], p2: Tuple[int, int]) -> None:
|
||||
if p1[0] > p2[0]:
|
||||
p1, p2 = p2, p1
|
||||
@@ -276,6 +303,26 @@ def half_cross_line(buf: BufType, width: int, height: int, which: str = 'tl', le
|
||||
thick_line(buf, width, height, thickness_in_pixels, p1, p2)
|
||||
|
||||
|
||||
@supersampled()
|
||||
def mid_lines(buf: BufType, width: int, height: int, level: int = 1, pts: Iterable[str] = ('lt',)) -> None:
|
||||
mid_x, mid_y = width // 2, height // 2
|
||||
supersample_factor = getattr(buf, 'supersample_factor')
|
||||
|
||||
def pt_to_coords(p: str) -> Tuple[int, int]:
|
||||
if p == 'l':
|
||||
return 0, mid_y
|
||||
if p == 't':
|
||||
return mid_x, 0
|
||||
if p == 'r':
|
||||
return width - 1, mid_y
|
||||
if p == 'b':
|
||||
return mid_x, height - 1
|
||||
|
||||
for x in pts:
|
||||
p1, p2 = map(pt_to_coords, x)
|
||||
thick_line(buf, width, height, supersample_factor * thickness(level), p1, p2)
|
||||
|
||||
|
||||
BezierFunc = Callable[[float], float]
|
||||
|
||||
|
||||
@@ -364,20 +411,19 @@ def D(buf: BufType, width: int, height: int, left: bool = True) -> None:
|
||||
buf[offset + dest_x] = mbuf[offset + src_x]
|
||||
|
||||
|
||||
def draw_parametrized_curve(buf: BufType, width: int, height: int, thickness_in_pixels: int, xfunc: BezierFunc, yfunc: BezierFunc) -> None:
|
||||
def draw_parametrized_curve(buf: BufType, width: int, height: int, delta: int, extra: int, xfunc: BezierFunc, yfunc: BezierFunc) -> None:
|
||||
num_samples = height*4
|
||||
seen = set()
|
||||
delta, extra = divmod(thickness_in_pixels, 2)
|
||||
for i in range(num_samples + 1):
|
||||
t = (i / num_samples)
|
||||
p = x_p, y_p = int(xfunc(t)), int(yfunc(t))
|
||||
if p in seen:
|
||||
continue
|
||||
seen.add(p)
|
||||
for y in range(int(y_p) - delta, int(y_p) + delta + extra):
|
||||
for y in range(y_p - delta, y_p + delta + extra):
|
||||
if 0 <= y < height:
|
||||
offset = y * width
|
||||
for x in range(int(x_p) - delta, int(x_p) + delta + extra):
|
||||
for x in range(x_p - delta, x_p + delta + extra):
|
||||
if 0 <= x < width:
|
||||
pos = offset + x
|
||||
buf[pos] = min(255, buf[pos] + 255)
|
||||
@@ -386,29 +432,31 @@ def draw_parametrized_curve(buf: BufType, width: int, height: int, thickness_in_
|
||||
@supersampled()
|
||||
def rounded_corner(buf: BufType, width: int, height: int, level: int = 1, which: str = '╭') -> None:
|
||||
supersample_factor = getattr(buf, 'supersample_factor')
|
||||
thickness_in_pixels = thickness(level) * supersample_factor
|
||||
delta, extra = divmod(thickness(level), 2)
|
||||
hw = ((width / supersample_factor) // 2) * supersample_factor
|
||||
hh = ((height / supersample_factor) // 2) * supersample_factor
|
||||
if which == '╭':
|
||||
start = width // 2, height - 1
|
||||
end = width - 1, height // 2
|
||||
c1 = width // 2, int(0.75 * height)
|
||||
c2 = width // 2, height // 2 + 1
|
||||
start = hw, height - 1
|
||||
end = width - 1, hh
|
||||
c1 = hw, int(0.75 * height)
|
||||
c2 = hw, hh + 1
|
||||
elif which == '╮':
|
||||
start = 0, height // 2
|
||||
end = width // 2, height - 1
|
||||
c1 = width // 2, height // 2 + 1
|
||||
c2 = width // 2, int(0.75 * height)
|
||||
start = 0, hh
|
||||
end = hw, height - 1
|
||||
c1 = hw, hh + 1
|
||||
c2 = hw, int(0.75 * height)
|
||||
elif which == '╰':
|
||||
start = width // 2, 0
|
||||
end = width - 1, height // 2
|
||||
c1 = width // 2, int(0.25 * height)
|
||||
c2 = width // 2 - 1, height // 2 - 1
|
||||
end = width - 1, hh
|
||||
c1 = hw, int(0.25 * height)
|
||||
c2 = hw, hh - 1
|
||||
elif which == '╯':
|
||||
start = 0, height // 2
|
||||
end = width // 2, 0
|
||||
c1 = width // 2 - 1, height // 2 - 1
|
||||
c2 = width // 2, int(0.25 * height)
|
||||
start = 0, hh
|
||||
end = hw, 0
|
||||
c1 = hw, hh - 1
|
||||
c2 = hw, int(0.25 * height)
|
||||
xfunc, yfunc = cubic_bezier(start, end, c1, c2)
|
||||
draw_parametrized_curve(buf, width, height, thickness_in_pixels, xfunc, yfunc)
|
||||
draw_parametrized_curve(buf, width, height, delta * supersample_factor, extra * supersample_factor, xfunc, yfunc)
|
||||
|
||||
|
||||
def half_dhline(buf: BufType, width: int, height: int, level: int = 1, which: str = 'left', only: Optional[str] = None) -> Tuple[int, int]:
|
||||
@@ -512,24 +560,6 @@ def inner_corner(buf: BufType, width: int, height: int, which: str = 'tl', level
|
||||
draw_vline(buf, width, y1, y2, width // 2 + (xd * hgap), level)
|
||||
|
||||
|
||||
def vblock(buf: BufType, width: int, height: int, frac: float = 1., gravity: str = 'top') -> None:
|
||||
num_rows = min(height, round(frac * height))
|
||||
start = 0 if gravity == 'top' else height - num_rows
|
||||
for r in range(start, start + num_rows):
|
||||
off = r * width
|
||||
for c in range(off, off + width):
|
||||
buf[c] = 255
|
||||
|
||||
|
||||
def hblock(buf: BufType, width: int, height: int, frac: float = 1., gravity: str = 'left') -> None:
|
||||
num_cols = min(width, round(frac * width))
|
||||
start = 0 if gravity == 'left' else width - num_cols
|
||||
for r in range(height):
|
||||
off = r * width + start
|
||||
for c in range(off, off + num_cols):
|
||||
buf[c] = 255
|
||||
|
||||
|
||||
def shade(buf: BufType, width: int, height: int, light: bool = False, invert: bool = False) -> None:
|
||||
square_sz = max(1, width // 12)
|
||||
number_of_rows = height // square_sz
|
||||
@@ -581,6 +611,80 @@ def quad(buf: BufType, width: int, height: int, x: int = 0, y: int = 0) -> None:
|
||||
buf[off + c] = 255
|
||||
|
||||
|
||||
def sextant(buf: BufType, width: int, height: int, level: int = 1, which: int = 0) -> None:
|
||||
|
||||
def draw_sextant(row: int = 0, col: int = 0) -> None:
|
||||
if row == 0:
|
||||
y_start, y_end = 0, height // 3
|
||||
elif row == 1:
|
||||
y_start, y_end = height // 3, 2 * height // 3
|
||||
else:
|
||||
y_start, y_end = 2 * height // 3, height
|
||||
if col == 0:
|
||||
x_start, x_end = 0, width // 2
|
||||
else:
|
||||
x_start, x_end = width // 2, width
|
||||
for r in range(y_start, y_end):
|
||||
off = r * width
|
||||
for c in range(x_start, x_end):
|
||||
buf[c + off] = 255
|
||||
|
||||
def add_row(q: int, r: int) -> None:
|
||||
if q & 1:
|
||||
draw_sextant(r)
|
||||
if q & 2:
|
||||
draw_sextant(r, col=1)
|
||||
|
||||
add_row(which % 4, 0)
|
||||
add_row(which // 4, 1)
|
||||
add_row(which // 16, 2)
|
||||
|
||||
|
||||
@supersampled()
|
||||
def smooth_mosaic(
|
||||
buf: BufType, width: int, height: int, level: int = 1,
|
||||
lower: bool = True, a: Tuple[float, float] = (0, 0), b: Tuple[float, float] = (0, 0)
|
||||
) -> None:
|
||||
ax, ay = int(a[0] * (width - 1)), int(a[1] * (height - 1))
|
||||
bx, by = int(b[0] * (width - 1)), int(b[1] * (height - 1))
|
||||
line = line_equation(ax, ay, bx, by)
|
||||
|
||||
def lower_condition(x: int, y: int) -> bool:
|
||||
return y >= line(x)
|
||||
|
||||
def upper_condition(x: int, y: int) -> bool:
|
||||
return y <= line(x)
|
||||
|
||||
condition = lower_condition if lower else upper_condition
|
||||
for y in range(height):
|
||||
offset = width * y
|
||||
for x in range(width):
|
||||
if condition(x, y):
|
||||
buf[offset + x] = 255
|
||||
|
||||
|
||||
def eight_bar(buf: BufType, width: int, height: int, level: int = 1, which: int = 0, horizontal: bool = False) -> None:
|
||||
if horizontal:
|
||||
x_range = range(0, width)
|
||||
thickness = height // 8
|
||||
y_start = min(which * thickness, height - 2)
|
||||
y_range = range(y_start, height if which == 7 else min(y_start + thickness, height))
|
||||
else:
|
||||
y_range = range(0, height)
|
||||
thickness = width // 8
|
||||
x_start = min(which * thickness, width - 2)
|
||||
x_range = range(x_start, width if which == 7 else min(x_start + thickness, width))
|
||||
for y in y_range:
|
||||
offset = y * width
|
||||
for x in x_range:
|
||||
buf[offset + x] = 255
|
||||
|
||||
|
||||
def eight_block(buf: BufType, width: int, height: int, level: int = 1, which: Tuple[int, ...] = (0,), horizontal: bool = False) -> None:
|
||||
for x in which:
|
||||
eight_bar(buf, width, height, level, x, horizontal)
|
||||
|
||||
|
||||
box_chars: Dict[str, List[Callable]] = {
|
||||
'─': [hline],
|
||||
'━': [p(hline, level=3)],
|
||||
@@ -648,28 +752,28 @@ box_chars: Dict[str, List[Callable]] = {
|
||||
'╱': [p(cross_line, left=False)],
|
||||
'╲': [cross_line],
|
||||
'╳': [cross_line, p(cross_line, left=False)],
|
||||
'▀': [p(vblock, frac=1/2)],
|
||||
'▁': [p(vblock, frac=1/8, gravity='bottom')],
|
||||
'▂': [p(vblock, frac=1/4, gravity='bottom')],
|
||||
'▃': [p(vblock, frac=3/8, gravity='bottom')],
|
||||
'▄': [p(vblock, frac=1/2, gravity='bottom')],
|
||||
'▅': [p(vblock, frac=5/8, gravity='bottom')],
|
||||
'▆': [p(vblock, frac=3/4, gravity='bottom')],
|
||||
'▇': [p(vblock, frac=7/8, gravity='bottom')],
|
||||
'█': [p(vblock, frac=1, gravity='bottom')],
|
||||
'▉': [p(hblock, frac=7/8)],
|
||||
'▊': [p(hblock, frac=3/4)],
|
||||
'▋': [p(hblock, frac=5/8)],
|
||||
'▌': [p(hblock, frac=1/2)],
|
||||
'▍': [p(hblock, frac=3/8)],
|
||||
'▎': [p(hblock, frac=1/4)],
|
||||
'▏': [p(hblock, frac=1/8)],
|
||||
'▐': [p(hblock, frac=1/2, gravity='right')],
|
||||
'▀': [p(eight_block, horizontal=True, which=(0, 1, 2, 3))],
|
||||
'▁': [p(eight_bar, which=7, horizontal=True)],
|
||||
'▂': [p(eight_block, horizontal=True, which=(6, 7))],
|
||||
'▃': [p(eight_block, horizontal=True, which=(5, 6, 7))],
|
||||
'▄': [p(eight_block, horizontal=True, which=(4, 5, 6, 7))],
|
||||
'▅': [p(eight_block, horizontal=True, which=(3, 4, 5, 6, 7))],
|
||||
'▆': [p(eight_block, horizontal=True, which=(2, 3, 4, 5, 6, 7))],
|
||||
'▇': [p(eight_block, horizontal=True, which=(1, 2, 3, 4, 5, 6, 7))],
|
||||
'█': [p(eight_block, horizontal=True, which=(0, 1, 2, 3, 4, 5, 6, 7))],
|
||||
'▉': [p(eight_block, which=(0, 1, 2, 3, 4, 5, 6))],
|
||||
'▊': [p(eight_block, which=(0, 1, 2, 3, 4, 5))],
|
||||
'▋': [p(eight_block, which=(0, 1, 2, 3, 4))],
|
||||
'▌': [p(eight_block, which=(0, 1, 2, 3))],
|
||||
'▍': [p(eight_block, which=(0, 1, 2))],
|
||||
'▎': [p(eight_block, which=(0, 1))],
|
||||
'▏': [p(eight_bar)],
|
||||
'▐': [p(eight_block, which=(4, 5, 6, 7))],
|
||||
'░': [p(shade, light=True)],
|
||||
'▒': [shade],
|
||||
'▓': [p(shade, invert=True)],
|
||||
'▔': [p(vblock, frac=1/8)],
|
||||
'▕': [p(hblock, frac=1/8, gravity='right')],
|
||||
'▔': [p(eight_bar, horizontal=True)],
|
||||
'▕': [p(eight_bar, which=7)],
|
||||
'▖': [p(quad, y=1)],
|
||||
'▗': [p(quad, x=1, y=1)],
|
||||
'▘': [quad],
|
||||
@@ -680,13 +784,109 @@ box_chars: Dict[str, List[Callable]] = {
|
||||
'▝': [p(quad, x=1)],
|
||||
'▞': [p(quad, x=1), p(quad, y=1)],
|
||||
'▟': [p(quad, x=1), p(quad, y=1), p(quad, x=1, y=1)],
|
||||
|
||||
'🬼': [p(smooth_mosaic, a=(0, 0.75), b=(0.5, 1))],
|
||||
'🬽': [p(smooth_mosaic, a=(0, 0.75), b=(1, 1))],
|
||||
'🬾': [p(smooth_mosaic, a=(0, 0.5), b=(0.5, 1))],
|
||||
'🬿': [p(smooth_mosaic, a=(0, 0.5), b=(1, 1))],
|
||||
'🭀': [p(smooth_mosaic, a=(0, 0), b=(0.5, 1))],
|
||||
|
||||
'🭁': [p(smooth_mosaic, a=(0, 0.25), b=(0.5, 0))],
|
||||
'🭂': [p(smooth_mosaic, a=(0, 0.25), b=(1, 0))],
|
||||
'🭃': [p(smooth_mosaic, a=(0, 0.75), b=(0.5, 0))],
|
||||
'🭄': [p(smooth_mosaic, a=(0, 0.75), b=(1, 0))],
|
||||
'🭅': [p(smooth_mosaic, a=(0, 1), b=(0.5, 0))],
|
||||
'🭆': [p(smooth_mosaic, a=(0, 0.75), b=(1, 0.25))],
|
||||
|
||||
'🭇': [p(smooth_mosaic, a=(0.5, 1), b=(1, 0.75))],
|
||||
'🭈': [p(smooth_mosaic, a=(0, 1), b=(1, 0.75))],
|
||||
'🭉': [p(smooth_mosaic, a=(0.5, 1), b=(1, 0.25))],
|
||||
'🭊': [p(smooth_mosaic, a=(0, 1), b=(1, 0.25))],
|
||||
'🭋': [p(smooth_mosaic, a=(0.5, 1), b=(1, 0))],
|
||||
|
||||
'🭌': [p(smooth_mosaic, a=(0.5, 0), b=(1, 0.25))],
|
||||
'🭍': [p(smooth_mosaic, a=(0, 0), b=(1, 0.25))],
|
||||
'🭎': [p(smooth_mosaic, a=(0.5, 0), b=(1, 0.75))],
|
||||
'🭏': [p(smooth_mosaic, a=(0, 0), b=(1, 0.75))],
|
||||
'🭐': [p(smooth_mosaic, a=(0.5, 0), b=(1, 1))],
|
||||
'🭑': [p(smooth_mosaic, a=(0, 0.25), b=(1, 0.75))],
|
||||
|
||||
'🭒': [p(smooth_mosaic, lower=False, a=(0, 0.75), b=(0.5, 1))],
|
||||
'🭓': [p(smooth_mosaic, lower=False, a=(0, 0.75), b=(1, 1))],
|
||||
'🭔': [p(smooth_mosaic, lower=False, a=(0, 0.25), b=(0.5, 1))],
|
||||
'🭕': [p(smooth_mosaic, lower=False, a=(0, 0.25), b=(1, 1))],
|
||||
'🭖': [p(smooth_mosaic, lower=False, a=(0, 0), b=(0.5, 1))],
|
||||
|
||||
'🭗': [p(smooth_mosaic, lower=False, a=(0, 0.25), b=(0.5, 0))],
|
||||
'🭘': [p(smooth_mosaic, lower=False, a=(0, 0.25), b=(1, 0))],
|
||||
'🭙': [p(smooth_mosaic, lower=False, a=(0, 0.75), b=(0.5, 0))],
|
||||
'🭚': [p(smooth_mosaic, lower=False, a=(0, 0.75), b=(1, 0))],
|
||||
'🭛': [p(smooth_mosaic, lower=False, a=(0, 1), b=(0.5, 0))],
|
||||
|
||||
'🭜': [p(smooth_mosaic, lower=False, a=(0, 0.75), b=(1, 0.25))],
|
||||
'🭝': [p(smooth_mosaic, lower=False, a=(0.5, 1), b=(1, 0.75))],
|
||||
'🭞': [p(smooth_mosaic, lower=False, a=(0, 1), b=(1, 0.75))],
|
||||
'🭟': [p(smooth_mosaic, lower=False, a=(0.5, 1), b=(1, 0.25))],
|
||||
'🭠': [p(smooth_mosaic, lower=False, a=(0, 1), b=(1, 0.25))],
|
||||
'🭡': [p(smooth_mosaic, lower=False, a=(0.5, 1), b=(1, 0))],
|
||||
|
||||
'🭢': [p(smooth_mosaic, lower=False, a=(0.5, 0), b=(1, 0.25))],
|
||||
'🭣': [p(smooth_mosaic, lower=False, a=(0, 0), b=(1, 0.25))],
|
||||
'🭤': [p(smooth_mosaic, lower=False, a=(0.5, 0), b=(1, 0.75))],
|
||||
'🭥': [p(smooth_mosaic, lower=False, a=(0, 0), b=(1, 0.75))],
|
||||
'🭦': [p(smooth_mosaic, lower=False, a=(0.5, 0), b=(1, 1))],
|
||||
'🭧': [p(smooth_mosaic, lower=False, a=(0, 0.25), b=(1, 0.75))],
|
||||
|
||||
'🭨': [p(half_triangle, inverted=True)],
|
||||
'🭩': [p(half_triangle, which='top', inverted=True)],
|
||||
'🭪': [p(half_triangle, which='right', inverted=True)],
|
||||
'🭫': [p(half_triangle, which='bottom', inverted=True)],
|
||||
'🭬': [half_triangle],
|
||||
'🭭': [p(half_triangle, which='top')],
|
||||
'🭮': [p(half_triangle, which='right')],
|
||||
'🭯': [p(half_triangle, which='bottom')],
|
||||
|
||||
'🭼': [eight_bar, p(eight_bar, which=7, horizontal=True)],
|
||||
'🭽': [eight_bar, p(eight_bar, horizontal=True)],
|
||||
'🭾': [p(eight_bar, which=7), p(eight_bar, horizontal=True)],
|
||||
'🭿': [p(eight_bar, which=7), p(eight_bar, which=7, horizontal=True)],
|
||||
'🮀': [p(eight_bar, horizontal=True), p(eight_bar, which=7, horizontal=True)],
|
||||
'🮁': [
|
||||
p(eight_bar, horizontal=True), p(eight_bar, which=2, horizontal=True),
|
||||
p(eight_bar, which=4, horizontal=True), p(eight_bar, which=7, horizontal=True)],
|
||||
'🮂': [p(eight_block, horizontal=True, which=(0, 1))],
|
||||
'🮃': [p(eight_block, horizontal=True, which=(0, 1, 2))],
|
||||
'🮄': [p(eight_block, horizontal=True, which=(0, 1, 2, 3, 4))],
|
||||
'🮅': [p(eight_block, horizontal=True, which=(0, 1, 2, 3, 4, 5))],
|
||||
'🮆': [p(eight_block, horizontal=True, which=(0, 1, 2, 3, 4, 5, 6))],
|
||||
'🮇': [p(eight_block, which=(6, 7))],
|
||||
'🮈': [p(eight_block, which=(5, 6, 7))],
|
||||
'🮉': [p(eight_block, which=(3, 4, 5, 6, 7))],
|
||||
'🮊': [p(eight_block, which=(2, 3, 4, 5, 6, 7))],
|
||||
'🮋': [p(eight_block, which=(1, 2, 3, 4, 5, 6, 7))],
|
||||
|
||||
'🮠': [mid_lines],
|
||||
'🮡': [p(mid_lines, pts=('tr',))],
|
||||
'🮢': [p(mid_lines, pts=('lb',))],
|
||||
'🮣': [p(mid_lines, pts=('br',))],
|
||||
'🮤': [p(mid_lines, pts=('lt', 'lb'))],
|
||||
'🮥': [p(mid_lines, pts=('rt', 'rb'))],
|
||||
'🮦': [p(mid_lines, pts=('rb', 'lb'))],
|
||||
'🮧': [p(mid_lines, pts=('rt', 'lt'))],
|
||||
'🮨': [p(mid_lines, pts=('rb', 'lt'))],
|
||||
'🮩': [p(mid_lines, pts=('lb', 'rt'))],
|
||||
'🮪': [p(mid_lines, pts=('lb', 'rt', 'rb'))],
|
||||
'🮫': [p(mid_lines, pts=('lb', 'lt', 'rb'))],
|
||||
'🮬': [p(mid_lines, pts=('rt', 'lt', 'rb'))],
|
||||
'🮭': [p(mid_lines, pts=('rt', 'lt', 'lb'))],
|
||||
'🮮': [p(mid_lines, pts=('rt', 'rb', 'lt', 'lb'))],
|
||||
}
|
||||
|
||||
t, f = 1, 3
|
||||
for start in '┌┐└┘':
|
||||
for i, (hlevel, vlevel) in enumerate(((t, t), (f, t), (t, f), (f, f))):
|
||||
box_chars[chr(ord(start) + i)] = [p(corner, which=start, hlevel=hlevel, vlevel=vlevel)]
|
||||
for ch in '╭╮╯╰':
|
||||
for ch in '╭╮╰╯':
|
||||
box_chars[ch] = [p(rounded_corner, which=ch)]
|
||||
|
||||
for i, (a_, b_, c_, d_) in enumerate((
|
||||
@@ -709,6 +909,18 @@ for chars, func_ in (('╒╕╘╛', dvcorner), ('╓╖╙╜', dhcorner), ('
|
||||
box_chars[ch] = [p(cast(Callable, func_), which=ch)]
|
||||
|
||||
|
||||
c = 0x1fb00
|
||||
for i in range(1, 63):
|
||||
if i in (20, 40):
|
||||
continue
|
||||
box_chars[chr(c)] = [p(sextant, which=i)]
|
||||
c += 1
|
||||
|
||||
for i in range(1, 7):
|
||||
box_chars[chr(0x1fb6f + i)] = [p(eight_bar, which=i)]
|
||||
box_chars[chr(0x1fb75 + i)] = [p(eight_bar, which=i, horizontal=True)]
|
||||
|
||||
|
||||
def render_box_char(ch: str, buf: BufType, width: int, height: int, dpi: float = 96.0) -> BufType:
|
||||
global _dpi
|
||||
_dpi = dpi
|
||||
|
||||
@@ -84,9 +84,14 @@ def find_best_match(family: str, bold: bool = False, italic: bool = False, monos
|
||||
# First look for an exact match
|
||||
for selector in ('ps_map', 'full_map', 'family_map'):
|
||||
candidates = font_map[selector].get(q)
|
||||
if candidates:
|
||||
candidates.sort(key=score)
|
||||
return candidates[0]
|
||||
if not candidates:
|
||||
continue
|
||||
if len(candidates) == 1 and (bold or italic) and candidates[0].get('family') == candidates[0].get('full_name'):
|
||||
# IBM Plex Mono does this, where the full name of the regular font
|
||||
# face is the same as its family name
|
||||
continue
|
||||
candidates.sort(key=score)
|
||||
return candidates[0]
|
||||
|
||||
# Use fc-match to see if we can find a monospaced font that matches family
|
||||
for spacing in (FC_MONO, FC_DUAL):
|
||||
|
||||
@@ -589,10 +589,16 @@ render_glyphs_in_cells(PyObject *f, bool bold, bool italic, hb_glyph_info_t *inf
|
||||
if (!render_color_bitmap(self, info[i].codepoint, &bm, cell_width, cell_height, num_cells, baseline)) {
|
||||
if (PyErr_Occurred()) PyErr_Print();
|
||||
*was_colored = false;
|
||||
if (!render_bitmap(self, info[i].codepoint, &bm, cell_width, cell_height, num_cells, bold, italic, true, fg)) return false;
|
||||
if (!render_bitmap(self, info[i].codepoint, &bm, cell_width, cell_height, num_cells, bold, italic, true, fg)) {
|
||||
free_processed_bitmap(&bm);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!render_bitmap(self, info[i].codepoint, &bm, cell_width, cell_height, num_cells, bold, italic, true, fg)) return false;
|
||||
if (!render_bitmap(self, info[i].codepoint, &bm, cell_width, cell_height, num_cells, bold, italic, true, fg)) {
|
||||
free_processed_bitmap(&bm);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
x_offset = x + (float)positions[i].x_offset / 64.0f;
|
||||
y = (float)positions[i].y_offset / 64.0f;
|
||||
|
||||
2
kitty/glfw-wrapper.h
generated
2
kitty/glfw-wrapper.h
generated
@@ -1240,7 +1240,7 @@ typedef void (* GLFWwindowiconifyfun)(GLFWwindow*,int);
|
||||
* @endcode
|
||||
*
|
||||
* @param[in] window The window that was maximized or restored.
|
||||
* @param[in] iconified `true` if the window was maximized, or
|
||||
* @param[in] maximized `true` if the window was maximized, or
|
||||
* `false` if it was restored.
|
||||
*
|
||||
* @sa @ref window_maximize
|
||||
|
||||
16
kitty/glfw.c
16
kitty/glfw.c
@@ -15,6 +15,7 @@ extern long cocoa_window_number(void *w);
|
||||
extern void cocoa_create_global_menu(void);
|
||||
extern void cocoa_hide_window_title(void *w);
|
||||
extern void cocoa_hide_titlebar(void *w);
|
||||
extern void cocoa_system_beep(void);
|
||||
extern void cocoa_set_activation_policy(bool);
|
||||
extern void cocoa_set_titlebar_color(void *w, color_type color);
|
||||
extern bool cocoa_alt_option_key_pressed(unsigned long);
|
||||
@@ -888,16 +889,14 @@ get_clipboard_string(PYNOARG) {
|
||||
return Py_BuildValue("s", "");
|
||||
}
|
||||
|
||||
void
|
||||
ring_audio_bell(OSWindow *w UNUSED) {
|
||||
static void
|
||||
ring_audio_bell(void) {
|
||||
static monotonic_t last_bell_at = -1;
|
||||
monotonic_t now = monotonic();
|
||||
if (now - last_bell_at <= ms_to_monotonic_t(100ll)) return;
|
||||
if (last_bell_at >= 0 && now - last_bell_at <= ms_to_monotonic_t(100ll)) return;
|
||||
last_bell_at = now;
|
||||
#ifdef __APPLE__
|
||||
if (w->handle) {
|
||||
glfwWindowBell(w->handle);
|
||||
}
|
||||
cocoa_system_beep();
|
||||
#else
|
||||
play_canberra_sound("bell", "kitty bell");
|
||||
#endif
|
||||
@@ -905,8 +904,7 @@ ring_audio_bell(OSWindow *w UNUSED) {
|
||||
|
||||
static PyObject*
|
||||
ring_bell(PYNOARG) {
|
||||
OSWindow *w = current_os_window();
|
||||
ring_audio_bell(w);
|
||||
ring_audio_bell();
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
@@ -960,7 +958,7 @@ void
|
||||
request_window_attention(id_type kitty_window_id, bool audio_bell) {
|
||||
OSWindow *w = os_window_for_kitty_window(kitty_window_id);
|
||||
if (w) {
|
||||
if (audio_bell) ring_audio_bell(w);
|
||||
if (audio_bell) ring_audio_bell();
|
||||
if (OPT(window_alert_on_bell)) glfwRequestWindowAttention(w->handle);
|
||||
glfwPostEmptyEvent();
|
||||
}
|
||||
|
||||
@@ -645,20 +645,32 @@ grman_update_layers(GraphicsManager *self, unsigned int scrolled_by, float scree
|
||||
// Image lifetime/scrolling {{{
|
||||
|
||||
static inline void
|
||||
filter_refs(GraphicsManager *self, const void* data, bool free_images, bool (*filter_func)(ImageRef*, Image*, const void*, CellPixelSize), CellPixelSize cell) {
|
||||
self->layers_dirty = self->image_count > 0;
|
||||
filter_refs(GraphicsManager *self, const void* data, bool free_images, bool (*filter_func)(const ImageRef*, Image*, const void*, CellPixelSize), CellPixelSize cell) {
|
||||
for (size_t i = self->image_count; i-- > 0;) {
|
||||
Image *img = self->images + i;
|
||||
for (size_t j = img->refcnt; j-- > 0;) {
|
||||
ImageRef *ref = img->refs + j;
|
||||
if (filter_func(ref, img, data, cell)) {
|
||||
remove_i_from_array(img->refs, j, img->refcnt);
|
||||
self->layers_dirty = true;
|
||||
}
|
||||
}
|
||||
if (img->refcnt == 0 && (free_images || img->client_id == 0)) remove_image(self, i);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void
|
||||
modify_refs(GraphicsManager *self, const void* data, bool free_images, bool (*filter_func)(ImageRef*, Image*, const void*, CellPixelSize), CellPixelSize cell) {
|
||||
for (size_t i = self->image_count; i-- > 0;) {
|
||||
Image *img = self->images + i;
|
||||
for (size_t j = img->refcnt; j-- > 0;) {
|
||||
if (filter_func(img->refs + j, img, data, cell)) remove_i_from_array(img->refs, j, img->refcnt);
|
||||
}
|
||||
if (img->refcnt == 0 && (free_images || img->client_id == 0)) remove_image(self, i);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static inline bool
|
||||
scroll_filter_func(ImageRef *ref, Image UNUSED *img, const void *data, CellPixelSize cell UNUSED) {
|
||||
ScrollData *d = (ScrollData*)data;
|
||||
@@ -667,12 +679,12 @@ scroll_filter_func(ImageRef *ref, Image UNUSED *img, const void *data, CellPixel
|
||||
}
|
||||
|
||||
static inline bool
|
||||
ref_within_region(ImageRef *ref, index_type margin_top, index_type margin_bottom) {
|
||||
ref_within_region(const ImageRef *ref, index_type margin_top, index_type margin_bottom) {
|
||||
return ref->start_row >= (int32_t)margin_top && ref->start_row + ref->effective_num_rows <= margin_bottom;
|
||||
}
|
||||
|
||||
static inline bool
|
||||
ref_outside_region(ImageRef *ref, index_type margin_top, index_type margin_bottom) {
|
||||
ref_outside_region(const ImageRef *ref, index_type margin_top, index_type margin_bottom) {
|
||||
return ref->start_row + ref->effective_num_rows <= margin_top || ref->start_row > (int32_t)margin_bottom;
|
||||
}
|
||||
|
||||
@@ -709,16 +721,19 @@ scroll_filter_margins_func(ImageRef* ref, Image* img, const void* data, CellPixe
|
||||
|
||||
void
|
||||
grman_scroll_images(GraphicsManager *self, const ScrollData *data, CellPixelSize cell) {
|
||||
filter_refs(self, data, true, data->has_margins ? scroll_filter_margins_func : scroll_filter_func, cell);
|
||||
if (self->image_count) {
|
||||
self->layers_dirty = true;
|
||||
modify_refs(self, data, true, data->has_margins ? scroll_filter_margins_func : scroll_filter_func, cell);
|
||||
}
|
||||
}
|
||||
|
||||
static inline bool
|
||||
clear_filter_func(ImageRef *ref, Image UNUSED *img, const void UNUSED *data, CellPixelSize cell UNUSED) {
|
||||
clear_filter_func(const ImageRef *ref, Image UNUSED *img, const void UNUSED *data, CellPixelSize cell UNUSED) {
|
||||
return ref->start_row + (int32_t)ref->effective_num_rows > 0;
|
||||
}
|
||||
|
||||
static inline bool
|
||||
clear_all_filter_func(ImageRef *ref UNUSED, Image UNUSED *img, const void UNUSED *data, CellPixelSize cell UNUSED) {
|
||||
clear_all_filter_func(const ImageRef *ref UNUSED, Image UNUSED *img, const void UNUSED *data, CellPixelSize cell UNUSED) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -728,37 +743,37 @@ grman_clear(GraphicsManager *self, bool all, CellPixelSize cell) {
|
||||
}
|
||||
|
||||
static inline bool
|
||||
id_filter_func(ImageRef UNUSED *ref, Image *img, const void *data, CellPixelSize cell UNUSED) {
|
||||
id_filter_func(const ImageRef UNUSED *ref, Image *img, const void *data, CellPixelSize cell UNUSED) {
|
||||
uint32_t iid = *(uint32_t*)data;
|
||||
return img->client_id == iid;
|
||||
}
|
||||
|
||||
static inline bool
|
||||
x_filter_func(ImageRef *ref, Image UNUSED *img, const void *data, CellPixelSize cell UNUSED) {
|
||||
x_filter_func(const ImageRef *ref, Image UNUSED *img, const void *data, CellPixelSize cell UNUSED) {
|
||||
const GraphicsCommand *g = data;
|
||||
return ref->start_column <= (int32_t)g->x_offset - 1 && ((int32_t)g->x_offset - 1) < ((int32_t)(ref->start_column + ref->effective_num_cols));
|
||||
}
|
||||
|
||||
static inline bool
|
||||
y_filter_func(ImageRef *ref, Image UNUSED *img, const void *data, CellPixelSize cell UNUSED) {
|
||||
y_filter_func(const ImageRef *ref, Image UNUSED *img, const void *data, CellPixelSize cell UNUSED) {
|
||||
const GraphicsCommand *g = data;
|
||||
return ref->start_row <= (int32_t)g->y_offset - 1 && ((int32_t)(g->y_offset - 1 < ref->start_row + ref->effective_num_rows));
|
||||
}
|
||||
|
||||
static inline bool
|
||||
z_filter_func(ImageRef *ref, Image UNUSED *img, const void *data, CellPixelSize cell UNUSED) {
|
||||
z_filter_func(const ImageRef *ref, Image UNUSED *img, const void *data, CellPixelSize cell UNUSED) {
|
||||
const GraphicsCommand *g = data;
|
||||
return ref->z_index == g->z_index;
|
||||
}
|
||||
|
||||
|
||||
static inline bool
|
||||
point_filter_func(ImageRef *ref, Image *img, const void *data, CellPixelSize cell) {
|
||||
point_filter_func(const ImageRef *ref, Image *img, const void *data, CellPixelSize cell) {
|
||||
return x_filter_func(ref, img, data, cell) && y_filter_func(ref, img, data, cell);
|
||||
}
|
||||
|
||||
static inline bool
|
||||
point3d_filter_func(ImageRef *ref, Image *img, const void *data, CellPixelSize cell) {
|
||||
point3d_filter_func(const ImageRef *ref, Image *img, const void *data, CellPixelSize cell) {
|
||||
return z_filter_func(ref, img, data, cell) && point_filter_func(ref, img, data, cell);
|
||||
}
|
||||
|
||||
|
||||
381
kitty/history.c
381
kitty/history.c
@@ -5,8 +5,9 @@
|
||||
* Distributed under terms of the GPL3 license.
|
||||
*/
|
||||
|
||||
#include "data-types.h"
|
||||
#include "wcswidth.h"
|
||||
#include "lineops.h"
|
||||
#include "charsets.h"
|
||||
#include <structmember.h>
|
||||
|
||||
extern PyTypeObject Line_Type;
|
||||
@@ -55,14 +56,14 @@ attrptr(HistoryBuf *self, index_type y) {
|
||||
}
|
||||
|
||||
static inline PagerHistoryBuf*
|
||||
alloc_pagerhist(unsigned int pagerhist_sz) {
|
||||
alloc_pagerhist(size_t pagerhist_sz) {
|
||||
PagerHistoryBuf *ph;
|
||||
if (!pagerhist_sz) return NULL;
|
||||
ph = PyMem_Calloc(1, sizeof(PagerHistoryBuf));
|
||||
if (!ph) return NULL;
|
||||
ph->maxsz = pagerhist_sz / sizeof(Py_UCS4);
|
||||
ph->bufsize = 1024*1024 / sizeof(Py_UCS4);
|
||||
ph->buffer = PyMem_RawMalloc(1024*1024);
|
||||
ph->max_sz = pagerhist_sz;
|
||||
ph->buffer_size = MIN(1024u*1024u, ph->max_sz);
|
||||
ph->buffer = PyMem_RawMalloc(ph->buffer_size);
|
||||
if (!ph->buffer) { PyMem_Free(ph); return NULL; }
|
||||
return ph;
|
||||
}
|
||||
@@ -76,36 +77,35 @@ free_pagerhist(HistoryBuf *self) {
|
||||
|
||||
static inline bool
|
||||
pagerhist_extend(PagerHistoryBuf *ph, size_t minsz) {
|
||||
if (ph->bufsize >= ph->maxsz) return false;
|
||||
size_t newsz = ph->bufsize + MAX(1024u * 1024u, minsz);
|
||||
void *newbuf = PyMem_Realloc(ph->buffer, newsz * sizeof(Py_UCS4));
|
||||
if (ph->buffer_size >= ph->max_sz) return false;
|
||||
size_t newsz = MIN(ph->max_sz, ph->buffer_size + MAX(1024u * 1024u, minsz));
|
||||
uint8_t *newbuf = PyMem_Malloc(newsz);
|
||||
if (!newbuf) return false;
|
||||
size_t copied = MIN(ph->length, ph->buffer_size - ph->start);
|
||||
if (copied) memcpy(newbuf, ph->buffer + ph->start, copied);
|
||||
if (copied < ph->length) memcpy(newbuf + copied, ph->buffer, (ph->length - copied));
|
||||
PyMem_Free(ph->buffer);
|
||||
ph->start = 0;
|
||||
ph->buffer = newbuf;
|
||||
ph->bufsize = newsz;
|
||||
ph->buffer_size = newsz;
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline void
|
||||
pagerhist_clear(HistoryBuf *self) {
|
||||
if (!self->pagerhist || !self->pagerhist->maxsz) return;
|
||||
index_type pagerhist_sz = self->pagerhist->maxsz * sizeof(Py_UCS4);
|
||||
if (!self->pagerhist || !self->pagerhist->max_sz) return;
|
||||
index_type pagerhist_sz = self->pagerhist->max_sz;
|
||||
free_pagerhist(self);
|
||||
self->pagerhist = alloc_pagerhist(pagerhist_sz);
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
|
||||
HistoryBuf *self;
|
||||
unsigned int xnum = 1, ynum = 1, pagerhist_sz = 0;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "II|I", &ynum, &xnum, &pagerhist_sz)) return NULL;
|
||||
|
||||
static HistoryBuf*
|
||||
create_historybuf(PyTypeObject *type, unsigned int xnum, unsigned int ynum, unsigned int pagerhist_sz) {
|
||||
if (xnum == 0 || ynum == 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "Cannot create an empty history buffer");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
self = (HistoryBuf *)type->tp_alloc(type, 0);
|
||||
HistoryBuf *self = (HistoryBuf *)type->tp_alloc(type, 0);
|
||||
if (self != NULL) {
|
||||
self->xnum = xnum;
|
||||
self->ynum = ynum;
|
||||
@@ -115,8 +115,15 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
|
||||
self->line->xnum = xnum;
|
||||
self->pagerhist = alloc_pagerhist(pagerhist_sz);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
return (PyObject*)self;
|
||||
static PyObject *
|
||||
new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
|
||||
unsigned int xnum = 1, ynum = 1, pagerhist_sz = 0;
|
||||
if (!PyArg_ParseTuple(args, "II|I", &ynum, &xnum, &pagerhist_sz)) return NULL;
|
||||
HistoryBuf *ans = create_historybuf(type, xnum, ynum, pagerhist_sz);
|
||||
return (PyObject*)ans;
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -155,6 +162,11 @@ historybuf_init_line(HistoryBuf *self, index_type lnum, Line *l) {
|
||||
init_line(self, index_of(self, lnum), l);
|
||||
}
|
||||
|
||||
CPUCell*
|
||||
historybuf_cpu_cells(HistoryBuf *self, index_type lnum) {
|
||||
return cpu_lineptr(self, index_of(self, lnum));
|
||||
}
|
||||
|
||||
void
|
||||
historybuf_mark_line_clean(HistoryBuf *self, index_type y) {
|
||||
line_attrs_type *p = attrptr(self, index_of(self, y));
|
||||
@@ -174,57 +186,80 @@ historybuf_clear(HistoryBuf *self) {
|
||||
self->start_of_data = 0;
|
||||
}
|
||||
|
||||
static inline bool
|
||||
pagerhist_write_bytes(PagerHistoryBuf *ph, const uint8_t *buf, size_t sz) {
|
||||
if (sz > ph->max_sz) return false;
|
||||
if (!sz) return true;
|
||||
if (sz > ph->buffer_size - ph->length) pagerhist_extend(ph, sz);
|
||||
if (sz > ph->buffer_size) return false;
|
||||
size_t start_writing_at = (ph->start + ph->length) % ph->buffer_size;
|
||||
size_t available_space = ph->buffer_size - ph->length;
|
||||
size_t overlap = available_space < sz ? sz - available_space : 0;
|
||||
size_t copied = MIN(sz, ph->buffer_size - start_writing_at);
|
||||
ph->length += sz - overlap;
|
||||
ph->start = (ph->start + overlap) % ph->buffer_size;
|
||||
if (copied) memcpy(ph->buffer + start_writing_at, buf, copied);
|
||||
if (copied < sz) memcpy(ph->buffer, buf + copied, (sz - copied));
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline bool
|
||||
pagerhist_ensure_start_is_valid_utf8(PagerHistoryBuf *ph) {
|
||||
uint32_t state = UTF8_ACCEPT, codep;
|
||||
size_t pos = ph->start, count = 0;
|
||||
size_t last_reject_at = 0;
|
||||
while (count < ph->length) {
|
||||
decode_utf8(&state, &codep, ph->buffer[pos]);
|
||||
count++;
|
||||
if (state == UTF8_ACCEPT) break;
|
||||
if (state == UTF8_REJECT) { state = UTF8_ACCEPT; last_reject_at = count; }
|
||||
pos = pos == ph->buffer_size - 1 ? 0: pos + 1;
|
||||
}
|
||||
if (last_reject_at) {
|
||||
ph->start = (ph->start + last_reject_at) % ph->buffer_size;
|
||||
ph->length -= last_reject_at;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline bool
|
||||
pagerhist_write_ucs4(PagerHistoryBuf *ph, const Py_UCS4 *buf, size_t sz) {
|
||||
uint8_t scratch[4];
|
||||
for (size_t i = 0; i < sz; i++) {
|
||||
unsigned int num = encode_utf8(buf[i], (char*)scratch);
|
||||
if (!pagerhist_write_bytes(ph, scratch, num)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline void
|
||||
pagerhist_push(HistoryBuf *self) {
|
||||
pagerhist_push(HistoryBuf *self, ANSIBuf *as_ansi_buf) {
|
||||
PagerHistoryBuf *ph = self->pagerhist;
|
||||
if (!ph) return;
|
||||
bool truncated;
|
||||
const GPUCell *prev_cell = NULL;
|
||||
Line l = {.xnum=self->xnum};
|
||||
init_line(self, self->start_of_data, &l);
|
||||
#define EXPAND_IF_FULL(sz) { \
|
||||
if (ph->bufsize - ph->end < sz && !pagerhist_extend(ph, sz)) { \
|
||||
ph->bufend = ph->end; ph->end = 0; \
|
||||
} \
|
||||
}
|
||||
size_t sz = MAX(1024u, ph->bufsize - ph->end);
|
||||
sz = MAX(sz, self->xnum + self->xnum);
|
||||
EXPAND_IF_FULL(sz);
|
||||
if (ph->start != ph->end && !l.continued) {
|
||||
ph->buffer[ph->end++] = '\n';
|
||||
}
|
||||
while(sz < ph->maxsz - 2) {
|
||||
size_t num = line_as_ansi(&l, ph->buffer + ph->end, ph->bufsize - ph->end - 2, &truncated, &prev_cell);
|
||||
if (!truncated) {
|
||||
ph->end += num;
|
||||
ph->buffer[ph->end++] = '\r';
|
||||
if (ph->bufend) {
|
||||
ph->start = ph->end + 1 < ph->bufend ? ph->end + 1 : 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// check if sz is too large too fit in buffer
|
||||
if (ph->bufsize > ph->maxsz && !ph->end) break;
|
||||
sz *= 2;
|
||||
EXPAND_IF_FULL(sz);
|
||||
}
|
||||
#undef EXPAND_IF_FULL
|
||||
line_as_ansi(&l, as_ansi_buf, &prev_cell);
|
||||
if (ph->length != 0 && !l.continued) pagerhist_write_bytes(ph, (const uint8_t*)"\n", 1);
|
||||
pagerhist_write_bytes(ph, (const uint8_t*)"\x1b[m", 3);
|
||||
if (pagerhist_write_ucs4(ph, as_ansi_buf->buf, as_ansi_buf->len)) pagerhist_write_bytes(ph, (const uint8_t*)"\r", 1);
|
||||
}
|
||||
|
||||
static inline index_type
|
||||
historybuf_push(HistoryBuf *self) {
|
||||
historybuf_push(HistoryBuf *self, ANSIBuf *as_ansi_buf) {
|
||||
index_type idx = (self->start_of_data + self->count) % self->ynum;
|
||||
init_line(self, idx, self->line);
|
||||
if (self->count == self->ynum) {
|
||||
pagerhist_push(self);
|
||||
pagerhist_push(self, as_ansi_buf);
|
||||
self->start_of_data = (self->start_of_data + 1) % self->ynum;
|
||||
} else self->count++;
|
||||
return idx;
|
||||
}
|
||||
|
||||
void
|
||||
historybuf_add_line(HistoryBuf *self, const Line *line) {
|
||||
index_type idx = historybuf_push(self);
|
||||
historybuf_add_line(HistoryBuf *self, const Line *line, ANSIBuf *as_ansi_buf) {
|
||||
index_type idx = historybuf_push(self, as_ansi_buf);
|
||||
copy_line(line, self->line);
|
||||
*attrptr(self, idx) = (line->continued & CONTINUED_MASK) | (line->has_dirty_text ? TEXT_DIRTY_MASK : 0);
|
||||
}
|
||||
@@ -261,122 +296,154 @@ push(HistoryBuf *self, PyObject *args) {
|
||||
#define push_doc "Push a line into this buffer, removing the oldest line, if necessary"
|
||||
Line *line;
|
||||
if (!PyArg_ParseTuple(args, "O!", &Line_Type, &line)) return NULL;
|
||||
historybuf_add_line(self, line);
|
||||
ANSIBuf as_ansi_buf = {0};
|
||||
historybuf_add_line(self, line, &as_ansi_buf);
|
||||
free(as_ansi_buf.buf);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
as_ansi(HistoryBuf *self, PyObject *callback) {
|
||||
#define as_ansi_doc "as_ansi(callback) -> The contents of this buffer as ANSI escaped text. callback is called with each successive line."
|
||||
static Py_UCS4 t[5120];
|
||||
Line l = {.xnum=self->xnum};
|
||||
bool truncated;
|
||||
const GPUCell *prev_cell = NULL;
|
||||
ANSIBuf output = {0};
|
||||
for(unsigned int i = 0; i < self->count; i++) {
|
||||
init_line(self, i, &l);
|
||||
if (i < self->count - 1) {
|
||||
l.continued = *attrptr(self, index_of(self, i + 1)) & CONTINUED_MASK;
|
||||
} else l.continued = false;
|
||||
index_type num = line_as_ansi(&l, t, 5120, &truncated, &prev_cell);
|
||||
if (!(l.continued) && num < 5119) t[num++] = 10; // 10 = \n
|
||||
PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, t, num);
|
||||
if (ans == NULL) return PyErr_NoMemory();
|
||||
line_as_ansi(&l, &output, &prev_cell);
|
||||
if (!l.continued) {
|
||||
ensure_space_for(&output, buf, Py_UCS4, output.len + 1, capacity, 2048, false);
|
||||
output.buf[output.len++] = 10; // 10 = \n
|
||||
}
|
||||
PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, output.buf, output.len);
|
||||
if (ans == NULL) { PyErr_NoMemory(); goto end; }
|
||||
PyObject *ret = PyObject_CallFunctionObjArgs(callback, ans, NULL);
|
||||
Py_CLEAR(ans);
|
||||
if (ret == NULL) return NULL;
|
||||
if (ret == NULL) goto end;
|
||||
Py_CLEAR(ret);
|
||||
}
|
||||
end:
|
||||
free(output.buf);
|
||||
if (PyErr_Occurred()) return NULL;
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static inline Line*
|
||||
get_line(HistoryBuf *self, index_type y, Line *l) { init_line(self, index_of(self, self->count - y - 1), l); return l; }
|
||||
|
||||
static inline char_type
|
||||
pagerhist_read_char(PagerHistoryBuf *ph, size_t pos, unsigned *count, uint8_t record[8]) {
|
||||
uint32_t codep, state = UTF8_ACCEPT;
|
||||
*count = 0;
|
||||
while (true) {
|
||||
decode_utf8(&state, &codep, ph->buffer[pos]);
|
||||
record[(*count)++] = ph->buffer[pos];
|
||||
if (state == UTF8_REJECT) { codep = 0; break; }
|
||||
if (state == UTF8_ACCEPT) break;
|
||||
pos = pos == ph->buffer_size - 1 ? 0 : (pos + 1);
|
||||
}
|
||||
return codep;
|
||||
}
|
||||
|
||||
static void
|
||||
pagerhist_rewrap(PagerHistoryBuf *ph, index_type xnum) {
|
||||
Py_UCS4 *buf = PyMem_RawMalloc(ph->bufsize * sizeof(Py_UCS4));
|
||||
if (!buf) return;
|
||||
index_type s = ph->start, i = s, dest = 0, dest_bufend = 0, x = 0;
|
||||
index_type end = ph->bufend ? ph->bufend : ph->end;
|
||||
index_type lastmod_s = 0, lastmod_len = 0;
|
||||
#define CPY(_s, _l) { if (dest + (_l) >= ph->bufsize - 1) { dest_bufend = dest; dest = 0; } \
|
||||
memcpy(buf + dest, ph->buffer + (_s), (_l) * sizeof(Py_UCS4)); dest += (_l); }
|
||||
while (i < end) {
|
||||
switch (ph->buffer[i]) {
|
||||
case '\n':
|
||||
CPY(s, i - s + 1);
|
||||
x = 0; s = i + 1; lastmod_len = 0;
|
||||
break;
|
||||
case '\r':
|
||||
CPY(s, i - s);
|
||||
if (!memcmp(ph->buffer + lastmod_s, ph->buffer + i + 1, lastmod_len * sizeof(Py_UCS4)))
|
||||
i += lastmod_len;
|
||||
s = i + 1;
|
||||
break;
|
||||
case '\x1b':
|
||||
if (ph->buffer[i+1] != '[') break;
|
||||
lastmod_s = i;
|
||||
while (ph->buffer[++i] != 'm');
|
||||
lastmod_len = i - lastmod_s + 1;
|
||||
break;
|
||||
default:
|
||||
x++; break;
|
||||
}
|
||||
i++;
|
||||
if (ph->bufend && i == ph->bufend) {
|
||||
if (s != i) CPY(s, i - s);
|
||||
end = ph->end; i = s = 0;
|
||||
}
|
||||
if (x == xnum) {
|
||||
CPY(s, i - s); buf[dest++] = '\r'; s = i; x = 0;
|
||||
if (!(ph->buffer[i] == '\x1b' && ph->buffer[i+1] == '[') && lastmod_len)
|
||||
CPY(lastmod_s, lastmod_len);
|
||||
pagerhist_rewrap_to(HistoryBuf *self, index_type cells_in_line) {
|
||||
PagerHistoryBuf *ph = self->pagerhist;
|
||||
if (!ph->length) return;
|
||||
PagerHistoryBuf *nph = PyMem_Calloc(sizeof(PagerHistoryBuf), 1);
|
||||
if (!nph) return;
|
||||
nph->buffer_size = ph->buffer_size;
|
||||
nph->max_sz = ph->max_sz;
|
||||
nph->buffer = PyMem_Malloc(nph->buffer_size);
|
||||
if (!nph->buffer) { PyMem_Free(nph); return ; }
|
||||
size_t i = 0, pos;
|
||||
ssize_t ch_width = 0;
|
||||
unsigned count;
|
||||
uint8_t record[8];
|
||||
index_type num_in_current_line = 0;
|
||||
char_type ch;
|
||||
WCSState wcs_state;
|
||||
initialize_wcs_state(&wcs_state);
|
||||
|
||||
#define READ_CHAR(ch) { \
|
||||
ch = pagerhist_read_char(ph, pos, &count, record); \
|
||||
i += count; pos += count; \
|
||||
if (pos >= ph->buffer_size) pos = pos - ph->buffer_size; \
|
||||
}
|
||||
#define WRITE_CHAR() { \
|
||||
if (num_in_current_line + ch_width > cells_in_line) { \
|
||||
pagerhist_write_bytes(nph, (const uint8_t*)"\r", 1); \
|
||||
num_in_current_line = 0; \
|
||||
}\
|
||||
if (ch_width >= 0 || (int)num_in_current_line >= -ch_width) num_in_current_line += ch_width; \
|
||||
pagerhist_write_bytes(nph, record, count); \
|
||||
}
|
||||
|
||||
for (i = 0; i < ph->length;) {
|
||||
pos = ph->start + i;
|
||||
if (pos >= ph->buffer_size) pos = pos - ph->buffer_size;
|
||||
READ_CHAR(ch);
|
||||
if (ch == '\n') {
|
||||
initialize_wcs_state(&wcs_state);
|
||||
ch_width = 1;
|
||||
WRITE_CHAR();
|
||||
num_in_current_line = 0;
|
||||
} else if (ch != '\r') {
|
||||
ch_width = wcswidth_step(&wcs_state, ch);
|
||||
WRITE_CHAR();
|
||||
}
|
||||
}
|
||||
#undef CPY
|
||||
PyMem_Free(ph->buffer);
|
||||
ph->buffer = buf;
|
||||
ph->end = dest; ph->bufend = dest_bufend;
|
||||
ph->start = dest_bufend ? dest + 1 : 0;
|
||||
ph->rewrap_needed = false;
|
||||
free_pagerhist(self);
|
||||
self->pagerhist = nph;
|
||||
#undef READ_CHAR
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
pagerhist_write(HistoryBuf *self, PyObject *what) {
|
||||
if (self->pagerhist && self->pagerhist->max_sz) {
|
||||
if (PyBytes_Check(what)) pagerhist_write_bytes(self->pagerhist, (const uint8_t*)PyBytes_AS_STRING(what), PyBytes_GET_SIZE(what));
|
||||
else if (PyUnicode_Check(what) && PyUnicode_READY(what) == 0) {
|
||||
Py_UCS4 *buf = PyUnicode_AsUCS4Copy(what);
|
||||
if (buf) {
|
||||
pagerhist_write_ucs4(self->pagerhist, buf, PyUnicode_GET_LENGTH(what));
|
||||
PyMem_Free(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
pagerhist_as_bytes(HistoryBuf *self, PyObject *args UNUSED) {
|
||||
PagerHistoryBuf *ph = self->pagerhist;
|
||||
if (!ph || !ph->length) return PyBytes_FromStringAndSize("", 0);
|
||||
pagerhist_ensure_start_is_valid_utf8(ph);
|
||||
if (ph->rewrap_needed) pagerhist_rewrap_to(self, self->xnum);
|
||||
|
||||
Line l = {.xnum=self->xnum}; get_line(self, 0, &l);
|
||||
size_t sz = ph->length;
|
||||
if (!l.continued) sz += 1;
|
||||
PyObject *ans = PyBytes_FromStringAndSize(NULL, sz);
|
||||
if (!ans) return NULL;
|
||||
uint8_t *buf = (uint8_t*)PyBytes_AS_STRING(ans);
|
||||
size_t copied = MIN(ph->length, ph->buffer_size - ph->start);
|
||||
if (copied) memcpy(buf, ph->buffer + ph->start, copied);
|
||||
if (copied < ph->length) memcpy(buf + copied, ph->buffer, (ph->length - copied));
|
||||
if (!l.continued) buf[sz-1] = '\n';
|
||||
return ans;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
pagerhist_as_text(HistoryBuf *self, PyObject *callback) {
|
||||
PagerHistoryBuf *ph = self->pagerhist;
|
||||
PyObject *ret = NULL, *t = NULL;
|
||||
Py_UCS4 *buf = NULL;
|
||||
index_type num;
|
||||
if (!ph) Py_RETURN_NONE;
|
||||
|
||||
if (ph->rewrap_needed) pagerhist_rewrap(ph, self->xnum);
|
||||
|
||||
#define CALLBACK { \
|
||||
if (t == NULL) goto end; \
|
||||
ret = PyObject_CallFunctionObjArgs(callback, t, NULL); \
|
||||
Py_DECREF(t); \
|
||||
if (ret == NULL) goto end; \
|
||||
Py_DECREF(ret); \
|
||||
}
|
||||
|
||||
num = (ph->bufend ? ph->bufend : ph->end) - ph->start;
|
||||
buf = ph->buffer + ph->start;
|
||||
t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, buf, num);
|
||||
CALLBACK;
|
||||
if (ph->bufend) {
|
||||
num = ph->end; buf = ph->buffer;
|
||||
t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, buf, num);
|
||||
CALLBACK;
|
||||
}
|
||||
Line l = {.xnum=self->xnum}; get_line(self, 0, &l);
|
||||
if (!l.continued) {
|
||||
t = PyUnicode_FromString("\n");
|
||||
CALLBACK;
|
||||
}
|
||||
#undef CALLBACK
|
||||
end:
|
||||
if (PyErr_Occurred()) return NULL;
|
||||
Py_RETURN_NONE;
|
||||
pagerhist_as_text(HistoryBuf *self, PyObject *args) {
|
||||
PyObject *ans = NULL;
|
||||
PyObject *bytes = pagerhist_as_bytes(self, args);
|
||||
if (bytes) {
|
||||
ans = PyUnicode_DecodeUTF8(PyBytes_AS_STRING(bytes), PyBytes_GET_SIZE(bytes), "ignore");
|
||||
Py_DECREF(bytes);
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
@@ -395,7 +462,10 @@ static PyObject*
|
||||
as_text(HistoryBuf *self, PyObject *args) {
|
||||
GetLineWrapper glw = {.self=self};
|
||||
glw.line.xnum = self->xnum;
|
||||
return as_text_generic(args, &glw, get_line_wrapper, self->count, self->xnum);
|
||||
ANSIBuf output = {0};
|
||||
PyObject *ans = as_text_generic(args, &glw, get_line_wrapper, self->count, &output);
|
||||
free(output.buf);
|
||||
return ans;
|
||||
}
|
||||
|
||||
|
||||
@@ -411,6 +481,14 @@ dirty_lines(HistoryBuf *self, PyObject *a UNUSED) {
|
||||
return ans;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
pagerhist_rewrap(HistoryBuf *self, PyObject *xnum) {
|
||||
if (self->pagerhist) {
|
||||
pagerhist_rewrap_to(self, PyLong_AsUnsignedLong(xnum));
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
|
||||
// Boilerplate {{{
|
||||
static PyObject* rewrap(HistoryBuf *self, PyObject *args);
|
||||
@@ -419,7 +497,10 @@ static PyObject* rewrap(HistoryBuf *self, PyObject *args);
|
||||
static PyMethodDef methods[] = {
|
||||
METHOD(line, METH_O)
|
||||
METHOD(as_ansi, METH_O)
|
||||
METHODB(pagerhist_as_text, METH_O),
|
||||
METHODB(pagerhist_write, METH_O),
|
||||
METHODB(pagerhist_rewrap, METH_O),
|
||||
METHODB(pagerhist_as_text, METH_NOARGS),
|
||||
METHODB(pagerhist_as_bytes, METH_NOARGS),
|
||||
METHODB(as_text, METH_VARARGS),
|
||||
METHOD(dirty_lines, METH_NOARGS)
|
||||
METHOD(push, METH_VARARGS)
|
||||
@@ -450,7 +531,7 @@ PyTypeObject HistoryBuf_Type = {
|
||||
INIT_TYPE(HistoryBuf)
|
||||
|
||||
HistoryBuf *alloc_historybuf(unsigned int lines, unsigned int columns, unsigned int pagerhist_sz) {
|
||||
return (HistoryBuf*)new(&HistoryBuf_Type, Py_BuildValue("III", lines, columns, pagerhist_sz), NULL);
|
||||
return create_historybuf(&HistoryBuf_Type, columns, lines, pagerhist_sz);
|
||||
}
|
||||
// }}}
|
||||
|
||||
@@ -462,13 +543,13 @@ HistoryBuf *alloc_historybuf(unsigned int lines, unsigned int columns, unsigned
|
||||
|
||||
#define is_src_line_continued(src_y) (map_src_index(src_y) < src->ynum - 1 ? (*attrptr(src, map_src_index(src_y + 1)) & CONTINUED_MASK) : false)
|
||||
|
||||
#define next_dest_line(cont) *attrptr(dest, historybuf_push(dest)) = cont & CONTINUED_MASK; dest->line->continued = cont;
|
||||
#define next_dest_line(cont) *attrptr(dest, historybuf_push(dest, as_ansi_buf)) = cont & CONTINUED_MASK; dest->line->continued = cont;
|
||||
|
||||
#define first_dest_line next_dest_line(false);
|
||||
|
||||
#include "rewrap.h"
|
||||
|
||||
void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other) {
|
||||
void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other, ANSIBuf *as_ansi_buf) {
|
||||
while(other->num_segments < self->num_segments) add_segment(other);
|
||||
if (other->xnum == self->xnum && other->ynum == self->ynum) {
|
||||
// Fast path
|
||||
@@ -480,12 +561,12 @@ void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other) {
|
||||
other->count = self->count; other->start_of_data = self->start_of_data;
|
||||
return;
|
||||
}
|
||||
if (other->pagerhist && other->xnum != self->xnum && other->pagerhist->end != other->pagerhist->start)
|
||||
if (other->pagerhist && other->xnum != self->xnum && other->pagerhist->length)
|
||||
other->pagerhist->rewrap_needed = true;
|
||||
other->count = 0; other->start_of_data = 0;
|
||||
index_type x = 0, y = 0;
|
||||
if (self->count > 0) {
|
||||
rewrap_inner(self, other, self->count, NULL, &x, &y);
|
||||
rewrap_inner(self, other, self->count, NULL, &x, &y, as_ansi_buf);
|
||||
for (index_type i = 0; i < other->count; i++) *attrptr(other, (other->start_of_data + i) % other->ynum) |= TEXT_DIRTY_MASK;
|
||||
}
|
||||
}
|
||||
@@ -494,6 +575,8 @@ static PyObject*
|
||||
rewrap(HistoryBuf *self, PyObject *args) {
|
||||
HistoryBuf *other;
|
||||
if (!PyArg_ParseTuple(args, "O!", &HistoryBuf_Type, &other)) return NULL;
|
||||
historybuf_rewrap(self, other);
|
||||
ANSIBuf as_ansi_buf = {0};
|
||||
historybuf_rewrap(self, other, &as_ansi_buf);
|
||||
free(as_ansi_buf.buf);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
161
kitty/hyperlink.c
Normal file
161
kitty/hyperlink.c
Normal file
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* hyperlink.c
|
||||
* Copyright (C) 2020 Kovid Goyal <kovid at kovidgoyal.net>
|
||||
*
|
||||
* Distributed under terms of the GPL3 license.
|
||||
*/
|
||||
|
||||
#include "hyperlink.h"
|
||||
#include "uthash.h"
|
||||
#include <string.h>
|
||||
|
||||
#define MAX_KEY_LEN 2048
|
||||
#define MAX_ID_LEN 256
|
||||
#define MAX_ADDS_BEFORE_GC 256
|
||||
#undef uthash_fatal
|
||||
#define uthash_fatal(msg) fatal(msg)
|
||||
|
||||
typedef struct {
|
||||
const char *key;
|
||||
hyperlink_id_type id;
|
||||
UT_hash_handle hh;
|
||||
} HyperLinkEntry;
|
||||
|
||||
|
||||
typedef struct {
|
||||
HyperLinkEntry *hyperlinks;
|
||||
unsigned int max_link_id, num_of_adds_since_garbage_collection;
|
||||
} HyperLinkPool;
|
||||
|
||||
|
||||
static void
|
||||
free_hyperlink_entry(HyperLinkEntry *s) {
|
||||
free((void*)s->key);
|
||||
free(s);
|
||||
}
|
||||
|
||||
static void
|
||||
clear_pool(HyperLinkPool *pool) {
|
||||
if (pool->hyperlinks) {
|
||||
HyperLinkEntry *tmp, *s;
|
||||
HASH_ITER(hh, pool->hyperlinks, s, tmp) {
|
||||
HASH_DEL(pool->hyperlinks, s);
|
||||
free_hyperlink_entry(s); s = NULL;
|
||||
}
|
||||
pool->max_link_id = 0;
|
||||
}
|
||||
}
|
||||
|
||||
HYPERLINK_POOL_HANDLE
|
||||
alloc_hyperlink_pool(void) {
|
||||
return calloc(1, sizeof(HyperLinkPool));
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
clear_hyperlink_pool(HYPERLINK_POOL_HANDLE h) {
|
||||
if (h) {
|
||||
HyperLinkPool *pool = (HyperLinkPool*)h;
|
||||
clear_pool(pool);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
free_hyperlink_pool(HYPERLINK_POOL_HANDLE h) {
|
||||
if (h) {
|
||||
HyperLinkPool *pool = (HyperLinkPool*)h;
|
||||
clear_pool(pool);
|
||||
free(pool);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
screen_garbage_collect_hyperlink_pool(Screen *screen) {
|
||||
HyperLinkPool *pool = (HyperLinkPool*)screen->hyperlink_pool;
|
||||
pool->num_of_adds_since_garbage_collection = 0;
|
||||
if (!pool->max_link_id) return;
|
||||
hyperlink_id_type *map = calloc(HYPERLINK_MAX_NUMBER + 4, sizeof(hyperlink_id_type));
|
||||
if (!map) fatal("Out of memory");
|
||||
hyperlink_id_type num = remap_hyperlink_ids(screen, map);
|
||||
if (num) {
|
||||
HyperLinkEntry *s, *tmp;
|
||||
pool->max_link_id = 0;
|
||||
HASH_ITER(hh, pool->hyperlinks, s, tmp) {
|
||||
if (map[s->id]) {
|
||||
s->id = map[s->id];
|
||||
pool->max_link_id = MAX(pool->max_link_id, s->id);
|
||||
} else {
|
||||
HASH_DEL(pool->hyperlinks, s);
|
||||
free_hyperlink_entry(s); s = NULL;
|
||||
}
|
||||
}
|
||||
} else clear_pool(pool);
|
||||
free(map);
|
||||
}
|
||||
|
||||
|
||||
hyperlink_id_type
|
||||
get_id_for_hyperlink(Screen *screen, const char *id, const char *url) {
|
||||
if (!url) return 0;
|
||||
HyperLinkPool *pool = (HyperLinkPool*)screen->hyperlink_pool;
|
||||
static char key[MAX_KEY_LEN] = {0};
|
||||
int keylen = snprintf(key, MAX_KEY_LEN-1, "%.*s:%s", MAX_ID_LEN, id ? id : "", url);
|
||||
if (keylen < 0) keylen = strlen(key);
|
||||
else keylen = MIN(keylen, MAX_KEY_LEN - 2); // snprintf returns how many chars it would have written in case of truncation
|
||||
key[keylen] = 0;
|
||||
HyperLinkEntry *s = NULL;
|
||||
if (pool->hyperlinks) {
|
||||
HASH_FIND_STR(pool->hyperlinks, key, s);
|
||||
if (s) {
|
||||
// Remove and re-add s so that it is the last entry in the hash table and
|
||||
// The first entry is discarded when hash table is full.
|
||||
HASH_DEL(pool->hyperlinks, s);
|
||||
HASH_ADD_KEYPTR(hh, pool->hyperlinks, s->key, strlen(s->key), s);
|
||||
return s->id;
|
||||
}
|
||||
}
|
||||
hyperlink_id_type new_id = 0;
|
||||
if (pool->num_of_adds_since_garbage_collection >= MAX_ADDS_BEFORE_GC) screen_garbage_collect_hyperlink_pool(screen);
|
||||
if (pool->max_link_id >= HYPERLINK_MAX_NUMBER && pool->hyperlinks) {
|
||||
log_error("Too many hyperlinks, discarding oldest, this means some hyperlinks might be incorrect");
|
||||
new_id = pool->hyperlinks->id;
|
||||
HyperLinkEntry *s = pool->hyperlinks;
|
||||
HASH_DEL(pool->hyperlinks, s);
|
||||
free_hyperlink_entry(s); s = NULL;
|
||||
}
|
||||
s = malloc(sizeof(HyperLinkEntry));
|
||||
if (!s) fatal("Out of memory");
|
||||
s->key = malloc(keylen + 1);
|
||||
if (!s->key) fatal("Out of memory");
|
||||
memcpy((void*)s->key, key, keylen + 1);
|
||||
s->id = new_id ? new_id : ++pool->max_link_id;
|
||||
HASH_ADD_KEYPTR(hh, pool->hyperlinks, s->key, keylen, s);
|
||||
pool->num_of_adds_since_garbage_collection++;
|
||||
return s->id;
|
||||
}
|
||||
|
||||
const char*
|
||||
get_hyperlink_for_id(const HYPERLINK_POOL_HANDLE handle, hyperlink_id_type id, bool only_url) {
|
||||
const HyperLinkPool *pool = (HyperLinkPool*)handle;
|
||||
HyperLinkEntry *s, *tmp;
|
||||
HASH_ITER(hh, pool->hyperlinks, s, tmp) {
|
||||
if (s->id == id) return only_url ? strstr(s->key, ":") + 1 : s->key;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
PyObject*
|
||||
screen_hyperlinks_as_list(Screen *screen) {
|
||||
HyperLinkPool *pool = (HyperLinkPool*)screen->hyperlink_pool;
|
||||
PyObject *ans = PyList_New(0);
|
||||
HyperLinkEntry *s, *tmp;
|
||||
HASH_ITER(hh, pool->hyperlinks, s, tmp) {
|
||||
PyObject *e = Py_BuildValue("sH", s->key, s->id);
|
||||
PyList_Append(ans, e);
|
||||
Py_DECREF(e);
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
17
kitty/hyperlink.h
Normal file
17
kitty/hyperlink.h
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Kovid Goyal <kovid at kovidgoyal.net>
|
||||
*
|
||||
* Distributed under terms of the GPL3 license.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "screen.h"
|
||||
|
||||
HYPERLINK_POOL_HANDLE alloc_hyperlink_pool(void);
|
||||
void free_hyperlink_pool(HYPERLINK_POOL_HANDLE);
|
||||
void clear_hyperlink_pool(HYPERLINK_POOL_HANDLE);
|
||||
hyperlink_id_type get_id_for_hyperlink(Screen*, const char*, const char*);
|
||||
hyperlink_id_type remap_hyperlink_ids(Screen *self, hyperlink_id_type *map);
|
||||
PyObject* screen_hyperlinks_as_list(Screen *screen);
|
||||
void screen_garbage_collect_hyperlink_pool(Screen *screen);
|
||||
@@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional, Sequence, Tuple
|
||||
|
||||
from .boss import Boss
|
||||
from .child import Child
|
||||
from .cli import parse_args
|
||||
from .cli import parse_args, WATCHER_DEFINITION
|
||||
from .cli_stub import LaunchCLIOptions
|
||||
from .constants import resolve_custom_file
|
||||
from .fast_data_types import set_clipboard_string
|
||||
@@ -81,7 +81,11 @@ currently active window.
|
||||
--copy-env
|
||||
type=bool-set
|
||||
Copy the environment variables from the currently active window into the
|
||||
newly launched child process.
|
||||
newly launched child process. Note that most shells only set environment
|
||||
variables for child processes, so this will only copy the environment
|
||||
variables that the shell process itself has not the environment variables
|
||||
child processes inside the shell see. To copy that enviroment, use the
|
||||
kitty remote control feature with :code:`kitty @launch --copy-env`.
|
||||
|
||||
|
||||
--location
|
||||
@@ -112,11 +116,11 @@ computers (for example, over ssh) or as other users.
|
||||
type=choices
|
||||
default=none
|
||||
choices=none,@selection,@screen,@screen_scrollback,@alternate,@alternate_scrollback
|
||||
Pass the screen contents as :code:`STDIN` to the child process. @selection is
|
||||
the currently selected text. @screen is the contents of the currently active
|
||||
window. @screen_scrollback is the same as @screen, but includes the scrollback
|
||||
buffer as well. @alternate is the secondary screen of the current active
|
||||
window. For example if you run a full screen terminal application, the
|
||||
Pass the screen contents as :code:`STDIN` to the child process. :code:`@selection` is
|
||||
the currently selected text. :code:`@screen` is the contents of the currently active
|
||||
window. :code:`@screen_scrollback` is the same as :code:`@screen`, but includes the
|
||||
scrollback buffer as well. :code:`@alternate` is the secondary screen of the current
|
||||
active window. For example if you run a full screen terminal application, the
|
||||
secondary screen will be the screen you return to when quitting the
|
||||
application.
|
||||
|
||||
@@ -152,13 +156,7 @@ Set the WM_NAME property on X11 for the newly created OS Window when using
|
||||
:option:`launch --type`=os-window. Defaults to :option:`launch --os-window-class`.
|
||||
|
||||
|
||||
--watcher -w
|
||||
type=list
|
||||
Path to a python file. Appropriately named functions in this file will be called
|
||||
for various events, such as when the window is resized or closed. See the section
|
||||
on watchers in the launch command documentation :doc:`launch`. Relative paths are
|
||||
resolved relative to the kitty config directory.
|
||||
'''
|
||||
''' + WATCHER_DEFINITION
|
||||
|
||||
|
||||
def parse_launch_args(args: Optional[Sequence[str]] = None) -> Tuple[LaunchCLIOptions, List[str]]:
|
||||
@@ -202,12 +200,12 @@ def tab_for_window(boss: Boss, opts: LaunchCLIOptions, target_tab: Optional[Tab]
|
||||
return tab
|
||||
|
||||
|
||||
def load_watch_modules(opts: LaunchCLIOptions) -> Optional[Watchers]:
|
||||
if not opts.watcher:
|
||||
def load_watch_modules(watchers: Sequence[str]) -> Optional[Watchers]:
|
||||
if not watchers:
|
||||
return None
|
||||
import runpy
|
||||
ans = Watchers()
|
||||
for path in opts.watcher:
|
||||
for path in watchers:
|
||||
path = resolve_custom_file(path)
|
||||
m = runpy.run_path(path, run_name='__kitty_watcher__')
|
||||
w = m.get('on_close')
|
||||
@@ -216,6 +214,9 @@ def load_watch_modules(opts: LaunchCLIOptions) -> Optional[Watchers]:
|
||||
w = m.get('on_resize')
|
||||
if callable(w):
|
||||
ans.on_resize.append(w)
|
||||
w = m.get('on_focus_change')
|
||||
if callable(w):
|
||||
ans.on_focus_change.append(w)
|
||||
return ans
|
||||
|
||||
|
||||
@@ -304,7 +305,7 @@ def launch(boss: Boss, opts: LaunchCLIOptions, args: List[str], target_tab: Opti
|
||||
else:
|
||||
tab = tab_for_window(boss, opts, target_tab)
|
||||
if tab is not None:
|
||||
watchers = load_watch_modules(opts)
|
||||
watchers = load_watch_modules(opts.watcher)
|
||||
new_window: Window = tab.new_window(env=env or None, watchers=watchers or None, **kw)
|
||||
if opts.keep_focus and active:
|
||||
boss.set_active_window(active, switch_os_window_if_needed=True)
|
||||
|
||||
@@ -16,13 +16,12 @@ from kitty.fast_data_types import (
|
||||
from kitty.options_stub import Options
|
||||
from kitty.typing import TypedDict, WindowType
|
||||
from kitty.window_list import WindowGroup, WindowList
|
||||
from kitty.borders import BorderColor
|
||||
|
||||
|
||||
class Borders(NamedTuple):
|
||||
left: bool
|
||||
top: bool
|
||||
right: bool
|
||||
bottom: bool
|
||||
class BorderLine(NamedTuple):
|
||||
edges: Edges = Edges()
|
||||
color: BorderColor = BorderColor.inactive
|
||||
|
||||
|
||||
class LayoutOpts:
|
||||
@@ -39,8 +38,6 @@ class LayoutData(NamedTuple):
|
||||
content_size: int = 0
|
||||
|
||||
|
||||
all_borders = Borders(True, True, True, True)
|
||||
no_borders = Borders(False, False, False, False)
|
||||
DecorationPairs = Sequence[Tuple[int, int]]
|
||||
LayoutDimension = Generator[LayoutData, None, None]
|
||||
ListOfWindows = List[WindowType]
|
||||
@@ -330,10 +327,11 @@ class Layout:
|
||||
bias: Optional[Sequence[float]] = None,
|
||||
start: Optional[int] = None,
|
||||
size: Optional[int] = None,
|
||||
offset: int = 0
|
||||
offset: int = 0,
|
||||
border_mult: int = 1
|
||||
) -> LayoutDimension:
|
||||
decoration_pairs = tuple(
|
||||
(g.decoration('left'), g.decoration('right')) for i, g in
|
||||
(g.decoration('left', border_mult=border_mult), g.decoration('right', border_mult=border_mult)) for i, g in
|
||||
enumerate(groups) if i >= offset
|
||||
)
|
||||
if start is None:
|
||||
@@ -348,10 +346,11 @@ class Layout:
|
||||
bias: Optional[Sequence[float]] = None,
|
||||
start: Optional[int] = None,
|
||||
size: Optional[int] = None,
|
||||
offset: int = 0
|
||||
offset: int = 0,
|
||||
border_mult: int = 1
|
||||
) -> LayoutDimension:
|
||||
decoration_pairs = tuple(
|
||||
(g.decoration('top'), g.decoration('bottom')) for i, g in
|
||||
(g.decoration('top', border_mult=border_mult), g.decoration('bottom', border_mult=border_mult)) for i, g in
|
||||
enumerate(groups) if i >= offset
|
||||
)
|
||||
if start is None:
|
||||
@@ -375,21 +374,9 @@ class Layout:
|
||||
def compute_needs_borders_map(self, all_windows: WindowList) -> Dict[int, bool]:
|
||||
return all_windows.compute_needs_borders_map(lgd.draw_active_borders)
|
||||
|
||||
def resolve_borders(self, all_windows: WindowList) -> Generator[Borders, None, None]:
|
||||
if lgd.draw_minimal_borders:
|
||||
needs_borders_map = self.compute_needs_borders_map(all_windows)
|
||||
yield from self.minimal_borders(all_windows, needs_borders_map)
|
||||
else:
|
||||
for i in range(all_windows.num_groups):
|
||||
yield all_borders
|
||||
|
||||
def window_independent_borders(self, windows: WindowList) -> Generator[Edges, None, None]:
|
||||
def minimal_borders(self, windows: WindowList) -> Generator[BorderLine, None, None]:
|
||||
return
|
||||
yield Edges() # type: ignore
|
||||
|
||||
def minimal_borders(self, windows: WindowList, needs_borders_map: Dict[int, bool]) -> Generator[Borders, None, None]:
|
||||
for needs_border in needs_borders_map.values():
|
||||
yield all_borders if needs_border else no_borders
|
||||
yield BorderLine() # type: ignore
|
||||
|
||||
def layout_action(self, action_name: str, args: Sequence[str], all_windows: WindowList) -> Optional[bool]:
|
||||
pass
|
||||
|
||||
@@ -5,15 +5,18 @@
|
||||
from functools import lru_cache
|
||||
from itertools import repeat
|
||||
from math import ceil, floor
|
||||
from typing import Callable, Dict, Generator, List, Optional, Sequence, Tuple
|
||||
from typing import (
|
||||
Callable, Dict, Generator, List, Optional, Sequence, Set, Tuple
|
||||
)
|
||||
|
||||
from kitty.borders import BorderColor
|
||||
from kitty.constants import Edges
|
||||
from kitty.typing import WindowType
|
||||
from kitty.window_list import WindowGroup, WindowList
|
||||
|
||||
from .base import (
|
||||
Layout, LayoutData, LayoutDimension, ListOfWindows, NeighborsMap,
|
||||
layout_dimension, lgd, variable_bias
|
||||
BorderLine, Layout, LayoutData, LayoutDimension, ListOfWindows,
|
||||
NeighborsMap, layout_dimension, lgd, variable_bias
|
||||
)
|
||||
from .tall import neighbors_for_tall_window
|
||||
|
||||
@@ -35,6 +38,7 @@ def calc_grid_size(n: int) -> Tuple[int, int, int, int]:
|
||||
class Grid(Layout):
|
||||
|
||||
name = 'grid'
|
||||
no_minimal_window_borders = True
|
||||
|
||||
def remove_all_biases(self) -> bool:
|
||||
self.biased_rows: Dict[int, float] = {}
|
||||
@@ -165,9 +169,7 @@ class Grid(Layout):
|
||||
|
||||
def position_window_in_grid_cell(window_idx: int, xl: LayoutData, yl: LayoutData) -> None:
|
||||
wg = groups[window_idx]
|
||||
edges = Edges(
|
||||
wg.decoration('left'), wg.decoration('top'), wg.decoration('right'), wg.decoration('bottom')
|
||||
)
|
||||
edges = Edges(wg.decoration('left'), wg.decoration('top'), wg.decoration('right'), wg.decoration('bottom'))
|
||||
xl = layout(xl, lgd.cell_width, edges.left, edges.right)
|
||||
yl = layout(yl, lgd.cell_height, edges.top, edges.bottom)
|
||||
self.set_window_group_geometry(wg, xl, yl)
|
||||
@@ -176,34 +178,72 @@ class Grid(Layout):
|
||||
n, nrows, ncols, special_rows, special_col, on_col_done):
|
||||
position_window_in_grid_cell(window_idx, xl, yl)
|
||||
|
||||
def window_independent_borders(self, all_windows: WindowList) -> Generator[Edges, None, None]:
|
||||
def minimal_borders(self, all_windows: WindowList) -> Generator[BorderLine, None, None]:
|
||||
n = all_windows.num_groups
|
||||
if not lgd.draw_minimal_borders or n < 2:
|
||||
return
|
||||
needs_borders_map = all_windows.compute_needs_borders_map(lgd.draw_active_borders)
|
||||
ncols, nrows, special_rows, special_col = calc_grid_size(n)
|
||||
row_borders: List[List[Edges]] = [[]]
|
||||
col_borders: List[Edges] = []
|
||||
is_first_row: Set[int] = set()
|
||||
is_last_row: Set[int] = set()
|
||||
is_first_column: Set[int] = set()
|
||||
is_last_column: Set[int] = set()
|
||||
groups = tuple(all_windows.iter_all_layoutable_groups())
|
||||
bw = groups[0].effective_border()
|
||||
if not bw:
|
||||
return
|
||||
xl: LayoutData = LayoutData()
|
||||
yl: LayoutData = LayoutData()
|
||||
prev_col_windows: List[int] = []
|
||||
layout_data_map: Dict[int, Tuple[LayoutData, LayoutData]] = {}
|
||||
|
||||
def on_col_done(col_windows: List[int]) -> None:
|
||||
left = xl.content_pos + xl.content_size + xl.space_after - bw // 2
|
||||
col_borders.append(Edges(left, lgd.central.top, left + bw, lgd.central.bottom))
|
||||
row_borders.append([])
|
||||
nonlocal prev_col_windows, is_first_column
|
||||
if col_windows:
|
||||
is_first_row.add(groups[col_windows[0]].id)
|
||||
is_last_row.add(groups[col_windows[-1]].id)
|
||||
if not prev_col_windows:
|
||||
is_first_column = {groups[x].id for x in col_windows}
|
||||
prev_col_windows = col_windows
|
||||
|
||||
all_groups_in_order: List[WindowGroup] = []
|
||||
for window_idx, xl, yl in self.layout_windows(n, nrows, ncols, special_rows, special_col, on_col_done):
|
||||
top = yl.content_pos + yl.content_size + yl.space_after - bw // 2
|
||||
right = xl.content_pos + xl.content_size + xl.space_after
|
||||
row_borders[-1].append(Edges(xl.content_pos - xl.space_before, top, right, top + bw))
|
||||
wg = groups[window_idx]
|
||||
all_groups_in_order.append(wg)
|
||||
layout_data_map[wg.id] = xl, yl
|
||||
is_last_column = {groups[x].id for x in prev_col_windows}
|
||||
active_group = all_windows.active_group
|
||||
|
||||
for border in col_borders[:-1]:
|
||||
yield border
|
||||
def ends(yl: LayoutData) -> Tuple[int, int]:
|
||||
return yl.content_pos - yl.space_before, yl.content_pos + yl.content_size + yl.space_after
|
||||
|
||||
for rows in row_borders:
|
||||
for border in rows[:-1]:
|
||||
yield border
|
||||
def borders_for_window(gid: int) -> Generator[Edges, None, None]:
|
||||
xl, yl = layout_data_map[gid]
|
||||
left, right = ends(xl)
|
||||
top, bottom = ends(yl)
|
||||
first_row, last_row = gid in is_first_row, gid in is_last_row
|
||||
first_column, last_column = gid in is_first_column, gid in is_last_column
|
||||
|
||||
# Horizontal
|
||||
if not first_row:
|
||||
yield Edges(left, top, right, top + bw)
|
||||
if not last_row:
|
||||
yield Edges(left, bottom - bw, right, bottom)
|
||||
|
||||
# Vertical
|
||||
if not first_column:
|
||||
yield Edges(left, top, left + bw, bottom)
|
||||
if not last_column:
|
||||
yield Edges(right - bw, top, right, bottom)
|
||||
|
||||
for wg in all_groups_in_order:
|
||||
for edges in borders_for_window(wg.id):
|
||||
yield BorderLine(edges)
|
||||
for wg in all_groups_in_order:
|
||||
if needs_borders_map.get(wg.id):
|
||||
color = BorderColor.active if wg is active_group else BorderColor.bell
|
||||
for edges in borders_for_window(wg.id):
|
||||
yield BorderLine(edges, color)
|
||||
|
||||
def neighbors_for_window(self, window: WindowType, all_windows: WindowList) -> NeighborsMap:
|
||||
n = all_windows.num_groups
|
||||
|
||||
@@ -3,19 +3,26 @@
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from typing import (
|
||||
Collection, Dict, Generator, Optional, Sequence, Tuple, Union
|
||||
Collection, Dict, Generator, List, NamedTuple, Optional, Sequence, Tuple,
|
||||
Union
|
||||
)
|
||||
|
||||
from kitty.borders import BorderColor
|
||||
from kitty.constants import Edges, WindowGeometry
|
||||
from kitty.typing import EdgeLiteral, WindowType
|
||||
from kitty.window_list import WindowGroup, WindowList
|
||||
|
||||
from .base import (
|
||||
Layout, LayoutOpts, NeighborsMap, blank_rects_for_window, lgd,
|
||||
BorderLine, Layout, LayoutOpts, NeighborsMap, blank_rects_for_window, lgd,
|
||||
window_geometry_from_layouts
|
||||
)
|
||||
|
||||
|
||||
class Extent(NamedTuple):
|
||||
start: int = 0
|
||||
end: int = 0
|
||||
|
||||
|
||||
class Pair:
|
||||
|
||||
def __init__(self, horizontal: bool = True):
|
||||
@@ -23,11 +30,13 @@ class Pair:
|
||||
self.one: Optional[Union[Pair, int]] = None
|
||||
self.two: Optional[Union[Pair, int]] = None
|
||||
self.bias = 0.5
|
||||
self.between_border: Optional[Edges] = None
|
||||
self.top = self.left = self.width = self.height = 0
|
||||
self.between_borders: List[Edges] = []
|
||||
self.first_extent = self.second_extent = Extent()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return 'Pair(horizontal={}, bias={:.2f}, one={}, two={}, between_border={})'.format(
|
||||
self.horizontal, self.bias, self.one, self.two, self.between_border)
|
||||
return 'Pair(horizontal={}, bias={:.2f}, one={}, two={}, between_borders={})'.format(
|
||||
self.horizontal, self.bias, self.one, self.two, self.between_borders)
|
||||
|
||||
def all_window_ids(self) -> Generator[int, None, None]:
|
||||
if self.one is not None:
|
||||
@@ -168,7 +177,12 @@ class Pair:
|
||||
id_window_map: Dict[int, WindowGroup],
|
||||
layout_object: Layout
|
||||
) -> None:
|
||||
self.between_border = None
|
||||
self.between_borders = []
|
||||
self.left, self.top, self.width, self.height = left, top, width, height
|
||||
bw = self.effective_border(id_window_map) if lgd.draw_minimal_borders else 0
|
||||
border_mult = 0 if lgd.draw_minimal_borders else 1
|
||||
bw2 = bw * 2
|
||||
self.first_extent = self.second_extent = Extent()
|
||||
if self.one is None or self.two is None:
|
||||
q = self.one or self.two
|
||||
if isinstance(q, Pair):
|
||||
@@ -176,56 +190,62 @@ class Pair:
|
||||
if q is None:
|
||||
return
|
||||
wg = id_window_map[q]
|
||||
xl = next(layout_object.xlayout(iter((wg,)), start=left, size=width))
|
||||
yl = next(layout_object.ylayout(iter((wg,)), start=top, size=height))
|
||||
xl = next(layout_object.xlayout(iter((wg,)), start=left, size=width, border_mult=border_mult))
|
||||
yl = next(layout_object.ylayout(iter((wg,)), start=top, size=height, border_mult=border_mult))
|
||||
geom = window_geometry_from_layouts(xl, yl)
|
||||
self.first_extent = Extent(left, left + width)
|
||||
self.apply_window_geometry(q, geom, id_window_map, layout_object)
|
||||
return
|
||||
bw = self.effective_border(id_window_map) if lgd.draw_minimal_borders else 0
|
||||
b1 = bw // 2
|
||||
b2 = bw - b1
|
||||
if self.horizontal:
|
||||
w1 = max(2*lgd.cell_width + 1, int(self.bias * width) - b1)
|
||||
w2 = max(2*lgd.cell_width + 1, width - w1 - b1 - b2)
|
||||
w1 = max(2*lgd.cell_width + 1, int(self.bias * width) - bw)
|
||||
w2 = max(2*lgd.cell_width + 1, width - w1 - bw2)
|
||||
self.first_extent = Extent(max(0, left - bw), left + w1 + bw)
|
||||
self.second_extent = Extent(left + w1 + bw, left + width + bw)
|
||||
if isinstance(self.one, Pair):
|
||||
self.one.layout_pair(left, top, w1, height, id_window_map, layout_object)
|
||||
else:
|
||||
wg = id_window_map[self.one]
|
||||
yl = next(layout_object.ylayout(iter((wg,)), start=top, size=height))
|
||||
xl = next(layout_object.xlayout(iter((wg,)), start=left, size=w1))
|
||||
yl = next(layout_object.ylayout(iter((wg,)), start=top, size=height, border_mult=border_mult))
|
||||
xl = next(layout_object.xlayout(iter((wg,)), start=left, size=w1, border_mult=border_mult))
|
||||
geom = window_geometry_from_layouts(xl, yl)
|
||||
self.apply_window_geometry(self.one, geom, id_window_map, layout_object)
|
||||
if b1 + b2:
|
||||
self.between_border = Edges(left + w1, top, left + w1 + b1 + b2, top + height)
|
||||
left += b1 + b2
|
||||
self.between_borders = [
|
||||
Edges(left + w1, top, left + w1 + bw, top + height),
|
||||
Edges(left + w1 + bw, top, left + w1 + bw2, top + height),
|
||||
]
|
||||
left += bw2
|
||||
if isinstance(self.two, Pair):
|
||||
self.two.layout_pair(left + w1, top, w2, height, id_window_map, layout_object)
|
||||
else:
|
||||
wg = id_window_map[self.two]
|
||||
xl = next(layout_object.xlayout(iter((wg,)), start=left + w1, size=w2))
|
||||
yl = next(layout_object.ylayout(iter((wg,)), start=top, size=height))
|
||||
xl = next(layout_object.xlayout(iter((wg,)), start=left + w1, size=w2, border_mult=border_mult))
|
||||
yl = next(layout_object.ylayout(iter((wg,)), start=top, size=height, border_mult=border_mult))
|
||||
geom = window_geometry_from_layouts(xl, yl)
|
||||
self.apply_window_geometry(self.two, geom, id_window_map, layout_object)
|
||||
else:
|
||||
h1 = max(2*lgd.cell_height + 1, int(self.bias * height) - b1)
|
||||
h2 = max(2*lgd.cell_height + 1, height - h1 - b1 - b2)
|
||||
h1 = max(2*lgd.cell_height + 1, int(self.bias * height) - bw)
|
||||
h2 = max(2*lgd.cell_height + 1, height - h1 - bw2)
|
||||
self.first_extent = Extent(max(0, top - bw), top + h1 + bw)
|
||||
self.second_extent = Extent(top + h1 + bw, top + height + bw)
|
||||
if isinstance(self.one, Pair):
|
||||
self.one.layout_pair(left, top, width, h1, id_window_map, layout_object)
|
||||
else:
|
||||
wg = id_window_map[self.one]
|
||||
xl = next(layout_object.xlayout(iter((wg,)), start=left, size=width))
|
||||
yl = next(layout_object.ylayout(iter((wg,)), start=top, size=h1))
|
||||
xl = next(layout_object.xlayout(iter((wg,)), start=left, size=width, border_mult=border_mult))
|
||||
yl = next(layout_object.ylayout(iter((wg,)), start=top, size=h1, border_mult=border_mult))
|
||||
geom = window_geometry_from_layouts(xl, yl)
|
||||
self.apply_window_geometry(self.one, geom, id_window_map, layout_object)
|
||||
if b1 + b2:
|
||||
self.between_border = Edges(left, top + h1, left + width, top + h1 + b1 + b2)
|
||||
top += b1 + b2
|
||||
self.between_borders = [
|
||||
Edges(left, top + h1, left + width, top + h1 + bw),
|
||||
Edges(left, top + h1 + bw, left + width, top + h1 + bw2),
|
||||
]
|
||||
top += bw2
|
||||
if isinstance(self.two, Pair):
|
||||
self.two.layout_pair(left, top + h1, width, h2, id_window_map, layout_object)
|
||||
else:
|
||||
wg = id_window_map[self.two]
|
||||
xl = next(layout_object.xlayout(iter((wg,)), start=left, size=width))
|
||||
yl = next(layout_object.ylayout(iter((wg,)), start=top + h1, size=h2))
|
||||
xl = next(layout_object.xlayout(iter((wg,)), start=left, size=width, border_mult=border_mult))
|
||||
yl = next(layout_object.ylayout(iter((wg,)), start=top + h1, size=h2, border_mult=border_mult))
|
||||
geom = window_geometry_from_layouts(xl, yl)
|
||||
self.apply_window_geometry(self.two, geom, id_window_map, layout_object)
|
||||
|
||||
@@ -244,6 +264,55 @@ class Pair:
|
||||
return parent.modify_size_of_child(which, increment, is_horizontal, layout_object)
|
||||
return False
|
||||
|
||||
def borders_for_window(self, layout_object: 'Splits', window_id: int) -> Generator[Edges, None, None]:
|
||||
is_first = self.one == window_id
|
||||
if self.between_borders:
|
||||
yield self.between_borders[0 if is_first else 1]
|
||||
q = self
|
||||
found_same_direction = found_transverse1 = found_transverse2 = False
|
||||
while not (found_same_direction and found_transverse1 and found_transverse2):
|
||||
parent = q.parent(layout_object.pairs_root)
|
||||
if parent is None:
|
||||
break
|
||||
q = parent
|
||||
if not q.between_borders:
|
||||
continue
|
||||
if q.horizontal == self.horizontal:
|
||||
if not found_same_direction:
|
||||
if self.horizontal:
|
||||
is_before = q.between_borders[0].left <= self.left
|
||||
else:
|
||||
is_before = q.between_borders[0].top <= self.top
|
||||
if is_before == is_first:
|
||||
found_same_direction = True
|
||||
edges = q.between_borders[1 if is_before else 0]
|
||||
if self.horizontal:
|
||||
yield edges._replace(top=self.top, bottom=self.top + self.height)
|
||||
else:
|
||||
yield edges._replace(left=self.left, right=self.left + self.width)
|
||||
else:
|
||||
if self.horizontal:
|
||||
is_before = q.between_borders[0].top <= self.top
|
||||
else:
|
||||
is_before = q.between_borders[0].left <= self.left
|
||||
extent = self.first_extent if is_first else self.second_extent
|
||||
if is_before:
|
||||
if not found_transverse1:
|
||||
found_transverse1 = True
|
||||
edges = q.between_borders[1]
|
||||
if self.horizontal:
|
||||
yield edges._replace(left=extent.start, right=extent.end)
|
||||
else:
|
||||
yield edges._replace(top=extent.start, bottom=extent.end)
|
||||
else:
|
||||
if not found_transverse2:
|
||||
found_transverse2 = True
|
||||
edges = q.between_borders[0]
|
||||
if self.horizontal:
|
||||
yield edges._replace(left=extent.start, right=extent.end)
|
||||
else:
|
||||
yield edges._replace(top=extent.start, bottom=extent.end)
|
||||
|
||||
def neighbors_for_window(self, window_id: int, ans: NeighborsMap, layout_object: 'Splits', all_windows: WindowList) -> None:
|
||||
|
||||
def quadrant(is_horizontal: bool, is_first: bool) -> Tuple[EdgeLiteral, EdgeLiteral]:
|
||||
@@ -325,6 +394,7 @@ class Splits(Layout):
|
||||
name = 'splits'
|
||||
needs_all_windows = True
|
||||
layout_opts = SplitsLayoutOpts({})
|
||||
no_minimal_window_borders = True
|
||||
|
||||
@property
|
||||
def default_axis_is_horizontal(self) -> bool:
|
||||
@@ -417,14 +487,24 @@ class Splits(Layout):
|
||||
pair.bias = 0.5
|
||||
return True
|
||||
|
||||
def window_independent_borders(self, all_windows: WindowList) -> Generator[Edges, None, None]:
|
||||
def minimal_borders(self, all_windows: WindowList) -> Generator[BorderLine, None, None]:
|
||||
groups = tuple(all_windows.iter_all_layoutable_groups())
|
||||
window_count = len(groups)
|
||||
if not lgd.draw_minimal_borders or window_count < 2:
|
||||
return
|
||||
for pair in self.pairs_root.self_and_descendants():
|
||||
if pair.between_border is not None:
|
||||
yield pair.between_border
|
||||
for edges in pair.between_borders:
|
||||
yield BorderLine(edges)
|
||||
needs_borders_map = all_windows.compute_needs_borders_map(lgd.draw_active_borders)
|
||||
ag = all_windows.active_group
|
||||
active_group_id = -1 if ag is None else ag.id
|
||||
for grp_id, needs_borders in needs_borders_map.items():
|
||||
if needs_borders:
|
||||
qpair = self.pairs_root.pair_for_window(grp_id)
|
||||
if qpair is not None:
|
||||
color = BorderColor.active if grp_id is active_group_id else BorderColor.bell
|
||||
for edges in qpair.borders_for_window(self, grp_id):
|
||||
yield BorderLine(edges, color)
|
||||
|
||||
def neighbors_for_window(self, window: WindowType, all_windows: WindowList) -> NeighborsMap:
|
||||
wg = all_windows.group_for_window(window)
|
||||
|
||||
@@ -5,14 +5,17 @@
|
||||
from itertools import islice, repeat
|
||||
from typing import Dict, Generator, List, Optional, Sequence, Tuple
|
||||
|
||||
from kitty.borders import BorderColor
|
||||
from kitty.conf.utils import to_bool
|
||||
from kitty.constants import Edges
|
||||
from kitty.typing import EdgeLiteral, WindowType
|
||||
from kitty.window_list import WindowList
|
||||
from kitty.window_list import WindowGroup, WindowList
|
||||
|
||||
from .base import (
|
||||
Borders, Layout, LayoutDimension, LayoutOpts, NeighborsMap, all_borders,
|
||||
lgd, no_borders, normalize_biases, safe_increment_bias, variable_bias
|
||||
BorderLine, Layout, LayoutData, LayoutDimension, LayoutOpts, NeighborsMap,
|
||||
lgd, normalize_biases, safe_increment_bias, variable_bias
|
||||
)
|
||||
from .vertical import borders
|
||||
|
||||
|
||||
def neighbors_for_tall_window(
|
||||
@@ -75,9 +78,7 @@ class Tall(Layout):
|
||||
|
||||
name = 'tall'
|
||||
main_is_horizontal = True
|
||||
only_between_border = Borders(False, False, False, True)
|
||||
only_main_border = Borders(False, False, True, False)
|
||||
only_main_border_mirrored = Borders(True, False, False, False)
|
||||
no_minimal_window_borders = True
|
||||
layout_opts = TallLayoutOpts({})
|
||||
main_axis_layout = Layout.xlayout
|
||||
perp_axis_layout = Layout.ylayout
|
||||
@@ -121,27 +122,27 @@ class Tall(Layout):
|
||||
self.biased_map = candidate
|
||||
return before != after
|
||||
|
||||
def do_layout(self, all_windows: WindowList) -> None:
|
||||
def simple_layout(self, all_windows: WindowList) -> Generator[Tuple[WindowGroup, LayoutData, LayoutData, bool], None, None]:
|
||||
num = all_windows.num_groups
|
||||
if num == 1:
|
||||
self.layout_single_window_group(next(all_windows.iter_all_layoutable_groups()))
|
||||
return
|
||||
is_fat = not self.main_is_horizontal
|
||||
mirrored = self.layout_opts.mirrored
|
||||
groups = tuple(all_windows.iter_all_layoutable_groups())
|
||||
main_bias = self.main_bias[::-1] if mirrored else self.main_bias
|
||||
if num <= self.num_full_size_windows + 1:
|
||||
if mirrored:
|
||||
groups = tuple(reversed(groups))
|
||||
if num < self.num_full_size_windows + 1:
|
||||
main_bias = normalize_biases(main_bias[:num])
|
||||
xlayout = self.main_axis_layout(iter(groups), bias=main_bias)
|
||||
for wg, xl in zip(groups, xlayout):
|
||||
yl = next(self.perp_axis_layout(iter((wg,))))
|
||||
if is_fat:
|
||||
xl, yl = yl, xl
|
||||
self.set_window_group_geometry(wg, xl, yl)
|
||||
return
|
||||
if mirrored:
|
||||
groups = tuple(reversed(groups))
|
||||
main_bias = normalize_biases(main_bias[:num])
|
||||
xlayout = self.main_axis_layout(iter(groups), bias=main_bias)
|
||||
for wg, xl in zip(groups, xlayout):
|
||||
yl = next(self.perp_axis_layout(iter((wg,))))
|
||||
if is_fat:
|
||||
xl, yl = yl, xl
|
||||
yield wg, xl, yl, True
|
||||
|
||||
def full_layout(self, all_windows: WindowList) -> Generator[Tuple[WindowGroup, LayoutData, LayoutData, bool], None, None]:
|
||||
is_fat = not self.main_is_horizontal
|
||||
mirrored = self.layout_opts.mirrored
|
||||
groups = tuple(all_windows.iter_all_layoutable_groups())
|
||||
main_bias = self.main_bias[::-1] if mirrored else self.main_bias
|
||||
|
||||
start = lgd.central.top if is_fat else lgd.central.left
|
||||
size = 0
|
||||
@@ -156,19 +157,18 @@ class Tall(Layout):
|
||||
yl = next(self.perp_axis_layout(iter((wg,))))
|
||||
if is_fat:
|
||||
xl, yl = yl, xl
|
||||
self.set_window_group_geometry(wg, xl, yl)
|
||||
yield wg, xl, yl, True
|
||||
else:
|
||||
xlayout = self.main_axis_layout(islice(groups, self.num_full_size_windows + 1), bias=main_bias)
|
||||
attr: EdgeLiteral = 'bottom' if is_fat else 'right'
|
||||
for i, wg in enumerate(groups):
|
||||
if i >= self.num_full_size_windows:
|
||||
break
|
||||
xl = next(xlayout)
|
||||
yl = next(self.perp_axis_layout(iter((wg,))))
|
||||
start = xl.content_pos + xl.content_size + xl.space_after
|
||||
if is_fat:
|
||||
xl, yl = yl, xl
|
||||
geom = self.set_window_group_geometry(wg, xl, yl)
|
||||
start = getattr(geom, attr) + wg.decoration(attr)
|
||||
yield wg, xl, yl, True
|
||||
size = (lgd.central.height if is_fat else lgd.central.width) - start
|
||||
|
||||
ylayout = self.variable_layout(all_windows, self.biased_map)
|
||||
@@ -179,36 +179,20 @@ class Tall(Layout):
|
||||
xl = next(self.main_axis_layout(iter((wg,)), start=start, size=size))
|
||||
if is_fat:
|
||||
xl, yl = yl, xl
|
||||
yield wg, xl, yl, False
|
||||
|
||||
def do_layout(self, all_windows: WindowList) -> None:
|
||||
num = all_windows.num_groups
|
||||
if num == 1:
|
||||
self.layout_single_window_group(next(all_windows.iter_all_layoutable_groups()))
|
||||
return
|
||||
layouts = (self.simple_layout if num <= self.num_full_size_windows + 1 else self.full_layout)(all_windows)
|
||||
for wg, xl, yl, is_full_size in layouts:
|
||||
self.set_window_group_geometry(wg, xl, yl)
|
||||
|
||||
def neighbors_for_window(self, window: WindowType, windows: WindowList) -> NeighborsMap:
|
||||
return neighbors_for_tall_window(self.num_full_size_windows, window, windows, self.layout_opts.mirrored, self.main_is_horizontal)
|
||||
|
||||
def minimal_borders(self, all_windows: WindowList, needs_borders_map: Dict[int, bool]) -> Generator[Borders, None, None]:
|
||||
mirrored = self.layout_opts.mirrored
|
||||
only_main_border = self.only_main_border_mirrored if mirrored else self.only_main_border
|
||||
num = all_windows.num_groups
|
||||
last_i = num - 1
|
||||
groups = tuple(all_windows.iter_all_layoutable_groups())
|
||||
for i, wg in enumerate(groups):
|
||||
if needs_borders_map[wg.id]:
|
||||
yield all_borders
|
||||
continue
|
||||
if i < self.num_full_size_windows:
|
||||
next_window_is_full_sized = last_i == i+1 or i+1 < self.num_full_size_windows
|
||||
if next_window_is_full_sized and needs_borders_map[groups[i+1].id]:
|
||||
yield no_borders
|
||||
else:
|
||||
yield no_borders if i == last_i else only_main_border
|
||||
continue
|
||||
if i == last_i:
|
||||
yield no_borders
|
||||
break
|
||||
if needs_borders_map[groups[i+1].id]:
|
||||
yield no_borders
|
||||
else:
|
||||
yield self.only_between_border
|
||||
|
||||
def layout_action(self, action_name: str, args: Sequence[str], all_windows: WindowList) -> Optional[bool]:
|
||||
if action_name == 'increase_num_full_size_windows':
|
||||
self.layout_opts.full_size += 1
|
||||
@@ -218,13 +202,86 @@ class Tall(Layout):
|
||||
self.layout_opts.full_size -= 1
|
||||
return True
|
||||
|
||||
def minimal_borders(self, all_windows: WindowList) -> Generator[BorderLine, None, None]:
|
||||
num = all_windows.num_groups
|
||||
if num < 2 or not lgd.draw_minimal_borders:
|
||||
return
|
||||
try:
|
||||
bw = next(all_windows.iter_all_layoutable_groups()).effective_border()
|
||||
except StopIteration:
|
||||
bw = 0
|
||||
if not bw:
|
||||
return
|
||||
if num <= self.num_full_size_windows + 1:
|
||||
layout = (x[:3] for x in self.simple_layout(all_windows))
|
||||
yield from borders(layout, self.main_is_horizontal, all_windows)
|
||||
return
|
||||
main_layouts: List[Tuple[WindowGroup, LayoutData, LayoutData]] = []
|
||||
perp_borders: List[BorderLine] = []
|
||||
layouts = (self.simple_layout if num <= self.num_full_size_windows else self.full_layout)(all_windows)
|
||||
needs_borders_map = all_windows.compute_needs_borders_map(lgd.draw_active_borders)
|
||||
active_group = all_windows.active_group
|
||||
mirrored = self.layout_opts.mirrored
|
||||
for wg, xl, yl, is_full_size in layouts:
|
||||
if is_full_size:
|
||||
main_layouts.append((wg, xl, yl))
|
||||
else:
|
||||
color = BorderColor.inactive
|
||||
if needs_borders_map.get(wg.id):
|
||||
color = BorderColor.active if wg is active_group else BorderColor.bell
|
||||
if self.main_is_horizontal:
|
||||
e1 = Edges(
|
||||
xl.content_pos - xl.space_before,
|
||||
yl.content_pos - yl.space_before,
|
||||
xl.content_pos + xl.content_size + xl.space_after,
|
||||
yl.content_pos - yl.space_before + bw
|
||||
)
|
||||
e3 = Edges(
|
||||
xl.content_pos - xl.space_before,
|
||||
yl.content_pos + yl.content_size + yl.space_after - bw,
|
||||
xl.content_pos + xl.content_size + xl.space_after,
|
||||
yl.content_pos + yl.content_size + yl.space_after,
|
||||
)
|
||||
e2 = Edges(
|
||||
xl.content_pos + ((xl.content_size + xl.space_after - bw) if mirrored else -xl.space_before),
|
||||
yl.content_pos - yl.space_before,
|
||||
xl.content_pos + ((xl.content_size + xl.space_after) if mirrored else (bw - xl.space_before)),
|
||||
yl.content_pos + yl.content_size + yl.space_after,
|
||||
)
|
||||
else:
|
||||
e1 = Edges(
|
||||
xl.content_pos - xl.space_before,
|
||||
yl.content_pos - yl.space_before,
|
||||
xl.content_pos - xl.space_before + bw,
|
||||
yl.content_pos + yl.content_size + yl.space_after,
|
||||
)
|
||||
e3 = Edges(
|
||||
xl.content_pos + xl.content_size + xl.space_after - bw,
|
||||
yl.content_pos - yl.space_before,
|
||||
xl.content_pos + xl.content_size + xl.space_after,
|
||||
yl.content_pos + yl.content_size + yl.space_after,
|
||||
)
|
||||
e2 = Edges(
|
||||
xl.content_pos - xl.space_before,
|
||||
yl.content_pos + ((yl.content_size + yl.space_after - bw) if mirrored else -yl.space_before),
|
||||
xl.content_pos + xl.content_size + xl.space_after,
|
||||
yl.content_pos + ((yl.content_size + yl.space_after) if mirrored else (bw - yl.space_before)),
|
||||
)
|
||||
perp_borders.append(BorderLine(e1, color))
|
||||
perp_borders.append(BorderLine(e2, color))
|
||||
perp_borders.append(BorderLine(e3, color))
|
||||
|
||||
mirrored = self.layout_opts.mirrored
|
||||
yield from borders(
|
||||
main_layouts, self.main_is_horizontal, all_windows,
|
||||
start_offset=int(not mirrored), end_offset=int(mirrored)
|
||||
)
|
||||
yield from perp_borders[1:-1]
|
||||
|
||||
|
||||
class Fat(Tall):
|
||||
|
||||
name = 'fat'
|
||||
main_is_horizontal = False
|
||||
only_between_border = Borders(False, False, True, False)
|
||||
only_main_border = Borders(False, False, False, True)
|
||||
only_main_border_mirrored = Borders(False, True, False, False)
|
||||
main_axis_layout = Layout.ylayout
|
||||
perp_axis_layout = Layout.xlayout
|
||||
|
||||
@@ -2,22 +2,71 @@
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from typing import Dict, Generator
|
||||
from typing import Dict, Generator, Iterable, List, Tuple
|
||||
|
||||
from kitty.borders import BorderColor
|
||||
from kitty.constants import Edges
|
||||
from kitty.typing import WindowType
|
||||
from kitty.window_list import WindowList
|
||||
from kitty.window_list import WindowGroup, WindowList
|
||||
|
||||
from .base import (
|
||||
Borders, Layout, LayoutDimension, NeighborsMap, all_borders, no_borders,
|
||||
variable_bias
|
||||
BorderLine, Layout, LayoutData, LayoutDimension, NeighborsMap,
|
||||
lgd, variable_bias
|
||||
)
|
||||
|
||||
|
||||
def borders(
|
||||
data: Iterable[Tuple[WindowGroup, LayoutData, LayoutData]],
|
||||
is_horizontal: bool,
|
||||
all_windows: WindowList,
|
||||
start_offset: int = 1, end_offset: int = 1
|
||||
) -> Generator[BorderLine, None, None]:
|
||||
borders: List[BorderLine] = []
|
||||
active_group = all_windows.active_group
|
||||
needs_borders_map = all_windows.compute_needs_borders_map(lgd.draw_active_borders)
|
||||
try:
|
||||
bw = next(all_windows.iter_all_layoutable_groups()).effective_border()
|
||||
except StopIteration:
|
||||
bw = 0
|
||||
if not bw:
|
||||
return
|
||||
|
||||
for wg, xl, yl in data:
|
||||
if is_horizontal:
|
||||
e1 = Edges(
|
||||
xl.content_pos - xl.space_before, yl.content_pos - yl.space_before,
|
||||
xl.content_pos - xl.space_before + bw, yl.content_pos + yl.content_size + yl.space_after
|
||||
)
|
||||
e2 = Edges(
|
||||
xl.content_pos + xl.content_size + xl.space_after - bw, yl.content_pos - yl.space_before,
|
||||
xl.content_pos + xl.content_size + xl.space_after, yl.content_pos + yl.content_size + yl.space_after
|
||||
)
|
||||
else:
|
||||
e1 = Edges(
|
||||
xl.content_pos - xl.space_before, yl.content_pos - yl.space_before,
|
||||
xl.content_pos + xl.content_size + xl.space_after, yl.content_pos - yl.space_before + bw
|
||||
)
|
||||
e2 = Edges(
|
||||
xl.content_pos - xl.space_before, yl.content_pos + yl.content_size + yl.space_after - bw,
|
||||
xl.content_pos + xl.content_size + xl.space_after, yl.content_pos + yl.content_size + yl.space_after
|
||||
)
|
||||
color = BorderColor.inactive
|
||||
if needs_borders_map.get(wg.id):
|
||||
color = BorderColor.active if wg is active_group else BorderColor.bell
|
||||
borders.append(BorderLine(e1, color))
|
||||
borders.append(BorderLine(e2, color))
|
||||
|
||||
last_idx = len(borders) - 1 - end_offset
|
||||
for i, x in enumerate(borders):
|
||||
if start_offset <= i <= last_idx:
|
||||
yield x
|
||||
|
||||
|
||||
class Vertical(Layout):
|
||||
|
||||
name = 'vertical'
|
||||
main_is_horizontal = False
|
||||
only_between_border = Borders(False, False, False, True)
|
||||
no_minimal_window_borders = True
|
||||
main_axis_layout = Layout.ylayout
|
||||
perp_axis_layout = Layout.xlayout
|
||||
|
||||
@@ -26,6 +75,9 @@ class Vertical(Layout):
|
||||
bias = variable_bias(num_windows, biased_map) if num_windows else None
|
||||
return self.main_axis_layout(all_windows.iter_all_layoutable_groups(), bias=bias)
|
||||
|
||||
def fixed_layout(self, wg: WindowGroup) -> LayoutDimension:
|
||||
return self.perp_axis_layout(iter((wg,)), border_mult=0 if lgd.draw_minimal_borders else 1)
|
||||
|
||||
def remove_all_biases(self) -> bool:
|
||||
self.biased_map: Dict[int, float] = {}
|
||||
return True
|
||||
@@ -45,33 +97,27 @@ class Vertical(Layout):
|
||||
self.biased_map = candidate
|
||||
return True
|
||||
|
||||
def generate_layout_data(self, all_windows: WindowList) -> Generator[Tuple[WindowGroup, LayoutData, LayoutData], None, None]:
|
||||
ylayout = self.variable_layout(all_windows, self.biased_map)
|
||||
for wg, yl in zip(all_windows.iter_all_layoutable_groups(), ylayout):
|
||||
xl = next(self.fixed_layout(wg))
|
||||
if self.main_is_horizontal:
|
||||
xl, yl = yl, xl
|
||||
yield wg, xl, yl
|
||||
|
||||
def do_layout(self, all_windows: WindowList) -> None:
|
||||
window_count = all_windows.num_groups
|
||||
if window_count == 1:
|
||||
self.layout_single_window_group(next(all_windows.iter_all_layoutable_groups()))
|
||||
return
|
||||
|
||||
ylayout = self.variable_layout(all_windows, self.biased_map)
|
||||
for i, (wg, yl) in enumerate(zip(all_windows.iter_all_layoutable_groups(), ylayout)):
|
||||
xl = next(self.perp_axis_layout(iter((wg,))))
|
||||
if self.main_is_horizontal:
|
||||
xl, yl = yl, xl
|
||||
for wg, xl, yl in self.generate_layout_data(all_windows):
|
||||
self.set_window_group_geometry(wg, xl, yl)
|
||||
|
||||
def minimal_borders(self, all_windows: WindowList, needs_borders_map: Dict[int, bool]) -> Generator[Borders, None, None]:
|
||||
last_i = all_windows.num_groups - 1
|
||||
groups = tuple(all_windows.iter_all_layoutable_groups())
|
||||
for i, wg in enumerate(groups):
|
||||
if needs_borders_map[wg.id]:
|
||||
yield all_borders
|
||||
continue
|
||||
if i == last_i:
|
||||
yield no_borders
|
||||
break
|
||||
if needs_borders_map[groups[i+1].id]:
|
||||
yield no_borders
|
||||
else:
|
||||
yield self.only_between_border
|
||||
def minimal_borders(self, all_windows: WindowList) -> Generator[BorderLine, None, None]:
|
||||
window_count = all_windows.num_groups
|
||||
if window_count < 2 or not lgd.draw_minimal_borders:
|
||||
return
|
||||
yield from borders(self.generate_layout_data(all_windows), self.main_is_horizontal, all_windows)
|
||||
|
||||
def neighbors_for_window(self, window: WindowType, all_windows: WindowList) -> NeighborsMap:
|
||||
wg = all_windows.group_for_window(window)
|
||||
@@ -89,6 +135,5 @@ class Horizontal(Vertical):
|
||||
|
||||
name = 'horizontal'
|
||||
main_is_horizontal = True
|
||||
only_between_border = Borders(False, False, True, False)
|
||||
main_axis_layout = Layout.xlayout
|
||||
perp_axis_layout = Layout.ylayout
|
||||
|
||||
@@ -399,30 +399,36 @@ delete_lines(LineBuf *self, PyObject *args) {
|
||||
static PyObject*
|
||||
as_ansi(LineBuf *self, PyObject *callback) {
|
||||
#define as_ansi_doc "as_ansi(callback) -> The contents of this buffer as ANSI escaped text. callback is called with each successive line."
|
||||
static Py_UCS4 t[5120];
|
||||
Line l = {.xnum=self->xnum};
|
||||
// remove trailing empty lines
|
||||
index_type ylimit = self->ynum - 1;
|
||||
bool truncated;
|
||||
const GPUCell *prev_cell = NULL;
|
||||
ANSIBuf output = {0};
|
||||
do {
|
||||
init_line(self, (&l), self->line_map[ylimit]);
|
||||
if (line_as_ansi(&l, t, 5120, &truncated, &prev_cell) != 0) break;
|
||||
line_as_ansi(&l, &output, &prev_cell);
|
||||
if (output.len) break;
|
||||
ylimit--;
|
||||
} while(ylimit > 0);
|
||||
|
||||
for(index_type i = 0; i <= ylimit; i++) {
|
||||
l.continued = ((i < self->ynum - 1) ? self->line_attrs[i+1] : self->line_attrs[i]) & CONTINUED_MASK;
|
||||
init_line(self, (&l), self->line_map[i]);
|
||||
index_type num = line_as_ansi(&l, t, 5120, &truncated, &prev_cell);
|
||||
if (!(l.continued) && num < 5119) t[num++] = 10; // 10 = \n
|
||||
PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, t, num);
|
||||
if (ans == NULL) return PyErr_NoMemory();
|
||||
line_as_ansi(&l, &output, &prev_cell);
|
||||
if (!l.continued) {
|
||||
ensure_space_for(&output, buf, Py_UCS4, output.len + 1, capacity, 2048, false);
|
||||
output.buf[output.len++] = 10; // 10 = \n
|
||||
}
|
||||
PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, output.buf, output.len);
|
||||
if (ans == NULL) { PyErr_NoMemory(); goto end; }
|
||||
PyObject *ret = PyObject_CallFunctionObjArgs(callback, ans, NULL);
|
||||
Py_CLEAR(ans);
|
||||
if (ret == NULL) return NULL;
|
||||
if (ret == NULL) goto end;
|
||||
Py_CLEAR(ret);
|
||||
}
|
||||
end:
|
||||
free(output.buf);
|
||||
if (PyErr_Occurred()) return NULL;
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
@@ -435,7 +441,10 @@ get_line(void *x, int y) {
|
||||
|
||||
static PyObject*
|
||||
as_text(LineBuf *self, PyObject *args) {
|
||||
return as_text_generic(args, self, get_line, self->ynum, self->xnum);
|
||||
ANSIBuf output = {0};
|
||||
PyObject* ans = as_text_generic(args, self, get_line, self->ynum, &output);
|
||||
free(output.buf);
|
||||
return ans;
|
||||
}
|
||||
|
||||
|
||||
@@ -528,7 +537,7 @@ copy_old(LineBuf *self, PyObject *y) {
|
||||
#include "rewrap.h"
|
||||
|
||||
void
|
||||
linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *num_content_lines_before, index_type *num_content_lines_after, HistoryBuf *historybuf, index_type *track_x, index_type *track_y) {
|
||||
linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *num_content_lines_before, index_type *num_content_lines_after, HistoryBuf *historybuf, index_type *track_x, index_type *track_y, ANSIBuf *as_ansi_buf) {
|
||||
index_type first, i;
|
||||
bool is_empty = true;
|
||||
|
||||
@@ -558,7 +567,7 @@ linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *num_content_lines_befo
|
||||
return;
|
||||
}
|
||||
|
||||
rewrap_inner(self, other, first + 1, historybuf, track_x, track_y);
|
||||
rewrap_inner(self, other, first + 1, historybuf, track_x, track_y, as_ansi_buf);
|
||||
*num_content_lines_after = other->line->ynum + 1;
|
||||
for (i = 0; i < *num_content_lines_after; i++) other->line_attrs[i] |= TEXT_DIRTY_MASK;
|
||||
*num_content_lines_before = first + 1;
|
||||
@@ -572,7 +581,9 @@ rewrap(LineBuf *self, PyObject *args) {
|
||||
|
||||
if (!PyArg_ParseTuple(args, "O!O!", &LineBuf_Type, &other, &HistoryBuf_Type, &historybuf)) return NULL;
|
||||
index_type x = 0, y = 0;
|
||||
linebuf_rewrap(self, other, &nclb, &ncla, historybuf, &x, &y);
|
||||
ANSIBuf as_ansi_buf = {0};
|
||||
linebuf_rewrap(self, other, &nclb, &ncla, historybuf, &x, &y, &as_ansi_buf);
|
||||
free(as_ansi_buf.buf);
|
||||
|
||||
return Py_BuildValue("II", nclb, ncla);
|
||||
}
|
||||
|
||||
148
kitty/line.c
148
kitty/line.c
@@ -268,23 +268,48 @@ sprite_at(Line* self, PyObject *x) {
|
||||
return Py_BuildValue("HHH", c->sprite_x, c->sprite_y, c->sprite_z);
|
||||
}
|
||||
|
||||
static inline bool
|
||||
write_sgr(const char *val, Py_UCS4 *buf, index_type buflen, index_type *i) {
|
||||
static char s[128];
|
||||
unsigned int num = snprintf(s, sizeof(s), "\x1b[%sm", val);
|
||||
if (buflen - (*i) < num + 3) return false;
|
||||
for(unsigned int si=0; si < num; si++) buf[(*i)++] = s[si];
|
||||
return true;
|
||||
static inline void
|
||||
write_sgr(const char *val, ANSIBuf *output) {
|
||||
#define W(c) output->buf[output->len++] = c
|
||||
W(0x1b); W('[');
|
||||
for (size_t i = 0; val[i] != 0 && i < 122; i++) W(val[i]);
|
||||
W('m');
|
||||
#undef W
|
||||
}
|
||||
|
||||
index_type
|
||||
line_as_ansi(Line *self, Py_UCS4 *buf, index_type buflen, bool *truncated, const GPUCell** prev_cell) {
|
||||
#define WRITE_SGR(val) { if (!write_sgr(val, buf, buflen, &i)) { *truncated = true; return i; } }
|
||||
#define WRITE_CH(val) if (i > buflen - 1) { *truncated = true; return i; } buf[i++] = val;
|
||||
static inline void
|
||||
write_hyperlink(hyperlink_id_type hid, ANSIBuf *output) {
|
||||
#define W(c) output->buf[output->len++] = c
|
||||
const char *key = hid ? get_hyperlink_for_id(output->hyperlink_pool, hid, false) : NULL;
|
||||
if (!key) hid = 0;
|
||||
output->active_hyperlink_id = hid;
|
||||
W(0x1b); W(']'); W('8');
|
||||
if (!hid) {
|
||||
W(';'); W(';');
|
||||
} else {
|
||||
const char* partition = strstr(key, ":");
|
||||
W(';');
|
||||
if (partition != key) {
|
||||
W('i'); W('d'); W('=');
|
||||
while (key != partition) W(*(key++));
|
||||
}
|
||||
W(';');
|
||||
while(*(++partition)) W(*partition);
|
||||
}
|
||||
W(0x1b); W('\\');
|
||||
#undef W
|
||||
}
|
||||
|
||||
index_type limit = xlimit_for_line(self), i=0;
|
||||
*truncated = false;
|
||||
if (limit == 0) return 0;
|
||||
|
||||
void
|
||||
line_as_ansi(Line *self, ANSIBuf *output, const GPUCell** prev_cell) {
|
||||
#define ENSURE_SPACE(extra) ensure_space_for(output, buf, Py_UCS4, output->len + extra, capacity, 2048, false);
|
||||
#define WRITE_SGR(val) { ENSURE_SPACE(128); write_sgr(val, output); }
|
||||
#define WRITE_CH(val) { ENSURE_SPACE(1); output->buf[output->len++] = val; }
|
||||
#define WRITE_HYPERLINK(val) { ENSURE_SPACE(2256); write_hyperlink(val, output); }
|
||||
output->len = 0;
|
||||
index_type limit = xlimit_for_line(self);
|
||||
if (limit == 0) return;
|
||||
char_type previous_width = 0;
|
||||
|
||||
static const GPUCell blank_cell = { 0 };
|
||||
@@ -297,6 +322,12 @@ line_as_ansi(Line *self, Py_UCS4 *buf, index_type buflen, bool *truncated, const
|
||||
if (previous_width == 2) { previous_width = 0; continue; }
|
||||
ch = ' ';
|
||||
}
|
||||
if (output->hyperlink_pool) {
|
||||
hyperlink_id_type hid = self->cpu_cells[pos].hyperlink_id;
|
||||
if (hid != output->active_hyperlink_id) {
|
||||
WRITE_HYPERLINK(hid);
|
||||
}
|
||||
}
|
||||
|
||||
cell = &self->gpu_cells[pos];
|
||||
|
||||
@@ -320,21 +351,22 @@ line_as_ansi(Line *self, Py_UCS4 *buf, index_type buflen, bool *truncated, const
|
||||
}
|
||||
previous_width = cell->attrs & WIDTH_MASK;
|
||||
}
|
||||
return i;
|
||||
#undef CMP_ATTRS
|
||||
#undef CMP
|
||||
#undef WRITE_SGR
|
||||
#undef WRITE_CH
|
||||
#undef ENSURE_SPACE
|
||||
#undef WRITE_HYPERLINK
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
as_ansi(Line* self, PyObject *a UNUSED) {
|
||||
#define as_ansi_doc "Return the line's contents with ANSI (SGR) escape codes for formatting"
|
||||
static Py_UCS4 t[5120] = {0};
|
||||
bool truncated;
|
||||
const GPUCell *prev_cell = NULL;
|
||||
index_type num = line_as_ansi(self, t, 5120, &truncated, &prev_cell);
|
||||
PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, t, num);
|
||||
ANSIBuf output = {0};
|
||||
line_as_ansi(self, &output, &prev_cell);
|
||||
PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, output.buf, output.len);
|
||||
free(output.buf);
|
||||
return ans;
|
||||
}
|
||||
|
||||
@@ -420,6 +452,7 @@ set_text(Line* self, PyObject *args) {
|
||||
|
||||
for (index_type i = cursor->x; offset < limit && i < self->xnum; i++, offset++) {
|
||||
self->cpu_cells[i].ch = (PyUnicode_READ(kind, buf, offset));
|
||||
self->cpu_cells[i].hyperlink_id = 0;
|
||||
self->gpu_cells[i].attrs = attrs;
|
||||
self->gpu_cells[i].fg = fg;
|
||||
self->gpu_cells[i].bg = bg;
|
||||
@@ -454,15 +487,10 @@ cursor_from(Line* self, PyObject *args) {
|
||||
void
|
||||
line_clear_text(Line *self, unsigned int at, unsigned int num, char_type ch) {
|
||||
attrs_type width = ch ? 1 : 0;
|
||||
#define PREFIX \
|
||||
for (index_type i = at; i < MIN(self->xnum, at + num); i++) { \
|
||||
self->cpu_cells[i].ch = ch; memset(self->cpu_cells[i].cc_idx, 0, sizeof(self->cpu_cells[i].cc_idx)); \
|
||||
self->gpu_cells[i].attrs = (self->gpu_cells[i].attrs & ATTRS_MASK_WITHOUT_WIDTH) | width; \
|
||||
}
|
||||
if (CHAR_IS_BLANK(ch)) {
|
||||
PREFIX
|
||||
} else {
|
||||
PREFIX
|
||||
for (index_type i = at; i < MIN(self->xnum, at + num); i++) {
|
||||
self->cpu_cells[i].ch = ch; memset(self->cpu_cells[i].cc_idx, 0, sizeof(self->cpu_cells[i].cc_idx));
|
||||
self->cpu_cells[i].hyperlink_id = 0;
|
||||
self->gpu_cells[i].attrs = (self->gpu_cells[i].attrs & ATTRS_MASK_WITHOUT_WIDTH) | width;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,6 +514,7 @@ line_apply_cursor(Line *self, Cursor *cursor, unsigned int at, unsigned int num,
|
||||
for (index_type i = at; i < self->xnum && i < at + num; i++) {
|
||||
if (clear_char) {
|
||||
self->cpu_cells[i].ch = BLANK_CHAR;
|
||||
self->cpu_cells[i].hyperlink_id = 0;
|
||||
memset(self->cpu_cells[i].cc_idx, 0, sizeof(self->cpu_cells[i].cc_idx));
|
||||
self->gpu_cells[i].attrs = attrs;
|
||||
clear_sprite_position(self->gpu_cells[i]);
|
||||
@@ -517,6 +546,7 @@ void line_right_shift(Line *self, unsigned int at, unsigned int num) {
|
||||
char_type w = (self->gpu_cells[self->xnum - 1].attrs) & WIDTH_MASK;
|
||||
if (w != 1) {
|
||||
self->cpu_cells[self->xnum - 1].ch = BLANK_CHAR;
|
||||
self->cpu_cells[self->xnum - 1].hyperlink_id = 0;
|
||||
self->gpu_cells[self->xnum - 1].attrs = BLANK_CHAR ? 1 : 0;
|
||||
clear_sprite_position(self->gpu_cells[self->xnum - 1]);
|
||||
}
|
||||
@@ -558,32 +588,35 @@ line_get_char(Line *self, index_type at) {
|
||||
}
|
||||
|
||||
void
|
||||
line_set_char(Line *self, unsigned int at, uint32_t ch, unsigned int width, Cursor *cursor, bool UNUSED is_second) {
|
||||
line_set_char(Line *self, unsigned int at, uint32_t ch, unsigned int width, Cursor *cursor, hyperlink_id_type hyperlink_id) {
|
||||
GPUCell *g = self->gpu_cells + at;
|
||||
if (cursor == NULL) {
|
||||
self->gpu_cells[at].attrs = (self->gpu_cells[at].attrs & ATTRS_MASK_WITHOUT_WIDTH) | width;
|
||||
g->attrs = (self->gpu_cells[at].attrs & ATTRS_MASK_WITHOUT_WIDTH) | width;
|
||||
} else {
|
||||
self->gpu_cells[at].attrs = CURSOR_TO_ATTRS(cursor, width & WIDTH_MASK);
|
||||
self->gpu_cells[at].fg = (cursor->fg & COL_MASK);
|
||||
self->gpu_cells[at].bg = (cursor->bg & COL_MASK);
|
||||
self->gpu_cells[at].decoration_fg = cursor->decoration_fg & COL_MASK;
|
||||
g->attrs = CURSOR_TO_ATTRS(cursor, width & WIDTH_MASK);
|
||||
g->fg = cursor->fg & COL_MASK;
|
||||
g->bg = cursor->bg & COL_MASK;
|
||||
g->decoration_fg = cursor->decoration_fg & COL_MASK;
|
||||
}
|
||||
self->cpu_cells[at].ch = ch;
|
||||
self->cpu_cells[at].hyperlink_id = hyperlink_id;
|
||||
memset(self->cpu_cells[at].cc_idx, 0, sizeof(self->cpu_cells[at].cc_idx));
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
set_char(Line *self, PyObject *args) {
|
||||
#define set_char_doc "set_char(at, ch, width=1, cursor=None) -> Set the character at the specified cell. If cursor is not None, also set attributes from that cursor."
|
||||
#define set_char_doc "set_char(at, ch, width=1, cursor=None, hyperlink_id=0) -> Set the character at the specified cell. If cursor is not None, also set attributes from that cursor."
|
||||
unsigned int at, width=1;
|
||||
int ch;
|
||||
Cursor *cursor = NULL;
|
||||
unsigned int hyperlink_id = 0;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "IC|IO!", &at, &ch, &width, &Cursor_Type, &cursor)) return NULL;
|
||||
if (!PyArg_ParseTuple(args, "IC|IO!I", &at, &ch, &width, &Cursor_Type, &cursor, &hyperlink_id)) return NULL;
|
||||
if (at >= self->xnum) {
|
||||
PyErr_SetString(PyExc_ValueError, "Out of bounds");
|
||||
return NULL;
|
||||
}
|
||||
line_set_char(self, at, ch, width, cursor, false);
|
||||
line_set_char(self, at, ch, width, cursor, hyperlink_id);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
@@ -761,21 +794,17 @@ mark_text_in_line(PyObject *marker, Line *line) {
|
||||
}
|
||||
|
||||
PyObject*
|
||||
as_text_generic(PyObject *args, void *container, get_line_func get_line, index_type lines, index_type columns) {
|
||||
as_text_generic(PyObject *args, void *container, get_line_func get_line, index_type lines, ANSIBuf *ansibuf) {
|
||||
PyObject *callback;
|
||||
int as_ansi = 0, insert_wrap_markers = 0;
|
||||
if (!PyArg_ParseTuple(args, "O|pp", &callback, &as_ansi, &insert_wrap_markers)) return NULL;
|
||||
PyObject *ret = NULL, *t = NULL;
|
||||
Py_UCS4 *buf = NULL;
|
||||
PyObject *nl = PyUnicode_FromString("\n");
|
||||
PyObject *cr = PyUnicode_FromString("\r");
|
||||
PyObject *sgr_reset = PyUnicode_FromString("\x1b[m");
|
||||
if (nl == NULL || cr == NULL || sgr_reset == NULL) goto end;
|
||||
const GPUCell *prev_cell = NULL;
|
||||
if (nl == NULL || cr == NULL) goto end;
|
||||
if (as_ansi) {
|
||||
buf = malloc(sizeof(Py_UCS4) * columns * 100);
|
||||
if (buf == NULL) { PyErr_NoMemory(); goto end; }
|
||||
}
|
||||
ansibuf->active_hyperlink_id = 0;
|
||||
for (index_type y = 0; y < lines; y++) {
|
||||
Line *line = get_line(container, y);
|
||||
if (!line->continued && y > 0) {
|
||||
@@ -784,16 +813,15 @@ as_text_generic(PyObject *args, void *container, get_line_func get_line, index_t
|
||||
Py_CLEAR(ret);
|
||||
}
|
||||
if (as_ansi) {
|
||||
bool truncated;
|
||||
// less has a bug where it resets colors when it sees a \r, so work
|
||||
// around it by resetting SGR at the start of every line. This is
|
||||
// pretty sad performance wise, but I guess it will remain till I
|
||||
// get around to writing a nice pager kitten.
|
||||
// see https://github.com/kovidgoyal/kitty/issues/2381
|
||||
prev_cell = NULL;
|
||||
index_type num = line_as_ansi(line, buf, columns * 100 - 2, &truncated, &prev_cell);
|
||||
t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, buf, num);
|
||||
if (t && num > 0) {
|
||||
line_as_ansi(line, ansibuf, &prev_cell);
|
||||
t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, ansibuf->buf, ansibuf->len);
|
||||
if (t && ansibuf->len > 0) {
|
||||
ret = PyObject_CallFunctionObjArgs(callback, sgr_reset, NULL);
|
||||
if (ret == NULL) goto end;
|
||||
Py_CLEAR(ret);
|
||||
@@ -810,18 +838,37 @@ as_text_generic(PyObject *args, void *container, get_line_func get_line, index_t
|
||||
Py_CLEAR(ret);
|
||||
}
|
||||
}
|
||||
if (ansibuf->active_hyperlink_id) {
|
||||
ansibuf->active_hyperlink_id = 0;
|
||||
t = PyUnicode_FromString("\x1b]8;;\x1b\\");
|
||||
if (t) {
|
||||
ret = PyObject_CallFunctionObjArgs(callback, t, NULL);
|
||||
Py_CLEAR(t);
|
||||
Py_CLEAR(ret);
|
||||
}
|
||||
}
|
||||
end:
|
||||
Py_CLEAR(nl); Py_CLEAR(cr); Py_CLEAR(sgr_reset); free(buf);
|
||||
Py_CLEAR(nl); Py_CLEAR(cr); Py_CLEAR(sgr_reset);
|
||||
if (PyErr_Occurred()) return NULL;
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
|
||||
// Boilerplate {{{
|
||||
static PyObject*
|
||||
copy_char(Line* self, PyObject *args);
|
||||
#define copy_char_doc "copy_char(src, to, dest) -> Copy the character at src to to the character dest in the line `to`"
|
||||
|
||||
#define hyperlink_ids_doc "hyperlink_ids() -> Tuple of hyper link ids at every cell"
|
||||
static PyObject*
|
||||
hyperlink_ids(Line *self, PyObject *args UNUSED) {
|
||||
PyObject *ans = PyTuple_New(self->xnum);
|
||||
for (index_type x = 0; x < self->xnum; x++) {
|
||||
PyTuple_SET_ITEM(ans, x, PyLong_FromUnsignedLong(self->cpu_cells[x].hyperlink_id));
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
|
||||
|
||||
static PyObject *
|
||||
richcmp(PyObject *obj1, PyObject *obj2, int op);
|
||||
|
||||
@@ -844,6 +891,7 @@ static PyMethodDef methods[] = {
|
||||
METHOD(set_attribute, METH_VARARGS)
|
||||
METHOD(as_ansi, METH_NOARGS)
|
||||
METHOD(is_continued, METH_NOARGS)
|
||||
METHOD(hyperlink_ids, METH_NOARGS)
|
||||
METHOD(width, METH_O)
|
||||
METHOD(url_start_at, METH_O)
|
||||
METHOD(url_end_at, METH_VARARGS)
|
||||
|
||||
@@ -28,7 +28,7 @@ static inline void
|
||||
clear_chars_in_line(CPUCell *cpu_cells, GPUCell *gpu_cells, index_type xnum, char_type ch) {
|
||||
// Clear only the char part of each cell, the rest must have been cleared by a memset or similar
|
||||
if (ch) {
|
||||
for (index_type i = 0; i < xnum; i++) { cpu_cells[i].ch = ch; gpu_cells[i].attrs = 1; }
|
||||
for (index_type i = 0; i < xnum; i++) { cpu_cells[i].ch = ch; cpu_cells[i].hyperlink_id = 0; gpu_cells[i].attrs = 1; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ left_shift_line(Line *line, index_type at, index_type num) {
|
||||
}
|
||||
if (at < line->xnum && ((line->gpu_cells[at].attrs) & WIDTH_MASK) != 1) {
|
||||
line->cpu_cells[at].ch = BLANK_CHAR;
|
||||
line->cpu_cells[at].hyperlink_id = 0;
|
||||
line->gpu_cells[at].attrs = BLANK_CHAR ? 1 : 0;
|
||||
clear_sprite_position(line->gpu_cells[at]);
|
||||
}
|
||||
@@ -70,13 +71,13 @@ typedef Line*(get_line_func)(void *, int);
|
||||
void line_clear_text(Line *self, unsigned int at, unsigned int num, char_type ch);
|
||||
void line_apply_cursor(Line *self, Cursor *cursor, unsigned int at, unsigned int num, bool clear_char);
|
||||
char_type line_get_char(Line *self, index_type at);
|
||||
void line_set_char(Line *, unsigned int , uint32_t , unsigned int , Cursor *, bool);
|
||||
void line_set_char(Line *, unsigned int , uint32_t , unsigned int , Cursor *, hyperlink_id_type);
|
||||
void line_right_shift(Line *, unsigned int , unsigned int );
|
||||
void line_add_combining_char(Line *, uint32_t , unsigned int );
|
||||
index_type line_url_start_at(Line *self, index_type x);
|
||||
index_type line_url_end_at(Line *self, index_type x, bool, char_type, bool);
|
||||
bool line_startswith_url_chars(Line*);
|
||||
index_type line_as_ansi(Line *self, Py_UCS4 *buf, index_type buflen, bool*, const GPUCell**) __attribute__((nonnull));
|
||||
void line_as_ansi(Line *self, ANSIBuf *output, const GPUCell**) __attribute__((nonnull));
|
||||
unsigned int line_length(Line *self);
|
||||
size_t cell_as_unicode(CPUCell *cell, bool include_cc, Py_UCS4 *buf, char_type);
|
||||
size_t cell_as_unicode_for_fallback(CPUCell *cell, Py_UCS4 *buf);
|
||||
@@ -93,19 +94,20 @@ void linebuf_clear_line(LineBuf *self, index_type y);
|
||||
void linebuf_insert_lines(LineBuf *self, unsigned int num, unsigned int y, unsigned int bottom);
|
||||
void linebuf_delete_lines(LineBuf *self, index_type num, index_type y, index_type bottom);
|
||||
void linebuf_set_attribute(LineBuf *, unsigned int , unsigned int );
|
||||
void linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *, index_type *, HistoryBuf *, index_type *, index_type *);
|
||||
void linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *, index_type *, HistoryBuf *, index_type *, index_type *, ANSIBuf*);
|
||||
void linebuf_mark_line_dirty(LineBuf *self, index_type y);
|
||||
void linebuf_mark_line_clean(LineBuf *self, index_type y);
|
||||
void linebuf_mark_line_as_not_continued(LineBuf *self, index_type y);
|
||||
unsigned int linebuf_char_width_at(LineBuf *self, index_type x, index_type y);
|
||||
void linebuf_refresh_sprite_positions(LineBuf *self);
|
||||
void historybuf_add_line(HistoryBuf *self, const Line *line);
|
||||
void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other);
|
||||
void historybuf_add_line(HistoryBuf *self, const Line *line, ANSIBuf*);
|
||||
void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other, ANSIBuf*);
|
||||
void historybuf_init_line(HistoryBuf *self, index_type num, Line *l);
|
||||
CPUCell* historybuf_cpu_cells(HistoryBuf *self, index_type num);
|
||||
void historybuf_mark_line_clean(HistoryBuf *self, index_type y);
|
||||
void historybuf_mark_line_dirty(HistoryBuf *self, index_type y);
|
||||
void historybuf_refresh_sprite_positions(HistoryBuf *self);
|
||||
void historybuf_clear(HistoryBuf *self);
|
||||
void mark_text_in_line(PyObject *marker, Line *line);
|
||||
bool line_has_mark(Line *, attrs_type mark);
|
||||
PyObject* as_text_generic(PyObject *args, void *container, get_line_func get_line, index_type lines, index_type columns);
|
||||
PyObject* as_text_generic(PyObject *args, void *container, get_line_func get_line, index_type lines, ANSIBuf *ansibuf);
|
||||
|
||||
@@ -15,7 +15,7 @@ from .child import set_default_env
|
||||
from .cli import create_opts, parse_args
|
||||
from .cli_stub import CLIOptions
|
||||
from .conf.utils import BadLine
|
||||
from .config import cached_values_for, initial_window_size_func
|
||||
from .config import cached_values_for
|
||||
from .constants import (
|
||||
appname, beam_cursor_data_file, config_dir, glfw_path, is_macos,
|
||||
is_wayland, kitty_exe, logo_data_file, running_in_kitty
|
||||
@@ -28,6 +28,8 @@ from .fast_data_types import (
|
||||
from .fonts.box_drawing import set_scale
|
||||
from .fonts.render import set_font_family
|
||||
from .options_stub import Options as OptionsStub
|
||||
from .os_window_size import initial_window_size_func
|
||||
from .session import get_os_window_sizing_data
|
||||
from .utils import (
|
||||
detach, expandvars, log_error, read_shell_environment, single_instance,
|
||||
startup_notification_handler, unix_socket_paths
|
||||
@@ -129,7 +131,7 @@ def _run_app(opts: OptionsStub, args: CLIOptions, bad_lines: Sequence[BadLine] =
|
||||
with cached_values_for(run_app.cached_values_name) as cached_values:
|
||||
with startup_notification_handler(extra_callback=run_app.first_window_callback) as pre_show_callback:
|
||||
window_id = create_os_window(
|
||||
run_app.initial_window_size_func(opts, cached_values),
|
||||
run_app.initial_window_size_func(get_os_window_sizing_data(opts), cached_values),
|
||||
pre_show_callback,
|
||||
args.title or appname, args.name or args.cls or appname,
|
||||
args.cls or appname, load_all_shaders)
|
||||
|
||||
@@ -202,14 +202,15 @@ update_drag(bool from_button, Window *w, bool is_release, int modifiers) {
|
||||
if (is_release) {
|
||||
global_state.active_drag_in_window = 0;
|
||||
w->last_drag_scroll_at = 0;
|
||||
if (screen->selection.in_progress)
|
||||
if (screen->selections.in_progress) {
|
||||
screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, true, false);
|
||||
}
|
||||
}
|
||||
else {
|
||||
global_state.active_drag_in_window = w->id;
|
||||
screen_start_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, modifiers == (int)OPT(rectangle_select_modifiers) || modifiers == ((int)OPT(rectangle_select_modifiers) | (int)OPT(terminal_select_modifiers)), EXTEND_CELL);
|
||||
}
|
||||
} else if (screen->selection.in_progress) {
|
||||
} else if (screen->selections.in_progress) {
|
||||
screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, false, false);
|
||||
}
|
||||
}
|
||||
@@ -302,6 +303,11 @@ detect_url(Screen *screen, unsigned int x, unsigned int y) {
|
||||
bool has_url = false;
|
||||
index_type url_start, url_end = 0;
|
||||
Line *line = screen_visual_line(screen, y);
|
||||
if (line->cpu_cells[x].hyperlink_id) {
|
||||
mouse_cursor_shape = HAND;
|
||||
screen_mark_hyperlink(screen, x, y);
|
||||
return;
|
||||
}
|
||||
char_type sentinel;
|
||||
if (line) {
|
||||
url_start = line_url_start_at(line, x);
|
||||
@@ -331,7 +337,7 @@ detect_url(Screen *screen, unsigned int x, unsigned int y) {
|
||||
static inline void
|
||||
handle_mouse_movement_in_kitty(Window *w, int button, bool mouse_cell_changed) {
|
||||
Screen *screen = w->render_data.screen;
|
||||
if (screen->selection.in_progress && (button == GLFW_MOUSE_BUTTON_LEFT || button == GLFW_MOUSE_BUTTON_RIGHT)) {
|
||||
if (screen->selections.in_progress && (button == GLFW_MOUSE_BUTTON_LEFT || button == GLFW_MOUSE_BUTTON_RIGHT)) {
|
||||
monotonic_t now = monotonic();
|
||||
if ((now - w->last_drag_scroll_at) >= ms_to_monotonic_t(20ll) || mouse_cell_changed) {
|
||||
update_drag(false, w, false, 0);
|
||||
@@ -685,9 +691,8 @@ scroll_event(double UNUSED xoffset, double yoffset, int flags, int modifiers) {
|
||||
s = (int)round(pixels) / (int)global_state.callback_os_window->fonts_data->cell_height;
|
||||
screen->pending_scroll_pixels = pixels - s * (int) global_state.callback_os_window->fonts_data->cell_height;
|
||||
} else {
|
||||
if (screen->linebuf == screen->main_linebuf || !screen->modes.mouse_tracking_mode) {
|
||||
// Only use wheel_scroll_multiplier if we are scrolling kitty scrollback or in mouse
|
||||
// tracking mode, where the application is responsible for interpreting scroll events
|
||||
if (!screen->modes.mouse_tracking_mode) {
|
||||
// Dont use multiplier if we are sending events to the application
|
||||
yoffset *= OPT(wheel_scroll_multiplier);
|
||||
} else if (OPT(wheel_scroll_multiplier) < 0) {
|
||||
// ensure that changing scroll direction still works, even though
|
||||
@@ -702,20 +707,17 @@ scroll_event(double UNUSED xoffset, double yoffset, int flags, int modifiers) {
|
||||
}
|
||||
if (s == 0) return;
|
||||
bool upwards = s > 0;
|
||||
if (screen->linebuf == screen->main_linebuf) {
|
||||
screen_history_scroll(screen, abs(s), upwards);
|
||||
} else {
|
||||
if (screen->modes.mouse_tracking_mode) {
|
||||
int sz = encode_mouse_scroll(w, upwards, modifiers);
|
||||
if (sz > 0) {
|
||||
mouse_event_buf[sz] = 0;
|
||||
for (s = abs(s); s > 0; s--) {
|
||||
write_escape_code_to_child(screen, CSI, mouse_event_buf);
|
||||
}
|
||||
if (screen->modes.mouse_tracking_mode) {
|
||||
int sz = encode_mouse_scroll(w, upwards, modifiers);
|
||||
if (sz > 0) {
|
||||
mouse_event_buf[sz] = 0;
|
||||
for (s = abs(s); s > 0; s--) {
|
||||
write_escape_code_to_child(screen, CSI, mouse_event_buf);
|
||||
}
|
||||
} else {
|
||||
fake_scroll(w, abs(s), upwards);
|
||||
}
|
||||
} else {
|
||||
if (screen->linebuf == screen->main_linebuf) screen_history_scroll(screen, abs(s), upwards);
|
||||
else fake_scroll(w, abs(s), upwards);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
160
kitty/notify.py
160
kitty/notify.py
@@ -2,9 +2,14 @@
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from typing import Dict, Optional
|
||||
from base64 import standard_b64decode
|
||||
from collections import OrderedDict
|
||||
from itertools import count
|
||||
from typing import Dict, Optional, Callable
|
||||
|
||||
from .constants import is_macos, logo_png_file
|
||||
from .fast_data_types import get_boss
|
||||
from .utils import log_error
|
||||
|
||||
if is_macos:
|
||||
from .fast_data_types import cocoa_send_notification
|
||||
@@ -15,9 +20,10 @@ if is_macos:
|
||||
timeout: int = 5000,
|
||||
application: str = 'kitty',
|
||||
icon: bool = True,
|
||||
identifier: Optional[str] = None
|
||||
identifier: Optional[str] = None,
|
||||
subtitle: Optional[str] = None,
|
||||
) -> None:
|
||||
cocoa_send_notification(identifier, title, body, None)
|
||||
cocoa_send_notification(identifier, title, body, subtitle)
|
||||
|
||||
else:
|
||||
|
||||
@@ -35,7 +41,6 @@ else:
|
||||
rmap = {v: k for k, v in identifier_map.items()}
|
||||
identifier = rmap.get(notification_id)
|
||||
if identifier is not None:
|
||||
from .boss import notification_activated
|
||||
notification_activated(identifier)
|
||||
|
||||
def notify(
|
||||
@@ -44,7 +49,8 @@ else:
|
||||
timeout: int = -1,
|
||||
application: str = 'kitty',
|
||||
icon: bool = True,
|
||||
identifier: Optional[str] = None
|
||||
identifier: Optional[str] = None,
|
||||
subtitle: Optional[str] = None,
|
||||
) -> None:
|
||||
icf = ''
|
||||
if icon is True:
|
||||
@@ -52,3 +58,147 @@ else:
|
||||
alloc_id = dbus_send_notification(application, icf, title, body, 'Click to see changes', timeout)
|
||||
if alloc_id and identifier is not None:
|
||||
alloc_map[alloc_id] = identifier
|
||||
|
||||
|
||||
class NotificationCommand:
|
||||
|
||||
done: bool = True
|
||||
identifier: str = '0'
|
||||
title: str = ''
|
||||
body: str = ''
|
||||
actions: str = ''
|
||||
|
||||
|
||||
def parse_osc_9(raw: str) -> NotificationCommand:
|
||||
ans = NotificationCommand()
|
||||
ans.title = raw
|
||||
return ans
|
||||
|
||||
|
||||
def parse_osc_99(raw: str) -> NotificationCommand:
|
||||
cmd = NotificationCommand()
|
||||
metadata, payload = raw.partition(';')[::2]
|
||||
payload_is_encoded = False
|
||||
payload_type = 'title'
|
||||
if metadata:
|
||||
for part in metadata.split(':'):
|
||||
try:
|
||||
k, v = part.split('=', 1)
|
||||
except Exception:
|
||||
log_error('Malformed OSC 99: metadata is not key=value pairs')
|
||||
return cmd
|
||||
if k == 'p':
|
||||
payload_type = v
|
||||
elif k == 'i':
|
||||
cmd.identifier = v
|
||||
elif k == 'e':
|
||||
payload_is_encoded = v == '1'
|
||||
elif k == 'd':
|
||||
cmd.done = v != '0'
|
||||
elif k == 'a':
|
||||
cmd.actions += ',' + v
|
||||
if payload_type not in ('body', 'title'):
|
||||
log_error(f'Malformed OSC 99: unknown payload type: {payload_type}')
|
||||
return NotificationCommand()
|
||||
if payload_is_encoded:
|
||||
try:
|
||||
payload = standard_b64decode(payload).decode('utf-8')
|
||||
except Exception:
|
||||
log_error('Malformed OSC 99: payload is not base64 encoded UTF-8 text')
|
||||
return NotificationCommand()
|
||||
if payload_type == 'title':
|
||||
cmd.title = payload
|
||||
else:
|
||||
cmd.body = payload
|
||||
return cmd
|
||||
|
||||
|
||||
def limit_size(x: str) -> str:
|
||||
if len(x) > 1024:
|
||||
x = x[:1024]
|
||||
return x
|
||||
|
||||
|
||||
def merge_osc_99(prev: NotificationCommand, cmd: NotificationCommand) -> NotificationCommand:
|
||||
if prev.done or prev.identifier != cmd.identifier:
|
||||
return cmd
|
||||
cmd.actions = limit_size(prev.actions + ',' + cmd.actions)
|
||||
cmd.title = limit_size(prev.title + cmd.title)
|
||||
cmd.body = limit_size(prev.body + cmd.body)
|
||||
return cmd
|
||||
|
||||
|
||||
identifier_registry: "OrderedDict[str, RegisteredNotification]" = OrderedDict()
|
||||
id_counter = count()
|
||||
|
||||
|
||||
class RegisteredNotification:
|
||||
identifier: str
|
||||
window_id: int
|
||||
focus: bool = True
|
||||
report: bool = False
|
||||
|
||||
def __init__(self, cmd: NotificationCommand, window_id: int):
|
||||
self.window_id = window_id
|
||||
for x in cmd.actions.strip(',').split(','):
|
||||
val = not x.startswith('-')
|
||||
x = x.lstrip('+-')
|
||||
if x == 'focus':
|
||||
self.focus = val
|
||||
elif x == 'report':
|
||||
self.report = val
|
||||
self.identifier = cmd.identifier
|
||||
|
||||
|
||||
def register_identifier(identifier: str, cmd: NotificationCommand, window_id: int) -> None:
|
||||
identifier_registry[identifier] = RegisteredNotification(cmd, window_id)
|
||||
if len(identifier_registry) > 100:
|
||||
identifier_registry.popitem(False)
|
||||
|
||||
|
||||
def notification_activated(identifier: str, activated_implementation: Optional[Callable] = None) -> None:
|
||||
if identifier == 'new-version':
|
||||
from .update_check import notification_activated as do
|
||||
do()
|
||||
elif identifier.startswith('test-notify-'):
|
||||
log_error(f'Test notification {identifier} activated')
|
||||
else:
|
||||
r = identifier_registry.pop(identifier, None)
|
||||
if r is not None and (r.focus or r.report):
|
||||
if activated_implementation is None:
|
||||
get_boss().notification_activated(r.identifier, r.window_id, r.focus, r.report)
|
||||
else:
|
||||
activated_implementation(r.identifier, r.window_id, r.focus, r.report)
|
||||
|
||||
|
||||
def reset_registry() -> None:
|
||||
global id_counter
|
||||
identifier_registry.clear()
|
||||
id_counter = count()
|
||||
|
||||
|
||||
def notify_with_command(cmd: NotificationCommand, window_id: int, notify: Callable = notify) -> None:
|
||||
title = cmd.title or cmd.body
|
||||
body = cmd.body if cmd.title else ''
|
||||
if title:
|
||||
identifier = 'i' + str(next(id_counter))
|
||||
notify(title, body, identifier=identifier)
|
||||
register_identifier(identifier, cmd, window_id)
|
||||
|
||||
|
||||
def handle_notification_cmd(
|
||||
osc_code: int,
|
||||
raw_data: str,
|
||||
window_id: int,
|
||||
prev_cmd: NotificationCommand,
|
||||
notify_implementation: Callable = notify
|
||||
) -> Optional[NotificationCommand]:
|
||||
if osc_code == 99:
|
||||
cmd = merge_osc_99(prev_cmd, parse_osc_99(raw_data))
|
||||
if cmd.done:
|
||||
notify_with_command(cmd, window_id, notify_implementation)
|
||||
cmd = NotificationCommand()
|
||||
return cmd
|
||||
if osc_code == 9:
|
||||
cmd = parse_osc_9(raw_data)
|
||||
notify_with_command(cmd, window_id, notify_implementation)
|
||||
|
||||
190
kitty/open_actions.py
Normal file
190
kitty/open_actions.py
Normal file
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
import os
|
||||
import posixpath
|
||||
from contextlib import suppress
|
||||
from functools import lru_cache
|
||||
from typing import (
|
||||
Any, Generator, Iterable, List, NamedTuple, Optional, Tuple, cast
|
||||
)
|
||||
from urllib.parse import ParseResult, unquote, urlparse
|
||||
|
||||
from .conf.utils import to_cmdline
|
||||
from .config import KeyAction, parse_key_action
|
||||
from .constants import config_dir
|
||||
from .typing import MatchType
|
||||
from .utils import expandvars, log_error
|
||||
|
||||
|
||||
class MatchCriteria(NamedTuple):
|
||||
type: MatchType
|
||||
value: str
|
||||
|
||||
|
||||
class OpenAction(NamedTuple):
|
||||
match_criteria: Tuple[MatchCriteria, ...]
|
||||
actions: Tuple[KeyAction, ...]
|
||||
|
||||
|
||||
def parse(lines: Iterable[str]) -> Generator[OpenAction, None, None]:
|
||||
match_criteria: List[MatchCriteria] = []
|
||||
actions: List[KeyAction] = []
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line.startswith('#'):
|
||||
continue
|
||||
if not line:
|
||||
if match_criteria and actions:
|
||||
yield OpenAction(tuple(match_criteria), tuple(actions))
|
||||
match_criteria = []
|
||||
actions = []
|
||||
continue
|
||||
parts = line.split(maxsplit=1)
|
||||
if len(parts) != 2:
|
||||
continue
|
||||
key, rest = parts
|
||||
key = key.lower()
|
||||
if key == 'action':
|
||||
with to_cmdline.filter_env_vars('URL', 'FILE_PATH', 'FILE', 'FRAGMENT'):
|
||||
x = parse_key_action(rest)
|
||||
if x is not None:
|
||||
actions.append(x)
|
||||
elif key in ('mime', 'ext', 'protocol', 'file', 'path', 'url', 'fragment_matches'):
|
||||
if key != 'url':
|
||||
rest = rest.lower()
|
||||
match_criteria.append(MatchCriteria(cast(MatchType, key), rest))
|
||||
else:
|
||||
log_error(f'Ignoring malformed open actions line: {line}')
|
||||
|
||||
if match_criteria and actions:
|
||||
yield OpenAction(tuple(match_criteria), tuple(actions))
|
||||
|
||||
|
||||
def url_matches_criterion(purl: 'ParseResult', url: str, unquoted_path: str, mc: MatchCriteria) -> bool:
|
||||
if mc.type == 'url':
|
||||
import re
|
||||
try:
|
||||
pat = re.compile(mc.value)
|
||||
except re.error:
|
||||
return False
|
||||
return pat.search(unquote(url)) is not None
|
||||
|
||||
if mc.type == 'mime':
|
||||
import fnmatch
|
||||
from mimetypes import guess_type
|
||||
try:
|
||||
mt = guess_type(unquoted_path)[0]
|
||||
except Exception:
|
||||
return False
|
||||
if mt is None:
|
||||
return False
|
||||
mt = mt.lower()
|
||||
for mpat in mc.value.split(','):
|
||||
mpat = mpat.strip()
|
||||
with suppress(Exception):
|
||||
if fnmatch.fnmatchcase(mt, mpat):
|
||||
return True
|
||||
return False
|
||||
|
||||
if mc.type == 'ext':
|
||||
if not purl.path:
|
||||
return False
|
||||
path = unquoted_path.lower()
|
||||
for ext in mc.value.split(','):
|
||||
ext = ext.strip()
|
||||
if path.endswith('.' + ext):
|
||||
return True
|
||||
return False
|
||||
|
||||
if mc.type == 'protocol':
|
||||
protocol = (purl.scheme or 'file').lower()
|
||||
for key in mc.value.split(','):
|
||||
if key.strip() == protocol:
|
||||
return True
|
||||
return False
|
||||
|
||||
if mc.type == 'fragment_matches':
|
||||
import re
|
||||
try:
|
||||
pat = re.compile(mc.value)
|
||||
except re.error:
|
||||
return False
|
||||
|
||||
return pat.search(unquote(purl.fragment)) is not None
|
||||
|
||||
if mc.type == 'path':
|
||||
import fnmatch
|
||||
try:
|
||||
return fnmatch.fnmatchcase(unquoted_path.lower(), mc.value)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
if mc.type == 'file':
|
||||
import fnmatch
|
||||
import posixpath
|
||||
try:
|
||||
fname = posixpath.basename(unquoted_path)
|
||||
except Exception:
|
||||
return False
|
||||
try:
|
||||
return fnmatch.fnmatchcase(fname.lower(), mc.value)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def url_matches_criteria(purl: 'ParseResult', url: str, unquoted_path: str, criteria: Iterable[MatchCriteria]) -> bool:
|
||||
for x in criteria:
|
||||
try:
|
||||
if not url_matches_criterion(purl, url, unquoted_path, x):
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def actions_for_url_from_list(url: str, actions: Iterable[OpenAction]) -> Generator[KeyAction, None, None]:
|
||||
try:
|
||||
purl = urlparse(url)
|
||||
except Exception:
|
||||
return
|
||||
path = unquote(purl.path)
|
||||
|
||||
env = {
|
||||
'URL': url,
|
||||
'FILE_PATH': path,
|
||||
'FILE': posixpath.basename(path),
|
||||
'FRAGMENT': unquote(purl.fragment)
|
||||
}
|
||||
|
||||
def expand(x: Any) -> Any:
|
||||
if isinstance(x, str):
|
||||
return expandvars(x, env, fallback_to_os_env=False)
|
||||
return x
|
||||
|
||||
for action in actions:
|
||||
if url_matches_criteria(purl, url, path, action.match_criteria):
|
||||
for ac in action.actions:
|
||||
yield ac._replace(args=tuple(map(expand, ac.args)))
|
||||
return
|
||||
|
||||
|
||||
@lru_cache(maxsize=2)
|
||||
def load_open_actions() -> Tuple[OpenAction, ...]:
|
||||
try:
|
||||
f = open(os.path.join(config_dir, 'open-actions.conf'))
|
||||
except FileNotFoundError:
|
||||
return ()
|
||||
with f:
|
||||
return tuple(parse(f))
|
||||
|
||||
|
||||
def actions_for_url(url: str, actions_spec: Optional[str] = None) -> Generator[KeyAction, None, None]:
|
||||
if actions_spec is None:
|
||||
actions = load_open_actions()
|
||||
else:
|
||||
actions = tuple(parse(actions_spec.splitlines()))
|
||||
yield from actions_for_url_from_list(url, actions)
|
||||
74
kitty/os_window_size.py
Normal file
74
kitty/os_window_size.py
Normal file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from typing import Any, Callable, Dict, NamedTuple, Tuple
|
||||
|
||||
from .constants import FloatEdges, is_macos
|
||||
from .typing import EdgeLiteral
|
||||
from .utils import log_error
|
||||
|
||||
|
||||
class WindowSize(NamedTuple):
|
||||
|
||||
size: int
|
||||
unit: str
|
||||
|
||||
|
||||
class WindowSizes(NamedTuple):
|
||||
|
||||
width: WindowSize
|
||||
height: WindowSize
|
||||
|
||||
|
||||
class WindowSizeData(NamedTuple):
|
||||
initial_window_sizes: WindowSizes
|
||||
remember_window_size: bool
|
||||
single_window_margin_width: FloatEdges
|
||||
window_margin_width: FloatEdges
|
||||
window_padding_width: FloatEdges
|
||||
|
||||
|
||||
def initial_window_size_func(opts: WindowSizeData, cached_values: Dict) -> Callable[[int, int, float, float, float, float], Tuple[int, int]]:
|
||||
|
||||
if 'window-size' in cached_values and opts.remember_window_size:
|
||||
ws = cached_values['window-size']
|
||||
try:
|
||||
w, h = map(int, ws)
|
||||
|
||||
def initial_window_size(*a: Any) -> Tuple[int, int]:
|
||||
return w, h
|
||||
return initial_window_size
|
||||
except Exception:
|
||||
log_error('Invalid cached window size, ignoring')
|
||||
|
||||
w, w_unit = opts.initial_window_sizes.width
|
||||
h, h_unit = opts.initial_window_sizes.height
|
||||
|
||||
def get_window_size(cell_width: int, cell_height: int, dpi_x: float, dpi_y: float, xscale: float, yscale: float) -> Tuple[int, int]:
|
||||
if not is_macos:
|
||||
# scaling is not needed on Wayland, but is needed on macOS. Not
|
||||
# sure about X11.
|
||||
xscale = yscale = 1
|
||||
|
||||
def effective_margin(which: EdgeLiteral) -> float:
|
||||
ans: float = getattr(opts.single_window_margin_width, which)
|
||||
if ans < 0:
|
||||
ans = getattr(opts.window_margin_width, which)
|
||||
return ans
|
||||
|
||||
if w_unit == 'cells':
|
||||
spacing = effective_margin('left') + effective_margin('right')
|
||||
spacing += opts.window_padding_width.left + opts.window_padding_width.right
|
||||
width = cell_width * w / xscale + (dpi_x / 72) * spacing + 1
|
||||
else:
|
||||
width = w
|
||||
if h_unit == 'cells':
|
||||
spacing = effective_margin('top') + effective_margin('bottom')
|
||||
spacing += opts.window_padding_width.top + opts.window_padding_width.bottom
|
||||
height = cell_height * h / yscale + (dpi_y / 72) * spacing + 1
|
||||
else:
|
||||
height = h
|
||||
return int(width), int(height)
|
||||
|
||||
return get_window_size
|
||||
161
kitty/parser.c
161
kitty/parser.c
@@ -4,6 +4,9 @@
|
||||
* Distributed under terms of the GPL3 license.
|
||||
*/
|
||||
|
||||
// Need _POSIX_C_SOURCE for strtok_r
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include "data-types.h"
|
||||
#include "control-codes.h"
|
||||
#include "screen.h"
|
||||
@@ -121,6 +124,9 @@ _report_params(PyObject *dump_callback, const char *name, unsigned int *params,
|
||||
#define REPORT_OSC2(name, code, string) \
|
||||
Py_XDECREF(PyObject_CallFunction(dump_callback, "sIO", #name, code, string)); PyErr_Clear();
|
||||
|
||||
#define REPORT_HYPERLINK(id, url) \
|
||||
Py_XDECREF(PyObject_CallFunction(dump_callback, "szz", "set_active_hyperlink", id, url)); PyErr_Clear();
|
||||
|
||||
#else
|
||||
|
||||
#define DUMP_UNUSED UNUSED
|
||||
@@ -134,6 +140,7 @@ _report_params(PyObject *dump_callback, const char *name, unsigned int *params,
|
||||
#define FLUSH_DRAW
|
||||
#define REPORT_OSC(name, string)
|
||||
#define REPORT_OSC2(name, code, string)
|
||||
#define REPORT_HYPERLINK(id, url)
|
||||
|
||||
#endif
|
||||
|
||||
@@ -301,10 +308,58 @@ handle_esc_mode_char(Screen *screen, uint32_t ch, PyObject DUMP_UNUSED *dump_cal
|
||||
} // }}}
|
||||
|
||||
// OSC mode {{{
|
||||
|
||||
static inline bool
|
||||
parse_osc_8(char *buf, char **id, char **url) {
|
||||
char *boundary = strstr(buf, ";");
|
||||
if (boundary == NULL) return false;
|
||||
*boundary = 0;
|
||||
if (*(boundary + 1)) *url = boundary + 1;
|
||||
char *save, *token = strtok_r(buf, ":", &save);
|
||||
while (token != NULL) {
|
||||
size_t len = strlen(token);
|
||||
if (len > 3 && token[0] == 'i' && token[1] == 'd' && token[2] == '=' && token[3]) {
|
||||
*id = token + 3;
|
||||
break;
|
||||
}
|
||||
token = strtok_r(NULL, ":", &save);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline void
|
||||
dispatch_hyperlink(Screen *screen, size_t pos, size_t size, PyObject DUMP_UNUSED *dump_callback) {
|
||||
// since the spec says only ASCII printable chars are allowed in OSC 8, we
|
||||
// can just convert to char* directly
|
||||
if (!size) return; // ignore empty OSC 8 since it must have two semi-colons to be valid, which means one semi-colon here
|
||||
char *id = NULL, *url = NULL;
|
||||
char *data = malloc(size + 1);
|
||||
if (!data) fatal("Out of memory");
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
data[i] = screen->parser_buf[i + pos] & 0x7f;
|
||||
if (data[i] < 32 || data[i] > 126) data[i] = '_';
|
||||
}
|
||||
data[size] = 0;
|
||||
|
||||
if (parse_osc_8(data, &id, &url)) {
|
||||
REPORT_HYPERLINK(id, url);
|
||||
set_active_hyperlink(screen, id, url);
|
||||
} else {
|
||||
REPORT_ERROR("Ignoring malformed OSC 8 code");
|
||||
}
|
||||
|
||||
free(data);
|
||||
}
|
||||
|
||||
static inline void
|
||||
dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
|
||||
#define DISPATCH_OSC_WITH_CODE(name) REPORT_OSC2(name, code, string); name(screen, code, string);
|
||||
#define DISPATCH_OSC(name) REPORT_OSC(name, string); name(screen, string);
|
||||
#define SET_COLOR(name) REPORT_OSC2(name, code, string); name(screen, code, string);
|
||||
#define START_DISPATCH {\
|
||||
PyObject *string = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, screen->parser_buf + i, limit - i); \
|
||||
if (string) {
|
||||
#define END_DISPATCH Py_CLEAR(string); } PyErr_Clear(); break; }
|
||||
|
||||
const unsigned int limit = screen->parser_buf_pos;
|
||||
unsigned int code=0, i;
|
||||
for (i = 0; i < MIN(limit, 5u); i++) {
|
||||
@@ -314,54 +369,66 @@ dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
|
||||
code = utoi(screen->parser_buf, i);
|
||||
if (i < limit && screen->parser_buf[i] == ';') i++;
|
||||
}
|
||||
PyObject *string = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, screen->parser_buf + i, limit - i);
|
||||
if (string != NULL) {
|
||||
switch(code) {
|
||||
case 0:
|
||||
DISPATCH_OSC(set_title);
|
||||
DISPATCH_OSC(set_icon);
|
||||
break;
|
||||
case 1:
|
||||
DISPATCH_OSC(set_icon);
|
||||
break;
|
||||
case 2:
|
||||
DISPATCH_OSC(set_title);
|
||||
break;
|
||||
case 4:
|
||||
case 104:
|
||||
SET_COLOR(set_color_table_color);
|
||||
break;
|
||||
case 10:
|
||||
case 11:
|
||||
case 12:
|
||||
case 17:
|
||||
case 19:
|
||||
case 110:
|
||||
case 111:
|
||||
case 112:
|
||||
case 117:
|
||||
case 119:
|
||||
SET_COLOR(set_dynamic_color);
|
||||
break;
|
||||
case 52:
|
||||
DISPATCH_OSC(clipboard_control);
|
||||
break;
|
||||
case 30001:
|
||||
REPORT_COMMAND(screen_push_dynamic_colors);
|
||||
screen_push_dynamic_colors(screen);
|
||||
break;
|
||||
case 30101:
|
||||
REPORT_COMMAND(screen_pop_dynamic_colors);
|
||||
screen_pop_dynamic_colors(screen);
|
||||
break;
|
||||
default:
|
||||
REPORT_ERROR("Unknown OSC code: %u", code);
|
||||
break;
|
||||
}
|
||||
Py_CLEAR(string);
|
||||
switch(code) {
|
||||
case 0:
|
||||
START_DISPATCH
|
||||
DISPATCH_OSC(set_title);
|
||||
DISPATCH_OSC(set_icon);
|
||||
END_DISPATCH
|
||||
case 1:
|
||||
START_DISPATCH
|
||||
DISPATCH_OSC(set_icon);
|
||||
END_DISPATCH
|
||||
case 2:
|
||||
START_DISPATCH
|
||||
DISPATCH_OSC(set_title);
|
||||
END_DISPATCH
|
||||
case 4:
|
||||
case 104:
|
||||
START_DISPATCH
|
||||
DISPATCH_OSC_WITH_CODE(set_color_table_color);
|
||||
END_DISPATCH
|
||||
case 8:
|
||||
dispatch_hyperlink(screen, i, limit-i, dump_callback);
|
||||
break;
|
||||
case 9:
|
||||
case 99:
|
||||
START_DISPATCH
|
||||
DISPATCH_OSC_WITH_CODE(desktop_notify)
|
||||
END_DISPATCH
|
||||
case 10:
|
||||
case 11:
|
||||
case 12:
|
||||
case 17:
|
||||
case 19:
|
||||
case 110:
|
||||
case 111:
|
||||
case 112:
|
||||
case 117:
|
||||
case 119:
|
||||
START_DISPATCH
|
||||
DISPATCH_OSC_WITH_CODE(set_dynamic_color);
|
||||
END_DISPATCH
|
||||
case 52:
|
||||
START_DISPATCH
|
||||
DISPATCH_OSC(clipboard_control);
|
||||
END_DISPATCH
|
||||
case 30001:
|
||||
REPORT_COMMAND(screen_push_dynamic_colors);
|
||||
screen_push_dynamic_colors(screen);
|
||||
break;
|
||||
case 30101:
|
||||
REPORT_COMMAND(screen_pop_dynamic_colors);
|
||||
screen_pop_dynamic_colors(screen);
|
||||
break;
|
||||
default:
|
||||
REPORT_ERROR("Unknown OSC code: %u", code);
|
||||
break;
|
||||
}
|
||||
#undef DISPATCH_OSC
|
||||
#undef SET_COLOR
|
||||
#undef DISPATCH_OSC_WITH_CODE
|
||||
#undef START_DISPATCH
|
||||
#undef END_DISPATCH
|
||||
}
|
||||
// }}}
|
||||
|
||||
|
||||
@@ -6,8 +6,11 @@
|
||||
*/
|
||||
|
||||
#include "png-reader.h"
|
||||
#include "state.h"
|
||||
#include <lcms2.h>
|
||||
|
||||
|
||||
static cmsHPROFILE srgb_profile = NULL;
|
||||
struct fake_file { const uint8_t *buf; size_t sz, cur; };
|
||||
|
||||
static void
|
||||
@@ -35,8 +38,8 @@ read_png_error_handler(png_structp png_ptr, png_const_charp msg) {
|
||||
}
|
||||
|
||||
static void
|
||||
read_png_warn_handler(png_structp UNUSED png_ptr, png_const_charp UNUSED msg) {
|
||||
// ignore warnings
|
||||
read_png_warn_handler(png_structp UNUSED png_ptr, png_const_charp msg) {
|
||||
if (global_state.debug_rendering) log_error("libpng WARNING: %s", msg);
|
||||
}
|
||||
|
||||
#define ABRT(code, msg) { if(d->err_handler) d->err_handler(#code, msg); goto err; }
|
||||
@@ -61,6 +64,33 @@ inflate_png_inner(png_read_data *d, const uint8_t *buf, size_t bufsz) {
|
||||
d->height = png_get_image_height(png, info);
|
||||
color_type = png_get_color_type(png, info);
|
||||
bit_depth = png_get_bit_depth(png, info);
|
||||
double image_gamma;
|
||||
int intent;
|
||||
cmsHPROFILE input_profile = NULL;
|
||||
cmsHTRANSFORM colorspace_transform = NULL;
|
||||
if (png_get_sRGB(png, info, &intent)) {
|
||||
// do nothing since we output sRGB
|
||||
} else if (png_get_gAMA(png, info, &image_gamma)) {
|
||||
if (image_gamma != 0 && fabs(image_gamma - 1.0/2.2) > 0.0001) png_set_gamma(png, 2.2, image_gamma);
|
||||
} else {
|
||||
// Look for an embedded color profile
|
||||
png_charp name;
|
||||
int compression_type;
|
||||
png_bytep profdata;
|
||||
png_uint_32 proflen;
|
||||
if (png_get_iCCP(png, info, &name, &compression_type, &profdata, &proflen) & PNG_INFO_iCCP) {
|
||||
input_profile = cmsOpenProfileFromMem(profdata, proflen);
|
||||
if (input_profile) {
|
||||
if (!srgb_profile) {
|
||||
srgb_profile = cmsCreate_sRGBProfile();
|
||||
if (!srgb_profile) ABRT(ENOMEM, "Out of memory allocating sRGB colorspace profile");
|
||||
}
|
||||
colorspace_transform = cmsCreateTransform(
|
||||
input_profile, TYPE_RGBA_8, srgb_profile, TYPE_RGBA_8, INTENT_PERCEPTUAL, 0);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure we get RGBA data out of libpng
|
||||
if (bit_depth == 16) png_set_strip_16(png);
|
||||
@@ -76,15 +106,23 @@ inflate_png_inner(png_read_data *d, const uint8_t *buf, size_t bufsz) {
|
||||
if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) png_set_gray_to_rgb(png);
|
||||
png_read_update_info(png, info);
|
||||
|
||||
int rowbytes = png_get_rowbytes(png, info);
|
||||
png_uint_32 rowbytes = png_get_rowbytes(png, info);
|
||||
d->sz = sizeof(png_byte) * rowbytes * d->height;
|
||||
d->decompressed = malloc(d->sz + 16);
|
||||
if (d->decompressed == NULL) ABRT(ENOMEM, "Out of memory allocating decompression buffer for PNG");
|
||||
d->row_pointers = malloc(d->height * sizeof(png_bytep));
|
||||
if (d->row_pointers == NULL) ABRT(ENOMEM, "Out of memory allocating row_pointers buffer for PNG");
|
||||
for (int i = 0; i < d->height; i++) d->row_pointers[i] = d->decompressed + i * rowbytes;
|
||||
for (size_t i = 0; i < (size_t)d->height; i++) d->row_pointers[i] = d->decompressed + i * rowbytes * sizeof(png_byte);
|
||||
png_read_image(png, d->row_pointers);
|
||||
|
||||
if (colorspace_transform) {
|
||||
for (int i = 0; i < d->height; i++) {
|
||||
cmsDoTransform(colorspace_transform, d->row_pointers[i], d->row_pointers[i], d->width);
|
||||
}
|
||||
cmsDeleteTransform(colorspace_transform);
|
||||
}
|
||||
if (input_profile) cmsCloseProfile(input_profile);
|
||||
|
||||
d->ok = true;
|
||||
err:
|
||||
if (png) png_destroy_read_struct(&png, info ? &info : NULL, NULL);
|
||||
@@ -119,8 +157,20 @@ static PyMethodDef module_methods[] = {
|
||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
|
||||
static void
|
||||
unload(void) {
|
||||
if (srgb_profile) cmsCloseProfile(srgb_profile);
|
||||
srgb_profile = NULL;
|
||||
}
|
||||
|
||||
bool
|
||||
init_png_reader(PyObject *module) {
|
||||
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
|
||||
if (Py_AtExit(unload) != 0) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Failed to register the PNG library at exit handler");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
linebuf_index(dest, 0, dest->ynum - 1); \
|
||||
if (historybuf != NULL) { \
|
||||
init_dest_line(dest->ynum - 1); \
|
||||
historybuf_add_line(historybuf, dest->line); \
|
||||
dest->line->has_dirty_text = true; \
|
||||
historybuf_add_line(historybuf, dest->line, as_ansi_buf); \
|
||||
}\
|
||||
linebuf_clear_line(dest, dest->ynum - 1); \
|
||||
} else dest_y++; \
|
||||
@@ -49,7 +50,7 @@ copy_range(Line *src, index_type src_at, Line* dest, index_type dest_at, index_t
|
||||
|
||||
|
||||
static void
|
||||
rewrap_inner(BufType *src, BufType *dest, const index_type src_limit, HistoryBuf UNUSED *historybuf, index_type *track_x, index_type *track_y) {
|
||||
rewrap_inner(BufType *src, BufType *dest, const index_type src_limit, HistoryBuf UNUSED *historybuf, index_type *track_x, index_type *track_y, ANSIBuf *as_ansi_buf) {
|
||||
bool src_line_is_continued = false;
|
||||
index_type src_y = 0, src_x = 0, dest_x = 0, dest_y = 0, num = 0, src_x_limit = 0;
|
||||
|
||||
|
||||
599
kitty/screen.c
599
kitty/screen.c
@@ -13,7 +13,7 @@
|
||||
#include "state.h"
|
||||
#include "fonts.h"
|
||||
#include "lineops.h"
|
||||
#include "screen.h"
|
||||
#include "hyperlink.h"
|
||||
#include <structmember.h>
|
||||
#include <limits.h>
|
||||
#include <sys/types.h>
|
||||
@@ -26,12 +26,18 @@
|
||||
#include "charsets.h"
|
||||
|
||||
static const ScreenModes empty_modes = {0, .mDECAWM=true, .mDECTCEM=true, .mDECARM=true};
|
||||
static Selection EMPTY_SELECTION = {{0}};
|
||||
|
||||
#define CSI_REP_MAX_REPETITIONS 65535u
|
||||
|
||||
// Constructor/destructor {{{
|
||||
|
||||
static inline void
|
||||
clear_selection(Selections *selections) {
|
||||
selections->in_progress = false;
|
||||
selections->extend_mode = EXTEND_CELL;
|
||||
selections->count = 0;
|
||||
}
|
||||
|
||||
static inline void
|
||||
init_tabstops(bool *tabstops, index_type count) {
|
||||
// In terminfo we specify the number of initial tabstops (it) as 8
|
||||
@@ -111,6 +117,7 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
|
||||
self->historybuf = alloc_historybuf(MAX(scrollback, lines), columns, OPT(scrollback_pager_history_size));
|
||||
self->main_grman = grman_alloc();
|
||||
self->alt_grman = grman_alloc();
|
||||
self->active_hyperlink_id = 0;
|
||||
|
||||
self->grman = self->main_grman;
|
||||
self->pending_mode.wait_time = s_double_to_monotonic_t(2.0);
|
||||
@@ -128,6 +135,9 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
|
||||
init_tabstops(self->main_tabstops, self->columns);
|
||||
init_tabstops(self->alt_tabstops, self->columns);
|
||||
if (!init_overlay_line(self, self->columns)) { Py_CLEAR(self); return NULL; }
|
||||
self->hyperlink_pool = alloc_hyperlink_pool();
|
||||
if (!self->hyperlink_pool) { Py_CLEAR(self); return PyErr_NoMemory(); }
|
||||
self->as_ansi_buf.hyperlink_pool = self->hyperlink_pool;
|
||||
}
|
||||
return (PyObject*) self;
|
||||
}
|
||||
@@ -141,8 +151,10 @@ screen_reset(Screen *self) {
|
||||
if (self->overlay_line.is_active) deactivate_overlay_line(self);
|
||||
linebuf_clear(self->linebuf, BLANK_CHAR);
|
||||
historybuf_clear(self->historybuf);
|
||||
clear_hyperlink_pool(self->hyperlink_pool);
|
||||
grman_clear(self->grman, false, self->cell_size);
|
||||
self->modes = empty_modes;
|
||||
self->active_hyperlink_id = 0;
|
||||
#define R(name) self->color_profile->overridden.name = 0
|
||||
R(default_fg); R(default_bg); R(cursor_color); R(highlight_fg); R(highlight_bg);
|
||||
#undef R
|
||||
@@ -153,7 +165,8 @@ screen_reset(Screen *self) {
|
||||
init_tabstops(self->alt_tabstops, self->columns);
|
||||
cursor_reset(self->cursor);
|
||||
self->is_dirty = true;
|
||||
self->selection = EMPTY_SELECTION;
|
||||
clear_selection(&self->selections);
|
||||
clear_selection(&self->url_ranges);
|
||||
screen_cursor_position(self, 1, 1);
|
||||
set_dynamic_color(self, 110, NULL);
|
||||
set_dynamic_color(self, 111, NULL);
|
||||
@@ -175,19 +188,19 @@ screen_dirty_sprite_positions(Screen *self) {
|
||||
}
|
||||
|
||||
static inline HistoryBuf*
|
||||
realloc_hb(HistoryBuf *old, unsigned int lines, unsigned int columns) {
|
||||
realloc_hb(HistoryBuf *old, unsigned int lines, unsigned int columns, ANSIBuf *as_ansi_buf) {
|
||||
HistoryBuf *ans = alloc_historybuf(lines, columns, 0);
|
||||
if (ans == NULL) { PyErr_NoMemory(); return NULL; }
|
||||
ans->pagerhist = old->pagerhist; old->pagerhist = NULL;
|
||||
historybuf_rewrap(old, ans);
|
||||
historybuf_rewrap(old, ans, as_ansi_buf);
|
||||
return ans;
|
||||
}
|
||||
|
||||
static inline LineBuf*
|
||||
realloc_lb(LineBuf *old, unsigned int lines, unsigned int columns, index_type *nclb, index_type *ncla, HistoryBuf *hb, index_type *x, index_type *y) {
|
||||
realloc_lb(LineBuf *old, unsigned int lines, unsigned int columns, index_type *nclb, index_type *ncla, HistoryBuf *hb, index_type *x, index_type *y, ANSIBuf *as_ansi_buf) {
|
||||
LineBuf *ans = alloc_linebuf(lines, columns);
|
||||
if (ans == NULL) { PyErr_NoMemory(); return NULL; }
|
||||
linebuf_rewrap(old, ans, nclb, ncla, hb, x, y);
|
||||
linebuf_rewrap(old, ans, nclb, ncla, hb, x, y, as_ansi_buf);
|
||||
return ans;
|
||||
}
|
||||
|
||||
@@ -209,11 +222,11 @@ screen_resize(Screen *self, unsigned int lines, unsigned int columns) {
|
||||
if (!init_overlay_line(self, columns)) return false;
|
||||
|
||||
// Resize main linebuf
|
||||
HistoryBuf *nh = realloc_hb(self->historybuf, self->historybuf->ynum, columns);
|
||||
HistoryBuf *nh = realloc_hb(self->historybuf, self->historybuf->ynum, columns, &self->as_ansi_buf);
|
||||
if (nh == NULL) return false;
|
||||
Py_CLEAR(self->historybuf); self->historybuf = nh;
|
||||
index_type x = self->cursor->x, y = self->cursor->y;
|
||||
LineBuf *n = realloc_lb(self->main_linebuf, lines, columns, &num_content_lines_before, &num_content_lines_after, self->historybuf, &x, &y);
|
||||
LineBuf *n = realloc_lb(self->main_linebuf, lines, columns, &num_content_lines_before, &num_content_lines_after, self->historybuf, &x, &y, &self->as_ansi_buf);
|
||||
if (n == NULL) return false;
|
||||
Py_CLEAR(self->main_linebuf); self->main_linebuf = n;
|
||||
if (is_main) setup_cursor();
|
||||
@@ -221,7 +234,7 @@ screen_resize(Screen *self, unsigned int lines, unsigned int columns) {
|
||||
|
||||
// Resize alt linebuf
|
||||
x = self->cursor->x, y = self->cursor->y;
|
||||
n = realloc_lb(self->alt_linebuf, lines, columns, &num_content_lines_before, &num_content_lines_after, NULL, &x, &y);
|
||||
n = realloc_lb(self->alt_linebuf, lines, columns, &num_content_lines_before, &num_content_lines_after, NULL, &x, &y, &self->as_ansi_buf);
|
||||
if (n == NULL) return false;
|
||||
Py_CLEAR(self->alt_linebuf); self->alt_linebuf = n;
|
||||
if (!is_main) setup_cursor();
|
||||
@@ -241,8 +254,8 @@ screen_resize(Screen *self, unsigned int lines, unsigned int columns) {
|
||||
init_tabstops(self->main_tabstops, self->columns);
|
||||
init_tabstops(self->alt_tabstops, self->columns);
|
||||
self->is_dirty = true;
|
||||
self->selection = EMPTY_SELECTION;
|
||||
self->url_range = EMPTY_SELECTION;
|
||||
clear_selection(&self->selections);
|
||||
clear_selection(&self->url_ranges);
|
||||
/* printf("old_cursor: (%u, %u) new_cursor: (%u, %u) beyond_content: %d\n", self->cursor->x, self->cursor->y, cursor_x, cursor_y, cursor_is_beyond_content); */
|
||||
self->cursor->x = MIN(cursor_x, self->columns - 1);
|
||||
self->cursor->y = MIN(cursor_y, self->lines - 1);
|
||||
@@ -287,6 +300,10 @@ dealloc(Screen* self) {
|
||||
PyMem_Free(self->overlay_line.gpu_cells);
|
||||
PyMem_Free(self->main_tabstops);
|
||||
free(self->pending_mode.buf);
|
||||
free(self->selections.items);
|
||||
free(self->url_ranges.items);
|
||||
free_hyperlink_pool(self->hyperlink_pool);
|
||||
free(self->as_ansi_buf.buf);
|
||||
Py_TYPE(self)->tp_free((PyObject*)self);
|
||||
} // }}}
|
||||
|
||||
@@ -346,19 +363,59 @@ move_widened_char(Screen *self, CPUCell* cpu_cell, GPUCell *gpu_cell, index_type
|
||||
}
|
||||
|
||||
static inline bool
|
||||
is_selection_empty(Selection *s) {
|
||||
is_selection_empty(const Selection *s) {
|
||||
int start_y = (int)s->start.y - (int)s->start_scrolled_by, end_y = (int)s->end.y - (int)s->end_scrolled_by;
|
||||
return s->start.x == s->end.x && s->start.in_left_half_of_cell == s->end.in_left_half_of_cell && start_y == end_y;
|
||||
}
|
||||
|
||||
static inline bool
|
||||
selection_has_screen_line(Selection *s, int y) {
|
||||
if (is_selection_empty(s)) return false;
|
||||
int top = (int)s->start.y - s->start_scrolled_by;
|
||||
int bottom = (int)s->end.y - s->end_scrolled_by;
|
||||
return top <= y && y <= bottom;
|
||||
selection_has_screen_line(const Selections *selections, const int y) {
|
||||
for (size_t i = 0; i < selections->count; i++) {
|
||||
const Selection *s = selections->items + i;
|
||||
if (!is_selection_empty(s)) {
|
||||
int top = (int)s->start.y - s->start_scrolled_by;
|
||||
int bottom = (int)s->end.y - s->end_scrolled_by;
|
||||
if (top <= y && y <= bottom) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
set_active_hyperlink(Screen *self, char *id, char *url) {
|
||||
if (OPT(allow_hyperlinks)) {
|
||||
if (!url || !url[0]) {
|
||||
self->active_hyperlink_id = 0;
|
||||
return;
|
||||
}
|
||||
self->active_hyperlink_id = get_id_for_hyperlink(self, id, url);
|
||||
}
|
||||
}
|
||||
|
||||
hyperlink_id_type
|
||||
remap_hyperlink_ids(Screen *self, hyperlink_id_type *map) {
|
||||
#define PROCESS_CELL(cell) { hid = (cell).hyperlink_id; if (hid) { if (!map[hid]) map[hid] = ++num; (cell).hyperlink_id = map[hid]; }}
|
||||
hyperlink_id_type num = 0, hid;
|
||||
if (self->historybuf->count) {
|
||||
for (index_type y = self->historybuf->count; y-- > 0;) {
|
||||
CPUCell *cells = historybuf_cpu_cells(self->historybuf, y);
|
||||
for (index_type x = 0; x < self->historybuf->xnum; x++) {
|
||||
PROCESS_CELL(cells[x]);
|
||||
}
|
||||
}
|
||||
}
|
||||
LineBuf *second = self->linebuf, *first = second == self->main_linebuf ? self->alt_linebuf : self->main_linebuf;
|
||||
for (index_type i = 0; i < self->lines * self->columns; i++) {
|
||||
PROCESS_CELL(first->cpu_cell_buf[i]);
|
||||
}
|
||||
for (index_type i = 0; i < self->lines * self->columns; i++) {
|
||||
PROCESS_CELL(second->cpu_cell_buf[i]);
|
||||
}
|
||||
return num;
|
||||
#undef PROCESS_CELL
|
||||
}
|
||||
|
||||
|
||||
static inline bool is_flag_pair(char_type a, char_type b) {
|
||||
return is_flag_codepoint(a) && is_flag_codepoint(b);
|
||||
}
|
||||
@@ -379,7 +436,7 @@ draw_second_flag_codepoint(Screen *self, char_type ch) {
|
||||
if (!is_flag_pair(cell->ch, ch) || cell->cc_idx[0]) return false;
|
||||
line_add_combining_char(self->linebuf->line, ch, xpos);
|
||||
self->is_dirty = true;
|
||||
if (selection_has_screen_line(&self->selection, ypos)) self->selection = EMPTY_SELECTION;
|
||||
if (selection_has_screen_line(&self->selections, ypos)) clear_selection(&self->selections);
|
||||
linebuf_mark_line_dirty(self->linebuf, ypos);
|
||||
return true;
|
||||
}
|
||||
@@ -413,13 +470,13 @@ draw_combining_char(Screen *self, char_type ch) {
|
||||
if (has_prev_char) {
|
||||
line_add_combining_char(self->linebuf->line, ch, xpos);
|
||||
self->is_dirty = true;
|
||||
if (selection_has_screen_line(&self->selection, ypos)) self->selection = EMPTY_SELECTION;
|
||||
if (selection_has_screen_line(&self->selections, ypos)) clear_selection(&self->selections);
|
||||
linebuf_mark_line_dirty(self->linebuf, ypos);
|
||||
if (ch == 0xfe0f) { // emoji presentation variation marker makes default text presentation emoji (narrow emoji) into wide emoji
|
||||
CPUCell *cpu_cell = self->linebuf->line->cpu_cells + xpos;
|
||||
GPUCell *gpu_cell = self->linebuf->line->gpu_cells + xpos;
|
||||
if ((gpu_cell->attrs & WIDTH_MASK) != 2 && cpu_cell->cc_idx[0] == VS16 && is_emoji_presentation_base(cpu_cell->ch)) {
|
||||
if (self->cursor->x <= self->columns - 1) line_set_char(self->linebuf->line, self->cursor->x, 0, 0, self->cursor, true);
|
||||
if (self->cursor->x <= self->columns - 1) line_set_char(self->linebuf->line, self->cursor->x, 0, 0, self->cursor, self->active_hyperlink_id);
|
||||
gpu_cell->attrs = (gpu_cell->attrs & !WIDTH_MASK) | 2;
|
||||
if (xpos == self->columns - 1) move_widened_char(self, cpu_cell, gpu_cell, xpos, ypos);
|
||||
else self->cursor->x++;
|
||||
@@ -444,6 +501,9 @@ draw_combining_char(Screen *self, char_type ch) {
|
||||
void
|
||||
screen_draw(Screen *self, uint32_t och) {
|
||||
if (is_ignored_char(och)) return;
|
||||
if (!self->has_activity_since_last_focus && !self->has_focus) {
|
||||
self->has_activity_since_last_focus = true;
|
||||
}
|
||||
uint32_t ch = och < 256 ? self->g_charset[och] : och;
|
||||
bool is_cc = is_combining_char(ch);
|
||||
if (UNLIKELY(is_cc)) {
|
||||
@@ -473,14 +533,14 @@ screen_draw(Screen *self, uint32_t och) {
|
||||
if (self->modes.mIRM) {
|
||||
line_right_shift(self->linebuf->line, self->cursor->x, char_width);
|
||||
}
|
||||
line_set_char(self->linebuf->line, self->cursor->x, ch, char_width, self->cursor, false);
|
||||
line_set_char(self->linebuf->line, self->cursor->x, ch, char_width, self->cursor, self->active_hyperlink_id);
|
||||
self->cursor->x++;
|
||||
if (char_width == 2) {
|
||||
line_set_char(self->linebuf->line, self->cursor->x, 0, 0, self->cursor, true);
|
||||
line_set_char(self->linebuf->line, self->cursor->x, 0, 0, self->cursor, self->active_hyperlink_id);
|
||||
self->cursor->x++;
|
||||
}
|
||||
self->is_dirty = true;
|
||||
if (selection_has_screen_line(&self->selection, self->cursor->y)) self->selection = EMPTY_SELECTION;
|
||||
if (selection_has_screen_line(&self->selections, self->cursor->y)) clear_selection(&self->selections);
|
||||
linebuf_mark_line_dirty(self->linebuf, self->cursor->y);
|
||||
}
|
||||
|
||||
@@ -643,6 +703,7 @@ screen_handle_graphics_command(Screen *self, const GraphicsCommand *cmd, const u
|
||||
void
|
||||
screen_toggle_screen_buffer(Screen *self, bool save_cursor, bool clear_alt_screen) {
|
||||
bool to_alt = self->linebuf == self->main_linebuf;
|
||||
self->active_hyperlink_id = 0;
|
||||
grman_clear(self->alt_grman, true, self->cell_size); // always clear the alt buffer graphics to free up resources, since it has to be cleared when switching back to it anyway
|
||||
if (to_alt) {
|
||||
if (clear_alt_screen) linebuf_clear(self->alt_linebuf, BLANK_CHAR);
|
||||
@@ -660,7 +721,7 @@ screen_toggle_screen_buffer(Screen *self, bool save_cursor, bool clear_alt_scree
|
||||
}
|
||||
screen_history_scroll(self, SCROLL_FULL, false);
|
||||
self->is_dirty = true;
|
||||
self->selection = EMPTY_SELECTION;
|
||||
clear_selection(&self->selections);
|
||||
}
|
||||
|
||||
void screen_normal_keypad_mode(Screen UNUSED *self) {} // Not implemented as this is handled by the GUI
|
||||
@@ -904,18 +965,22 @@ screen_cursor_to_column(Screen *self, unsigned int column) {
|
||||
}
|
||||
|
||||
static inline void
|
||||
index_selection(Screen *self, Selection *s, bool up) {
|
||||
if (is_selection_empty(s)) return;
|
||||
if (up) {
|
||||
if (s->start.y == 0) s->start_scrolled_by += 1;
|
||||
else s->start.y--;
|
||||
if (s->end.y == 0) s->end_scrolled_by += 1;
|
||||
else s->end.y--;
|
||||
} else {
|
||||
if (s->start.y >= self->lines - 1) s->start_scrolled_by -= 1;
|
||||
else s->start.y++;
|
||||
if (s->end.y >= self->lines - 1) s->end_scrolled_by -= 1;
|
||||
else s->end.y++;
|
||||
index_selection(const Screen *self, Selections *selections, bool up) {
|
||||
for (size_t i = 0; i < selections->count; i++) {
|
||||
Selection *s = selections->items + i;
|
||||
if (!is_selection_empty(s)) {
|
||||
if (up) {
|
||||
if (s->start.y == 0) s->start_scrolled_by += 1;
|
||||
else s->start.y--;
|
||||
if (s->end.y == 0) s->end_scrolled_by += 1;
|
||||
else s->end.y--;
|
||||
} else {
|
||||
if (s->start.y >= self->lines - 1) s->start_scrolled_by -= 1;
|
||||
else s->start.y++;
|
||||
if (s->end.y >= self->lines - 1) s->end_scrolled_by -= 1;
|
||||
else s->end.y++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -935,12 +1000,12 @@ index_selection(Screen *self, Selection *s, bool up) {
|
||||
if (self->linebuf == self->main_linebuf && bottom == self->lines - 1) { \
|
||||
/* Only add to history when no page margins have been set */ \
|
||||
linebuf_init_line(self->linebuf, bottom); \
|
||||
historybuf_add_line(self->historybuf, self->linebuf->line); \
|
||||
historybuf_add_line(self->historybuf, self->linebuf->line, &self->as_ansi_buf); \
|
||||
self->history_line_added_count++; \
|
||||
} \
|
||||
linebuf_clear_line(self->linebuf, bottom); \
|
||||
self->is_dirty = true; \
|
||||
index_selection(self, &self->selection, true);
|
||||
index_selection(self, &self->selections, true);
|
||||
|
||||
void
|
||||
screen_index(Screen *self) {
|
||||
@@ -967,7 +1032,7 @@ screen_scroll(Screen *self, unsigned int count) {
|
||||
linebuf_clear_line(self->linebuf, top); \
|
||||
INDEX_GRAPHICS(1) \
|
||||
self->is_dirty = true; \
|
||||
index_selection(self, &self->selection, false);
|
||||
index_selection(self, &self->selections, false);
|
||||
|
||||
void
|
||||
screen_reverse_index(Screen *self) {
|
||||
@@ -1151,7 +1216,7 @@ screen_erase_in_line(Screen *self, unsigned int how, bool private) {
|
||||
line_apply_cursor(self->linebuf->line, self->cursor, s, n, true);
|
||||
}
|
||||
self->is_dirty = true;
|
||||
if (selection_has_screen_line(&self->selection, self->cursor->y)) self->selection = EMPTY_SELECTION;
|
||||
if (selection_has_screen_line(&self->selections, self->cursor->y)) clear_selection(&self->selections);
|
||||
linebuf_mark_line_dirty(self->linebuf, self->cursor->y);
|
||||
}
|
||||
}
|
||||
@@ -1196,7 +1261,7 @@ screen_erase_in_display(Screen *self, unsigned int how, bool private) {
|
||||
linebuf_mark_line_as_not_continued(self->linebuf, i);
|
||||
}
|
||||
self->is_dirty = true;
|
||||
self->selection = EMPTY_SELECTION;
|
||||
clear_selection(&self->selections);
|
||||
}
|
||||
if (how != 2) {
|
||||
screen_erase_in_line(self, how, private);
|
||||
@@ -1218,7 +1283,7 @@ screen_insert_lines(Screen *self, unsigned int count) {
|
||||
if (top <= self->cursor->y && self->cursor->y <= bottom) {
|
||||
linebuf_insert_lines(self->linebuf, count, self->cursor->y, bottom);
|
||||
self->is_dirty = true;
|
||||
self->selection = EMPTY_SELECTION;
|
||||
clear_selection(&self->selections);
|
||||
screen_carriage_return(self);
|
||||
}
|
||||
}
|
||||
@@ -1239,7 +1304,7 @@ screen_delete_lines(Screen *self, unsigned int count) {
|
||||
if (top <= self->cursor->y && self->cursor->y <= bottom) {
|
||||
linebuf_delete_lines(self->linebuf, count, self->cursor->y, bottom);
|
||||
self->is_dirty = true;
|
||||
self->selection = EMPTY_SELECTION;
|
||||
clear_selection(&self->selections);
|
||||
screen_carriage_return(self);
|
||||
}
|
||||
}
|
||||
@@ -1256,7 +1321,7 @@ screen_insert_characters(Screen *self, unsigned int count) {
|
||||
line_apply_cursor(self->linebuf->line, self->cursor, x, num, true);
|
||||
linebuf_mark_line_dirty(self->linebuf, self->cursor->y);
|
||||
self->is_dirty = true;
|
||||
if (selection_has_screen_line(&self->selection, self->cursor->y)) self->selection = EMPTY_SELECTION;
|
||||
if (selection_has_screen_line(&self->selections, self->cursor->y)) clear_selection(&self->selections);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1294,7 +1359,7 @@ screen_delete_characters(Screen *self, unsigned int count) {
|
||||
line_apply_cursor(self->linebuf->line, self->cursor, self->columns - num, num, true);
|
||||
linebuf_mark_line_dirty(self->linebuf, self->cursor->y);
|
||||
self->is_dirty = true;
|
||||
if (selection_has_screen_line(&self->selection, self->cursor->y)) self->selection = EMPTY_SELECTION;
|
||||
if (selection_has_screen_line(&self->selections, self->cursor->y)) clear_selection(&self->selections);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1308,7 +1373,7 @@ screen_erase_characters(Screen *self, unsigned int count) {
|
||||
line_apply_cursor(self->linebuf->line, self->cursor, x, num, true);
|
||||
linebuf_mark_line_dirty(self->linebuf, self->cursor->y);
|
||||
self->is_dirty = true;
|
||||
if (selection_has_screen_line(&self->selection, self->cursor->y)) self->selection = EMPTY_SELECTION;
|
||||
if (selection_has_screen_line(&self->selections, self->cursor->y)) clear_selection(&self->selections);
|
||||
}
|
||||
|
||||
// }}}
|
||||
@@ -1495,6 +1560,11 @@ set_title(Screen *self, PyObject *title) {
|
||||
CALLBACK("title_changed", "O", title);
|
||||
}
|
||||
|
||||
void
|
||||
desktop_notify(Screen *self, unsigned int osc_code, PyObject *data) {
|
||||
CALLBACK("desktop_notify", "IO", osc_code, data);
|
||||
}
|
||||
|
||||
void
|
||||
set_icon(Screen *self, PyObject *icon) {
|
||||
CALLBACK("icon_changed", "O", icon);
|
||||
@@ -1628,9 +1698,7 @@ screen_update_cell_data(Screen *self, void *address, FONTS_DATA_HANDLE fonts_dat
|
||||
}
|
||||
update_line_data(self->linebuf->line, y, address);
|
||||
}
|
||||
if (was_dirty) {
|
||||
self->url_range = EMPTY_SELECTION;
|
||||
}
|
||||
if (was_dirty) clear_selection(&self->url_ranges);
|
||||
}
|
||||
|
||||
|
||||
@@ -1772,41 +1840,52 @@ iteration_data_is_empty(const Screen *self, const IterationData *idata) {
|
||||
}
|
||||
|
||||
static inline void
|
||||
apply_selection(Screen *self, uint8_t *data, const Selection *s, IterationData *last_rendered, uint8_t set_mask) {
|
||||
iteration_data(self, s, last_rendered, -self->historybuf->count, true);
|
||||
apply_selection(Screen *self, uint8_t *data, Selection *s, uint8_t set_mask) {
|
||||
iteration_data(self, s, &s->last_rendered, -self->historybuf->count, true);
|
||||
|
||||
for (int y = MAX(0, last_rendered->y); y < last_rendered->y_limit && y < (int)self->lines; y++) {
|
||||
for (int y = MAX(0, s->last_rendered.y); y < s->last_rendered.y_limit && y < (int)self->lines; y++) {
|
||||
Line *line = visual_line_(self, y);
|
||||
uint8_t *line_start = data + self->columns * y;
|
||||
XRange xr = xrange_for_iteration(last_rendered, y, line);
|
||||
XRange xr = xrange_for_iteration(&s->last_rendered, y, line);
|
||||
for (index_type x = xr.x; x < xr.x_limit; x++) line_start[x] |= set_mask;
|
||||
}
|
||||
last_rendered->y = MAX(0, last_rendered->y);
|
||||
s->last_rendered.y = MAX(0, s->last_rendered.y);
|
||||
}
|
||||
|
||||
bool
|
||||
screen_has_selection(Screen *self) {
|
||||
if (is_selection_empty(&self->selection)) return false;
|
||||
IterationData idata;
|
||||
iteration_data(self, &self->selection, &idata, -self->historybuf->count, true);
|
||||
if (iteration_data_is_empty(self, &idata)) return false;
|
||||
return true;
|
||||
for (size_t i = 0; i < self->selections.count; i++) {
|
||||
Selection *s = self->selections.items + i;
|
||||
if (!is_selection_empty(s)) {
|
||||
iteration_data(self, s, &idata, -self->historybuf->count, true);
|
||||
if (!iteration_data_is_empty(self, &idata)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
screen_apply_selection(Screen *self, void *address, size_t size) {
|
||||
memset(address, 0, size);
|
||||
apply_selection(self, address, &self->selection, &self->last_rendered.selection, 1);
|
||||
apply_selection(self, address, &self->url_range, &self->last_rendered.url, 2);
|
||||
for (size_t i = 0; i < self->selections.count; i++) {
|
||||
apply_selection(self, address, self->selections.items + i, 1);
|
||||
}
|
||||
self->selections.last_rendered_count = self->selections.count;
|
||||
for (size_t i = 0; i < self->url_ranges.count; i++) {
|
||||
apply_selection(self, address, self->url_ranges.items + i, 2);
|
||||
}
|
||||
self->url_ranges.last_rendered_count = self->url_ranges.count;
|
||||
}
|
||||
|
||||
static inline PyObject*
|
||||
text_for_range(Screen *self, const Selection *sel, bool insert_newlines) {
|
||||
IterationData idata;
|
||||
iteration_data(self, sel, &idata, -self->historybuf->count, false);
|
||||
PyObject *ans = PyTuple_New(idata.y_limit - idata.y);
|
||||
int limit = MIN((int)self->lines, idata.y_limit);
|
||||
PyObject *ans = PyTuple_New(limit - idata.y);
|
||||
if (!ans) return NULL;
|
||||
for (int i = 0, y = idata.y; y < idata.y_limit && y < (int)self->lines; y++, i++) {
|
||||
for (int i = 0, y = idata.y; y < limit; y++, i++) {
|
||||
Line *line = range_line_(self, y);
|
||||
XRange xr = xrange_for_iteration(&idata, y, line);
|
||||
char leading_char = (i > 0 && insert_newlines && !line->continued) ? '\n' : 0;
|
||||
@@ -1817,22 +1896,86 @@ text_for_range(Screen *self, const Selection *sel, bool insert_newlines) {
|
||||
return ans;
|
||||
}
|
||||
|
||||
static inline hyperlink_id_type
|
||||
hyperlink_id_for_range(Screen *self, const Selection *sel) {
|
||||
IterationData idata;
|
||||
iteration_data(self, sel, &idata, -self->historybuf->count, false);
|
||||
for (int i = 0, y = idata.y; y < idata.y_limit && y < (int)self->lines; y++, i++) {
|
||||
Line *line = range_line_(self, y);
|
||||
XRange xr = xrange_for_iteration(&idata, y, line);
|
||||
for (index_type x = xr.x; x < xr.x_limit; x++) {
|
||||
if (line->cpu_cells[x].hyperlink_id) return line->cpu_cells[x].hyperlink_id;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline PyObject*
|
||||
extend_tuple(PyObject *a, PyObject *b) {
|
||||
Py_ssize_t bs = PyBytes_GET_SIZE(b);
|
||||
if (bs < 1) return a;
|
||||
Py_ssize_t off = PyTuple_GET_SIZE(a);
|
||||
if (_PyTuple_Resize(&a, off + bs) != 0) return NULL;
|
||||
for (Py_ssize_t y = 0; y < bs; y++) {
|
||||
PyObject *t = PyTuple_GET_ITEM(b, y);
|
||||
Py_INCREF(t);
|
||||
PyTuple_SET_ITEM(a, off + y, t);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
current_url_text(Screen *self, PyObject *args UNUSED) {
|
||||
PyObject *empty_string = PyUnicode_FromString(""), *ans = NULL;
|
||||
if (!empty_string) return NULL;
|
||||
for (size_t i = 0; i < self->url_ranges.count; i++) {
|
||||
Selection *s = self->url_ranges.items + i;
|
||||
if (!is_selection_empty(s)) {
|
||||
PyObject *temp = text_for_range(self, s, false);
|
||||
if (!temp) goto error;
|
||||
PyObject *text = PyUnicode_Join(empty_string, temp);
|
||||
Py_CLEAR(temp);
|
||||
if (!text) goto error;
|
||||
if (ans) {
|
||||
PyObject *t = ans;
|
||||
ans = PyUnicode_Concat(ans, text);
|
||||
Py_CLEAR(text); Py_CLEAR(t);
|
||||
if (!ans) goto error;
|
||||
} else ans = text;
|
||||
}
|
||||
}
|
||||
Py_CLEAR(empty_string);
|
||||
if (!ans) Py_RETURN_NONE;
|
||||
return ans;
|
||||
error:
|
||||
Py_CLEAR(empty_string); Py_CLEAR(ans);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
screen_open_url(Screen *self) {
|
||||
if (is_selection_empty(&self->url_range)) return false;
|
||||
PyObject *lines = text_for_range(self, &self->url_range, false);
|
||||
bool ret = false;
|
||||
if (lines) {
|
||||
PyObject *joiner = PyUnicode_FromString("");
|
||||
if (joiner) {
|
||||
PyObject *url = PyUnicode_Join(joiner, lines);
|
||||
if (url) { call_boss(open_url_lines, "(O)", url); Py_CLEAR(url); ret = true; }
|
||||
Py_CLEAR(joiner);
|
||||
if (!self->url_ranges.count) return false;
|
||||
hyperlink_id_type hid = hyperlink_id_for_range(self, self->url_ranges.items);
|
||||
if (hid) {
|
||||
const char *url = get_hyperlink_for_id(self->hyperlink_pool, hid, true);
|
||||
if (url) {
|
||||
CALLBACK("open_url", "sH", url, hid);
|
||||
return true;
|
||||
}
|
||||
Py_CLEAR(lines);
|
||||
}
|
||||
if (PyErr_Occurred()) PyErr_Print();
|
||||
return ret;
|
||||
PyObject *text = current_url_text(self, NULL);
|
||||
if (!text) {
|
||||
if (PyErr_Occurred()) PyErr_Print();
|
||||
return false;
|
||||
}
|
||||
bool found = false;
|
||||
if (PyUnicode_Check(text)) {
|
||||
CALLBACK("open_url", "OH", text, 0);
|
||||
found = true;
|
||||
}
|
||||
Py_CLEAR(text);
|
||||
return found;
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -1862,6 +2005,20 @@ deactivate_overlay_line(Screen *self) {
|
||||
#define WRAP2(name, defval1, defval2) static PyObject* name(Screen *self, PyObject *args) { unsigned int a=defval1, b=defval2; if(!PyArg_ParseTuple(args, "|II", &a, &b)) return NULL; screen_##name(self, a, b); Py_RETURN_NONE; }
|
||||
#define WRAP2B(name) static PyObject* name(Screen *self, PyObject *args) { unsigned int a, b; int p; if(!PyArg_ParseTuple(args, "IIp", &a, &b, &p)) return NULL; screen_##name(self, a, b, (bool)p); Py_RETURN_NONE; }
|
||||
|
||||
WRAP0(garbage_collect_hyperlink_pool)
|
||||
|
||||
static PyObject*
|
||||
hyperlinks_as_list(Screen *self, PyObject *args UNUSED) {
|
||||
return screen_hyperlinks_as_list(self);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
hyperlink_for_id(Screen *self, PyObject *val) {
|
||||
unsigned long id = PyLong_AsUnsignedLong(val);
|
||||
if (id > HYPERLINK_MAX_NUMBER) { PyErr_SetString(PyExc_IndexError, "Out of bounds"); return NULL; }
|
||||
return Py_BuildValue("s", get_hyperlink_for_id(self->hyperlink_pool, id, true));
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
set_pending_timeout(Screen *self, PyObject *val) {
|
||||
if (!PyFloat_Check(val)) { PyErr_SetString(PyExc_TypeError, "timeout must be a float"); return NULL; }
|
||||
@@ -1875,17 +2032,17 @@ static Line* get_range_line(void *x, int y) { return range_line_(x, y); }
|
||||
|
||||
static PyObject*
|
||||
as_text(Screen *self, PyObject *args) {
|
||||
return as_text_generic(args, self, get_visual_line, self->lines, self->columns);
|
||||
return as_text_generic(args, self, get_visual_line, self->lines, &self->as_ansi_buf);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
as_text_non_visual(Screen *self, PyObject *args) {
|
||||
return as_text_generic(args, self, get_range_line, self->lines, self->columns);
|
||||
return as_text_generic(args, self, get_range_line, self->lines, &self->as_ansi_buf);
|
||||
}
|
||||
|
||||
static inline PyObject*
|
||||
as_text_generic_wrapper(Screen *self, PyObject *args, get_line_func get_line) {
|
||||
return as_text_generic(args, self, get_line, self->lines, self->columns);
|
||||
return as_text_generic(args, self, get_line, self->lines, &self->as_ansi_buf);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
@@ -1898,68 +2055,6 @@ as_text_alternate(Screen *self, PyObject *args) {
|
||||
}
|
||||
|
||||
|
||||
static PyObject*
|
||||
screen_wcswidth(PyObject UNUSED *self, PyObject *str) {
|
||||
if (PyUnicode_READY(str) != 0) return NULL;
|
||||
int kind = PyUnicode_KIND(str);
|
||||
void *data = PyUnicode_DATA(str);
|
||||
Py_ssize_t len = PyUnicode_GET_LENGTH(str), i;
|
||||
unsigned long ans = 0;
|
||||
char_type prev_ch = 0;
|
||||
int prev_width = 0;
|
||||
typedef enum {NORMAL, IN_SGR, FLAG_PAIR_STARTED} WCSState;
|
||||
WCSState state = NORMAL;
|
||||
for (i = 0; i < len; i++) {
|
||||
char_type ch = PyUnicode_READ(kind, data, i);
|
||||
switch(state) {
|
||||
case IN_SGR: {
|
||||
if (ch == 'm') state = NORMAL;
|
||||
} continue;
|
||||
|
||||
case FLAG_PAIR_STARTED: {
|
||||
state = NORMAL;
|
||||
if (is_flag_pair(prev_ch, ch)) break;
|
||||
} /* fallthrough */
|
||||
|
||||
case NORMAL: {
|
||||
if (ch == 0x1b && i + 1 < len && PyUnicode_READ(kind, data, i + 1) == '[') { state = IN_SGR; continue; }
|
||||
switch(ch) {
|
||||
case 0xfe0f: {
|
||||
if (is_emoji_presentation_base(prev_ch) && prev_width == 1) {
|
||||
ans += 1;
|
||||
prev_width = 2;
|
||||
} else prev_width = 0;
|
||||
} break;
|
||||
|
||||
case 0xfe0e: {
|
||||
if (is_emoji_presentation_base(prev_ch) && prev_width == 2) {
|
||||
ans -= 1;
|
||||
prev_width = 1;
|
||||
} else prev_width = 0;
|
||||
} break;
|
||||
|
||||
default: {
|
||||
if (is_flag_codepoint(ch)) state = FLAG_PAIR_STARTED;
|
||||
int w = wcwidth_std(ch);
|
||||
switch(w) {
|
||||
case -1:
|
||||
case 0:
|
||||
prev_width = 0; break;
|
||||
case 2:
|
||||
prev_width = 2; break;
|
||||
default:
|
||||
prev_width = 1; break;
|
||||
}
|
||||
ans += prev_width;
|
||||
} break;
|
||||
} break; // switch(ch)
|
||||
} break; // case NORMAL
|
||||
} // switch(state)
|
||||
prev_ch = ch;
|
||||
}
|
||||
return PyLong_FromUnsignedLong(ans);
|
||||
}
|
||||
|
||||
|
||||
static PyObject*
|
||||
screen_truncate_point_for_length(PyObject UNUSED *self, PyObject *args) {
|
||||
@@ -2147,8 +2242,8 @@ update_selection(Screen *self, PyObject *args) {
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
clear_selection(Screen *self, PyObject *args UNUSED) {
|
||||
self->selection = EMPTY_SELECTION;
|
||||
clear_selection_(Screen *s, PyObject *args UNUSED) {
|
||||
clear_selection(&s->selections);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
@@ -2176,9 +2271,8 @@ start_selection(Screen *self, PyObject *args) {
|
||||
|
||||
static PyObject*
|
||||
is_rectangle_select(Screen *self, PyObject *a UNUSED) {
|
||||
PyObject *ans = self->selection.rectangle_select ? Py_True : Py_False;
|
||||
Py_INCREF(ans);
|
||||
return ans;
|
||||
if (self->selections.count && self->selections.items[0].rectangle_select) Py_RETURN_TRUE;
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
@@ -2189,7 +2283,19 @@ copy_colors_from(Screen *self, Screen *other) {
|
||||
|
||||
static PyObject*
|
||||
text_for_selection(Screen *self, PyObject *a UNUSED) {
|
||||
return text_for_range(self, &self->selection, true);
|
||||
PyObject *lines = NULL;
|
||||
for (size_t i = 0; i < self->selections.count; i++) {
|
||||
PyObject *temp = text_for_range(self, self->selections.items + i, true);
|
||||
if (temp) {
|
||||
if (lines) {
|
||||
lines = extend_tuple(lines, temp);
|
||||
Py_DECREF(temp);
|
||||
} else lines = temp;
|
||||
} else break;
|
||||
}
|
||||
if (PyErr_Occurred()) { Py_CLEAR(lines); return NULL; }
|
||||
if (!lines) lines = PyTuple_New(0);
|
||||
return lines;
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -2296,44 +2402,135 @@ bool
|
||||
screen_is_selection_dirty(Screen *self) {
|
||||
IterationData q;
|
||||
if (self->scrolled_by != self->last_rendered.scrolled_by) return true;
|
||||
iteration_data(self, &self->selection, &q, 0, true);
|
||||
if (memcmp(&q, &self->last_rendered.selection, sizeof(IterationData)) != 0) return true;
|
||||
iteration_data(self, &self->url_range, &q, 0, true);
|
||||
if (memcmp(&q, &self->last_rendered.url, sizeof(IterationData)) != 0) return true;
|
||||
if (self->selections.last_rendered_count != self->selections.count || self->url_ranges.last_rendered_count != self->url_ranges.count) return true;
|
||||
for (size_t i = 0; i < self->selections.count; i++) {
|
||||
iteration_data(self, self->selections.items + i, &q, 0, true);
|
||||
if (memcmp(&q, &self->selections.items[i].last_rendered, sizeof(IterationData)) != 0) return true;
|
||||
}
|
||||
for (size_t i = 0; i < self->url_ranges.count; i++) {
|
||||
iteration_data(self, self->url_ranges.items + i, &q, 0, true);
|
||||
if (memcmp(&q, &self->url_ranges.items[i].last_rendered, sizeof(IterationData)) != 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
screen_start_selection(Screen *self, index_type x, index_type y, bool in_left_half_of_cell, bool rectangle_select, SelectionExtendMode extend_mode) {
|
||||
#define A(attr, val) self->selection.attr = val;
|
||||
#define A(attr, val) self->selections.items->attr = val;
|
||||
ensure_space_for(&self->selections, items, Selection, self->selections.count + 1, capacity, 1, false);
|
||||
memset(self->selections.items, 0, sizeof(Selection));
|
||||
self->selections.count = 1;
|
||||
self->selections.in_progress = true;
|
||||
self->selections.extend_mode = extend_mode;
|
||||
self->selections.items[0].last_rendered.y = INT_MAX;
|
||||
A(start.x, x); A(end.x, x); A(start.y, y); A(end.y, y); A(start_scrolled_by, self->scrolled_by); A(end_scrolled_by, self->scrolled_by);
|
||||
A(in_progress, true); A(rectangle_select, rectangle_select); A(extend_mode, extend_mode); A(start.in_left_half_of_cell, in_left_half_of_cell); A(end.in_left_half_of_cell, in_left_half_of_cell);
|
||||
A(rectangle_select, rectangle_select); A(start.in_left_half_of_cell, in_left_half_of_cell); A(end.in_left_half_of_cell, in_left_half_of_cell);
|
||||
A(input_start.x, x); A(input_start.y, y); A(input_start.in_left_half_of_cell, in_left_half_of_cell);
|
||||
A(input_current.x, x); A(input_current.y, y); A(input_current.in_left_half_of_cell, in_left_half_of_cell);
|
||||
#undef A
|
||||
}
|
||||
|
||||
void
|
||||
screen_mark_url(Screen *self, index_type start_x, index_type start_y, index_type end_x, index_type end_y) {
|
||||
#define A(attr, val) self->url_range.attr = val;
|
||||
A(start.x, start_x); A(end.x, end_x); A(start.y, start_y); A(end.y, end_y); A(start_scrolled_by, self->scrolled_by); A(end_scrolled_by, self->scrolled_by);
|
||||
A(start.in_left_half_of_cell, true); A(end.in_left_half_of_cell, start_x == end_x && start_y == end_y);
|
||||
static inline void
|
||||
add_url_range(Screen *self, index_type start_x, index_type start_y, index_type end_x, index_type end_y) {
|
||||
#define A(attr, val) r->attr = val;
|
||||
ensure_space_for(&self->url_ranges, items, Selection, self->url_ranges.count + 8, capacity, 8, false);
|
||||
Selection *r = self->url_ranges.items + self->url_ranges.count++;
|
||||
memset(r, 0, sizeof(Selection));
|
||||
r->last_rendered.y = INT_MAX;
|
||||
A(start.x, start_x); A(end.x, end_x); A(start.y, start_y); A(end.y, end_y);
|
||||
A(start_scrolled_by, self->scrolled_by); A(end_scrolled_by, self->scrolled_by);
|
||||
A(start.in_left_half_of_cell, true);
|
||||
#undef A
|
||||
}
|
||||
|
||||
void
|
||||
screen_update_selection(Screen *self, index_type x, index_type y, bool in_left_half_of_cell, bool ended, bool start_extended_selection) {
|
||||
self->selection.in_progress = !ended;
|
||||
self->selection.input_current.x = x; self->selection.input_current.y = y;
|
||||
self->selection.input_current.in_left_half_of_cell = in_left_half_of_cell;
|
||||
self->selection.end_scrolled_by = self->scrolled_by;
|
||||
SelectionBoundary start, end, *a, *b;
|
||||
a = &self->selection.start, b = &self->selection.end;
|
||||
screen_mark_url(Screen *self, index_type start_x, index_type start_y, index_type end_x, index_type end_y) {
|
||||
self->url_ranges.count = 0;
|
||||
if (start_x || start_y || end_x || end_y) add_url_range(self, start_x, start_y, end_x, end_y);
|
||||
}
|
||||
|
||||
switch(self->selection.extend_mode) {
|
||||
static bool
|
||||
mark_hyperlinks_in_line(Screen *self, Line *line, hyperlink_id_type id, index_type y) {
|
||||
index_type start = 0;
|
||||
bool found = false;
|
||||
bool in_range = false;
|
||||
for (index_type x = 0; x < line->xnum; x++) {
|
||||
bool has_hyperlink = line->cpu_cells[x].hyperlink_id == id;
|
||||
if (in_range) {
|
||||
if (!has_hyperlink) {
|
||||
add_url_range(self, start, y, x - 1, y);
|
||||
in_range = false;
|
||||
start = 0;
|
||||
}
|
||||
} else {
|
||||
if (has_hyperlink) {
|
||||
start = x; in_range = true;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (in_range) add_url_range(self, start, y, self->columns - 1, y);
|
||||
return found;
|
||||
}
|
||||
|
||||
static int
|
||||
compare_ranges(const void *a_, const void* b_) {
|
||||
const Selection *a = a_, *b = b_;
|
||||
int ans = a->sort_y - b->sort_y;
|
||||
if (!ans) ans = a->sort_x - b->sort_x;
|
||||
return ans;
|
||||
}
|
||||
|
||||
static void
|
||||
sort_ranges(const Screen *self, Selections *s) {
|
||||
IterationData a;
|
||||
for (size_t i = 0; i < s->count; i++) {
|
||||
iteration_data(self, s->items + i, &a, 0, false);
|
||||
s->items[i].sort_x = a.first.x;
|
||||
s->items[i].sort_y = a.y;
|
||||
}
|
||||
qsort(s->items, s->count, sizeof(Selection), compare_ranges);
|
||||
}
|
||||
|
||||
hyperlink_id_type
|
||||
screen_mark_hyperlink(Screen *self, index_type x, index_type y) {
|
||||
self->url_ranges.count = 0;
|
||||
Line *line = screen_visual_line(self, y);
|
||||
hyperlink_id_type id = line->cpu_cells[x].hyperlink_id;
|
||||
if (!id) return 0;
|
||||
index_type ypos = y, last_marked_line = y;
|
||||
do {
|
||||
if (mark_hyperlinks_in_line(self, line, id, ypos)) last_marked_line = ypos;
|
||||
if (ypos == 0) break;
|
||||
ypos--;
|
||||
line = screen_visual_line(self, ypos);
|
||||
} while (last_marked_line - ypos < 5);
|
||||
ypos = y + 1; last_marked_line = y;
|
||||
while (ypos < self->lines - 1 && ypos - last_marked_line < 5) {
|
||||
line = screen_visual_line(self, ypos);
|
||||
if (mark_hyperlinks_in_line(self, line, id, ypos)) last_marked_line = ypos;
|
||||
ypos++;
|
||||
}
|
||||
if (self->url_ranges.count > 1) sort_ranges(self, &self->url_ranges);
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
screen_update_selection(Screen *self, index_type x, index_type y, bool in_left_half_of_cell, bool ended, bool start_extended_selection) {
|
||||
if (!self->selections.count) return;
|
||||
self->selections.in_progress = !ended;
|
||||
Selection *s = self->selections.items;
|
||||
s->input_current.x = x; s->input_current.y = y;
|
||||
s->input_current.in_left_half_of_cell = in_left_half_of_cell;
|
||||
s->end_scrolled_by = self->scrolled_by;
|
||||
SelectionBoundary start, end, *a, *b;
|
||||
a = &s->start, b = &s->end;
|
||||
|
||||
switch(self->selections.extend_mode) {
|
||||
case EXTEND_WORD: {
|
||||
SelectionBoundary *before = &self->selection.input_start, *after = &self->selection.input_current;
|
||||
if (selection_boundary_less_than(after, before)) { before = after; after = &self->selection.input_start; }
|
||||
SelectionBoundary *before = &s->input_start, *after = &s->input_current;
|
||||
if (selection_boundary_less_than(after, before)) { before = after; after = &s->input_start; }
|
||||
bool found_at_start = screen_selection_range_for_word(self, before->x, before->y, &start.y, &end.y, &start.x, &end.x, true);
|
||||
if (found_at_start) {
|
||||
a->x = start.x; a->y = start.y; a->in_left_half_of_cell = true;
|
||||
@@ -2348,13 +2545,13 @@ screen_update_selection(Screen *self, index_type x, index_type y, bool in_left_h
|
||||
}
|
||||
case EXTEND_LINE: {
|
||||
index_type top_line, bottom_line;
|
||||
if (start_extended_selection || y == self->selection.start.y) {
|
||||
if (start_extended_selection || y == s->start.y) {
|
||||
top_line = y; bottom_line = y;
|
||||
} else if (y < self->selection.start.y) {
|
||||
top_line = y; bottom_line = self->selection.start.y;
|
||||
a = &self->selection.end; b = &self->selection.start;
|
||||
} else if (y > self->selection.start.y) {
|
||||
bottom_line = y; top_line = self->selection.start.y;
|
||||
} else if (y < s->start.y) {
|
||||
top_line = y; bottom_line = s->start.y;
|
||||
a = &s->end; b = &s->start;
|
||||
} else if (y > s->start.y) {
|
||||
bottom_line = y; top_line = s->start.y;
|
||||
} else break;
|
||||
while (top_line > 0 && visual_line_(self, top_line)->continued) {
|
||||
if (!screen_selection_range_for_line(self, top_line - 1, &start.x, &end.x)) break;
|
||||
@@ -2372,10 +2569,10 @@ screen_update_selection(Screen *self, index_type x, index_type y, bool in_left_h
|
||||
break;
|
||||
}
|
||||
case EXTEND_CELL:
|
||||
self->selection.end.x = x; self->selection.end.y = y; self->selection.end.in_left_half_of_cell = in_left_half_of_cell;
|
||||
s->end.x = x; s->end.y = y; s->end.in_left_half_of_cell = in_left_half_of_cell;
|
||||
break;
|
||||
}
|
||||
if (!self->selection.in_progress) call_boss(set_primary_selection, NULL);
|
||||
if (!self->selections.in_progress) call_boss(set_primary_selection, NULL);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
@@ -2520,6 +2717,30 @@ paste_bytes(Screen *self, PyObject *bytes) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
focus_changed(Screen *self, PyObject *has_focus_) {
|
||||
bool previous = self->has_focus;
|
||||
bool has_focus = PyObject_IsTrue(has_focus_) ? true : false;
|
||||
if (has_focus != previous) {
|
||||
self->has_focus = has_focus;
|
||||
if (has_focus) self->has_activity_since_last_focus = false;
|
||||
if (self->modes.mFOCUS_TRACKING) write_escape_code_to_child(self, CSI, has_focus ? "I" : "O");
|
||||
Py_RETURN_TRUE;
|
||||
}
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
has_focus(Screen *self, PyObject *args UNUSED) {
|
||||
if (self->has_focus) Py_RETURN_TRUE;
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
has_activity_since_last_focus(Screen *self, PyObject *args UNUSED) {
|
||||
if (self->has_activity_since_last_focus) Py_RETURN_TRUE;
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
|
||||
WRAP2(cursor_position, 1, 1)
|
||||
|
||||
@@ -2534,11 +2755,6 @@ COUNT_WRAP(cursor_down)
|
||||
COUNT_WRAP(cursor_down1)
|
||||
COUNT_WRAP(cursor_forward)
|
||||
|
||||
static PyObject*
|
||||
wcwidth_wrap(PyObject UNUSED *self, PyObject *chr) {
|
||||
return PyLong_FromLong(wcwidth_std(PyLong_AsLong(chr)));
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
screen_is_emoji_presentation_base(PyObject UNUSED *self, PyObject *code_) {
|
||||
unsigned long code = PyLong_AsUnsignedLong(code_);
|
||||
@@ -2546,6 +2762,17 @@ screen_is_emoji_presentation_base(PyObject UNUSED *self, PyObject *code_) {
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
hyperlink_at(Screen *self, PyObject *args) {
|
||||
unsigned int x, y;
|
||||
if (!PyArg_ParseTuple(args, "II", &x, &y)) return NULL;
|
||||
screen_mark_hyperlink(self, x, y);
|
||||
if (!self->url_ranges.count) Py_RETURN_NONE;
|
||||
hyperlink_id_type hid = hyperlink_id_for_range(self, self->url_ranges.items);
|
||||
if (!hid) Py_RETURN_NONE;
|
||||
const char *url = get_hyperlink_for_id(self->hyperlink_pool, hid, true);
|
||||
return Py_BuildValue("s", url);
|
||||
}
|
||||
|
||||
#define MND(name, args) {#name, (PyCFunction)name, args, #name},
|
||||
#define MODEFUNC(name) MND(name, METH_NOARGS) MND(set_##name, METH_O)
|
||||
@@ -2553,6 +2780,7 @@ screen_is_emoji_presentation_base(PyObject UNUSED *self, PyObject *code_) {
|
||||
static PyMethodDef methods[] = {
|
||||
MND(line, METH_O)
|
||||
MND(visual_line, METH_VARARGS)
|
||||
MND(current_url_text, METH_NOARGS)
|
||||
MND(draw, METH_O)
|
||||
MND(cursor_position, METH_VARARGS)
|
||||
MND(set_mode, METH_VARARGS)
|
||||
@@ -2565,6 +2793,9 @@ static PyMethodDef methods[] = {
|
||||
MND(erase_in_line, METH_VARARGS)
|
||||
MND(erase_in_display, METH_VARARGS)
|
||||
MND(scroll_until_cursor, METH_NOARGS)
|
||||
MND(hyperlinks_as_list, METH_NOARGS)
|
||||
MND(garbage_collect_hyperlink_pool, METH_NOARGS)
|
||||
MND(hyperlink_for_id, METH_O)
|
||||
METHOD(current_char_width, METH_NOARGS)
|
||||
MND(insert_lines, METH_VARARGS)
|
||||
MND(delete_lines, METH_VARARGS)
|
||||
@@ -2589,7 +2820,7 @@ static PyMethodDef methods[] = {
|
||||
MND(clear_tab_stop, METH_VARARGS)
|
||||
MND(start_selection, METH_VARARGS)
|
||||
MND(update_selection, METH_VARARGS)
|
||||
MND(clear_selection, METH_NOARGS)
|
||||
{"clear_selection", (PyCFunction)clear_selection_, METH_NOARGS, ""},
|
||||
MND(reverse_index, METH_NOARGS)
|
||||
MND(mark_as_dirty, METH_NOARGS)
|
||||
MND(resize, METH_VARARGS)
|
||||
@@ -2599,10 +2830,14 @@ static PyMethodDef methods[] = {
|
||||
MND(is_rectangle_select, METH_NOARGS)
|
||||
MND(scroll, METH_VARARGS)
|
||||
MND(send_escape_code_to_child, METH_VARARGS)
|
||||
MND(hyperlink_at, METH_VARARGS)
|
||||
MND(toggle_alt_screen, METH_NOARGS)
|
||||
MND(reset_callbacks, METH_NOARGS)
|
||||
MND(paste, METH_O)
|
||||
MND(paste_bytes, METH_O)
|
||||
MND(focus_changed, METH_O)
|
||||
MND(has_focus, METH_NOARGS)
|
||||
MND(has_activity_since_last_focus, METH_NOARGS)
|
||||
MND(copy_colors_from, METH_O)
|
||||
MND(set_marker, METH_VARARGS)
|
||||
MND(marked_cells, METH_NOARGS)
|
||||
@@ -2662,8 +2897,6 @@ PyTypeObject Screen_Type = {
|
||||
};
|
||||
|
||||
static PyMethodDef module_methods[] = {
|
||||
{"wcwidth", (PyCFunction)wcwidth_wrap, METH_O, ""},
|
||||
{"wcswidth", (PyCFunction)screen_wcswidth, METH_O, ""},
|
||||
{"is_emoji_presentation_base", (PyCFunction)screen_is_emoji_presentation_base, METH_O, ""},
|
||||
{"truncate_point_for_length", (PyCFunction)screen_truncate_point_for_length, METH_VARARGS, ""},
|
||||
{NULL} /* Sentinel */
|
||||
|
||||
@@ -26,13 +26,30 @@ typedef struct {
|
||||
|
||||
typedef enum SelectionExtendModes { EXTEND_CELL, EXTEND_WORD, EXTEND_LINE } SelectionExtendMode;
|
||||
|
||||
typedef struct {
|
||||
index_type x, x_limit;
|
||||
} XRange;
|
||||
|
||||
typedef struct {
|
||||
int y, y_limit;
|
||||
XRange first, body, last;
|
||||
} IterationData;
|
||||
|
||||
typedef struct {
|
||||
SelectionBoundary start, end, input_start, input_current;
|
||||
unsigned int start_scrolled_by, end_scrolled_by;
|
||||
bool in_progress, rectangle_select;
|
||||
SelectionExtendMode extend_mode;
|
||||
bool rectangle_select;
|
||||
IterationData last_rendered;
|
||||
int sort_y, sort_x;
|
||||
} Selection;
|
||||
|
||||
typedef struct {
|
||||
Selection *items;
|
||||
size_t count, capacity, last_rendered_count;
|
||||
bool in_progress;
|
||||
SelectionExtendMode extend_mode;
|
||||
} Selections;
|
||||
|
||||
#define SAVEPOINTS_SZ 256
|
||||
|
||||
typedef struct {
|
||||
@@ -63,15 +80,6 @@ typedef struct {
|
||||
index_type xstart, ynum, xnum;
|
||||
} OverlayLine;
|
||||
|
||||
typedef struct {
|
||||
index_type x, x_limit;
|
||||
} XRange;
|
||||
|
||||
typedef struct {
|
||||
int y, y_limit;
|
||||
XRange first, body, last;
|
||||
} IterationData;
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
|
||||
@@ -82,9 +90,8 @@ typedef struct {
|
||||
id_type window_id;
|
||||
uint32_t utf8_state, utf8_codepoint, *g0_charset, *g1_charset, *g_charset;
|
||||
unsigned int current_charset;
|
||||
Selection selection, url_range;
|
||||
Selections selections, url_ranges;
|
||||
struct {
|
||||
IterationData selection, url;
|
||||
unsigned int cursor_x, cursor_y, scrolled_by;
|
||||
index_type lines, columns;
|
||||
} last_rendered;
|
||||
@@ -121,6 +128,11 @@ typedef struct {
|
||||
} pending_mode;
|
||||
DisableLigature disable_ligatures;
|
||||
PyObject *marker;
|
||||
bool has_focus;
|
||||
bool has_activity_since_last_focus;
|
||||
hyperlink_id_type active_hyperlink_id;
|
||||
HYPERLINK_POOL_HANDLE hyperlink_pool;
|
||||
ANSIBuf as_ansi_buf;
|
||||
} Screen;
|
||||
|
||||
|
||||
@@ -178,6 +190,7 @@ void screen_handle_print(Screen *, PyObject *cmd);
|
||||
void screen_designate_charset(Screen *, uint32_t which, uint32_t as);
|
||||
void screen_use_latin1(Screen *, bool);
|
||||
void set_title(Screen *self, PyObject*);
|
||||
void desktop_notify(Screen *self, unsigned int, PyObject*);
|
||||
void set_icon(Screen *self, PyObject*);
|
||||
void set_dynamic_color(Screen *self, unsigned int code, PyObject*);
|
||||
void clipboard_control(Screen *self, PyObject*);
|
||||
@@ -203,6 +216,8 @@ bool screen_history_scroll(Screen *self, int amt, bool upwards);
|
||||
Line* screen_visual_line(Screen *self, index_type y);
|
||||
unsigned long screen_current_char_width(Screen *self);
|
||||
void screen_mark_url(Screen *self, index_type start_x, index_type start_y, index_type end_x, index_type end_y);
|
||||
void set_active_hyperlink(Screen*, char*, char*);
|
||||
hyperlink_id_type screen_mark_hyperlink(Screen*, index_type, index_type);
|
||||
void screen_handle_graphics_command(Screen *self, const GraphicsCommand *cmd, const uint8_t *payload);
|
||||
bool screen_open_url(Screen*);
|
||||
void screen_dirty_sprite_positions(Screen *self);
|
||||
|
||||
@@ -4,36 +4,38 @@
|
||||
|
||||
import shlex
|
||||
import sys
|
||||
from typing import Generator, List, NamedTuple, Optional, Tuple, Union
|
||||
from typing import Generator, List, Optional, Union
|
||||
|
||||
from .cli_stub import CLIOptions
|
||||
from .config_data import to_layout_names
|
||||
from .constants import FloatEdges, kitty_exe
|
||||
from .constants import kitty_exe
|
||||
from .layout.interface import all_layouts
|
||||
from .options_stub import Options
|
||||
from .os_window_size import WindowSize, WindowSizeData, WindowSizes
|
||||
from .typing import SpecialWindowInstance
|
||||
from .utils import log_error, resolved_shell
|
||||
from .window import Watchers
|
||||
|
||||
|
||||
class WindowSizeOpts(NamedTuple):
|
||||
|
||||
initial_window_width: Tuple[int, str]
|
||||
initial_window_height: Tuple[int, str]
|
||||
window_margin_width: FloatEdges
|
||||
window_padding_width: FloatEdges
|
||||
remember_window_size: bool
|
||||
def get_os_window_sizing_data(opts: Options, session: Optional['Session'] = None) -> WindowSizeData:
|
||||
if session is None or session.os_window_size is None:
|
||||
sizes = WindowSizes(WindowSize(*opts.initial_window_width), WindowSize(*opts.initial_window_height))
|
||||
else:
|
||||
sizes = session.os_window_size
|
||||
return WindowSizeData(sizes, opts.remember_window_size, opts.single_window_margin_width, opts.window_margin_width, opts.window_padding_width)
|
||||
|
||||
|
||||
class Tab:
|
||||
|
||||
def __init__(self, opts: Options, name: str):
|
||||
self.windows: List[Union[List[str], 'SpecialWindowInstance']] = []
|
||||
def __init__(self, opts: Options, name: str, watchers: Watchers):
|
||||
self.windows: List['SpecialWindowInstance'] = []
|
||||
self.name = name.strip()
|
||||
self.active_window_idx = 0
|
||||
self.enabled_layouts = opts.enabled_layouts
|
||||
self.layout = (self.enabled_layouts or ['tall'])[0]
|
||||
self.cwd: Optional[str] = None
|
||||
self.next_title: Optional[str] = None
|
||||
self.watchers: Watchers = watchers.copy()
|
||||
|
||||
|
||||
class Session:
|
||||
@@ -42,13 +44,27 @@ class Session:
|
||||
self.tabs: List[Tab] = []
|
||||
self.active_tab_idx = 0
|
||||
self.default_title = default_title
|
||||
self.os_window_size: Optional[WindowSizeOpts] = None
|
||||
self.os_window_size: Optional[WindowSizes] = None
|
||||
self.os_window_class: Optional[str] = None
|
||||
self.watchers = Watchers()
|
||||
|
||||
def add_watchers_to_all_windows(self, watchers: Watchers) -> None:
|
||||
def add(w: 'SpecialWindowInstance') -> 'SpecialWindowInstance':
|
||||
if w.watchers is None:
|
||||
return w._replace(watchers=watchers)
|
||||
wt = w.watchers.copy()
|
||||
wt.add(watchers)
|
||||
return w._replace(watchers=wt)
|
||||
|
||||
for tab in self.tabs:
|
||||
tab.windows = [add(w) for w in tab.windows]
|
||||
|
||||
def add_tab(self, opts: Options, name: str = '') -> None:
|
||||
if self.tabs and not self.tabs[-1].windows:
|
||||
del self.tabs[-1]
|
||||
self.tabs.append(Tab(opts, name))
|
||||
if self.tabs:
|
||||
self.tabs[-1].watchers = self.watchers.copy()
|
||||
self.tabs.append(Tab(opts, name, self.watchers))
|
||||
|
||||
def set_next_title(self, title: str) -> None:
|
||||
self.tabs[-1].next_title = title.strip()
|
||||
@@ -65,10 +81,16 @@ class Session:
|
||||
cmd = None
|
||||
from .tabs import SpecialWindow
|
||||
t = self.tabs[-1]
|
||||
t.windows.append(SpecialWindow(cmd, cwd=t.cwd, override_title=t.next_title or self.default_title))
|
||||
watchers: Optional[Watchers] = None
|
||||
if self.watchers.has_watchers:
|
||||
watchers = self.watchers.copy()
|
||||
sw = SpecialWindow(cmd, cwd=t.cwd, override_title=t.next_title or self.default_title, watchers=watchers)
|
||||
t.windows.append(sw)
|
||||
t.next_title = None
|
||||
|
||||
def add_special_window(self, sw: 'SpecialWindowInstance') -> None:
|
||||
if self.watchers.has_watchers:
|
||||
sw = sw._replace(watchers=self.watchers.copy())
|
||||
self.tabs[-1].windows.append(sw)
|
||||
|
||||
def focus(self) -> None:
|
||||
@@ -87,9 +109,13 @@ class Session:
|
||||
def parse_session(raw: str, opts: Options, default_title: Optional[str] = None) -> Generator[Session, None, None]:
|
||||
|
||||
def finalize_session(ans: Session) -> Session:
|
||||
from .tabs import SpecialWindow
|
||||
for t in ans.tabs:
|
||||
if not t.windows:
|
||||
t.windows.append(resolved_shell(opts))
|
||||
w: Optional[Watchers] = None
|
||||
if t.watchers.has_watchers:
|
||||
w = t.watchers.copy()
|
||||
t.windows.append(SpecialWindow(cmd=resolved_shell(opts), watchers=w))
|
||||
return ans
|
||||
|
||||
ans = Session(default_title)
|
||||
@@ -107,7 +133,9 @@ def parse_session(raw: str, opts: Options, default_title: Optional[str] = None)
|
||||
ans.add_tab(opts, rest)
|
||||
elif cmd == 'new_os_window':
|
||||
yield finalize_session(ans)
|
||||
wt = ans.watchers
|
||||
ans = Session(default_title)
|
||||
ans.watchers = wt.copy()
|
||||
ans.add_tab(opts, rest)
|
||||
elif cmd == 'layout':
|
||||
ans.set_layout(rest)
|
||||
@@ -124,9 +152,17 @@ def parse_session(raw: str, opts: Options, default_title: Optional[str] = None)
|
||||
elif cmd == 'os_window_size':
|
||||
from kitty.config_data import window_size
|
||||
w, h = map(window_size, rest.split(maxsplit=1))
|
||||
ans.os_window_size = WindowSizeOpts(w, h, opts.window_margin_width, opts.window_padding_width, False)
|
||||
ans.os_window_size = WindowSizes(WindowSize(*w), WindowSize(*h))
|
||||
elif cmd == 'os_window_class':
|
||||
ans.os_window_class = rest
|
||||
elif cmd == 'watcher':
|
||||
from .launch import load_watch_modules
|
||||
if rest == 'clear':
|
||||
ans.watchers = Watchers()
|
||||
else:
|
||||
watchers = load_watch_modules((rest,))
|
||||
if watchers is not None:
|
||||
ans.watchers.add(watchers)
|
||||
else:
|
||||
raise ValueError('Unknown command in session file: {}'.format(cmd))
|
||||
yield finalize_session(ans)
|
||||
|
||||
@@ -78,8 +78,7 @@ class Completer:
|
||||
def __init__(self) -> None:
|
||||
self.matches: List[str] = []
|
||||
ddir = cache_dir()
|
||||
with suppress(FileExistsError):
|
||||
os.makedirs(ddir)
|
||||
os.makedirs(ddir, exist_ok=True)
|
||||
self.history_path = os.path.join(ddir, 'shell.history')
|
||||
|
||||
def complete(self, text: str, state: int) -> Optional[str]:
|
||||
|
||||
@@ -701,6 +701,7 @@ PYWRAP1(set_options) {
|
||||
S(force_ltr, PyObject_IsTrue);
|
||||
S(resize_draw_strategy, PyLong_AsLong);
|
||||
S(resize_in_steps, PyObject_IsTrue);
|
||||
S(allow_hyperlinks, PyObject_IsTrue);
|
||||
S(pointer_shape_when_grabbed, pointer_shape);
|
||||
|
||||
GA(tab_bar_style);
|
||||
|
||||
@@ -63,6 +63,7 @@ typedef struct {
|
||||
bool close_on_child_death;
|
||||
bool window_alert_on_bell;
|
||||
bool debug_keyboard;
|
||||
bool allow_hyperlinks;
|
||||
monotonic_t resize_debounce_time;
|
||||
MouseShape pointer_shape_when_grabbed;
|
||||
struct {
|
||||
|
||||
@@ -24,6 +24,7 @@ class TabBarData(NamedTuple):
|
||||
needs_attention: bool
|
||||
num_windows: int
|
||||
layout_name: str
|
||||
has_activity_since_last_focus: bool
|
||||
|
||||
|
||||
class DrawData(NamedTuple):
|
||||
@@ -40,6 +41,7 @@ class DrawData(NamedTuple):
|
||||
default_bg: Color
|
||||
title_template: str
|
||||
active_title_template: Optional[str]
|
||||
tab_activity_symbol: Optional[str]
|
||||
|
||||
|
||||
def as_rgb(x: int) -> int:
|
||||
@@ -65,6 +67,12 @@ def draw_title(draw_data: DrawData, screen: Screen, tab: TabBarData, index: int)
|
||||
screen.cursor.fg = draw_data.bell_fg
|
||||
screen.draw('🔔 ')
|
||||
screen.cursor.fg = fg
|
||||
if tab.has_activity_since_last_focus and draw_data.tab_activity_symbol:
|
||||
fg = screen.cursor.fg
|
||||
screen.cursor.fg = draw_data.bell_fg
|
||||
screen.draw(draw_data.tab_activity_symbol)
|
||||
screen.cursor.fg = fg
|
||||
|
||||
template = draw_data.title_template
|
||||
if tab.is_active and draw_data.active_title_template is not None:
|
||||
template = draw_data.active_title_template
|
||||
@@ -226,7 +234,8 @@ class TabBar:
|
||||
self.opts.tab_fade, self.opts.active_tab_foreground, self.opts.active_tab_background,
|
||||
self.opts.inactive_tab_foreground, self.opts.inactive_tab_background,
|
||||
self.opts.tab_bar_background or self.opts.background, self.opts.tab_title_template,
|
||||
self.opts.active_tab_title_template
|
||||
self.opts.active_tab_title_template,
|
||||
self.opts.tab_activity_symbol
|
||||
)
|
||||
if self.opts.tab_bar_style == 'separator':
|
||||
self.draw_func = draw_tab_with_separator
|
||||
|
||||
@@ -48,6 +48,7 @@ class SpecialWindowInstance(NamedTuple):
|
||||
cwd: Optional[str]
|
||||
overlay_for: Optional[int]
|
||||
env: Optional[Dict[str, str]]
|
||||
watchers: Optional[Watchers]
|
||||
|
||||
|
||||
def SpecialWindow(
|
||||
@@ -57,9 +58,10 @@ def SpecialWindow(
|
||||
cwd_from: Optional[int] = None,
|
||||
cwd: Optional[str] = None,
|
||||
overlay_for: Optional[int] = None,
|
||||
env: Optional[Dict[str, str]] = None
|
||||
env: Optional[Dict[str, str]] = None,
|
||||
watchers: Optional[Watchers] = None
|
||||
) -> SpecialWindowInstance:
|
||||
return SpecialWindowInstance(cmd, stdin, override_title, cwd_from, cwd, overlay_for, env)
|
||||
return SpecialWindowInstance(cmd, stdin, override_title, cwd_from, cwd, overlay_for, env, watchers)
|
||||
|
||||
|
||||
def add_active_id_to_history(items: Deque[int], item_id: int, maxlen: int = 64) -> None:
|
||||
@@ -135,10 +137,7 @@ class Tab: # {{{
|
||||
|
||||
def startup(self, session_tab: 'SessionTab') -> None:
|
||||
for cmd in session_tab.windows:
|
||||
if isinstance(cmd, (SpecialWindowInstance,)):
|
||||
self.new_special_window(cmd)
|
||||
else:
|
||||
self.new_window(cmd=cmd)
|
||||
self.new_special_window(cmd)
|
||||
self.windows.set_active_window_group_for(self.windows.all_windows[session_tab.active_window_idx])
|
||||
|
||||
def serialize_state(self) -> Dict[str, Any]:
|
||||
@@ -333,14 +332,14 @@ class Tab: # {{{
|
||||
special_window: SpecialWindowInstance,
|
||||
location: Optional[str] = None,
|
||||
copy_colors_from: Optional[Window] = None,
|
||||
allow_remote_control: bool = False
|
||||
allow_remote_control: bool = False,
|
||||
) -> Window:
|
||||
return self.new_window(
|
||||
use_shell=False, cmd=special_window.cmd, stdin=special_window.stdin,
|
||||
override_title=special_window.override_title,
|
||||
cwd_from=special_window.cwd_from, cwd=special_window.cwd, overlay_for=special_window.overlay_for,
|
||||
env=special_window.env, location=location, copy_colors_from=copy_colors_from,
|
||||
allow_remote_control=allow_remote_control
|
||||
allow_remote_control=allow_remote_control, watchers=special_window.watchers
|
||||
)
|
||||
|
||||
def close_window(self) -> None:
|
||||
@@ -440,9 +439,9 @@ class Tab: # {{{
|
||||
self.relayout()
|
||||
|
||||
def move_window_to_top(self) -> None:
|
||||
n = self.windows.num_groups
|
||||
if n > 1:
|
||||
self.move_window(1 - n)
|
||||
n = self.windows.active_group_idx
|
||||
if n > 0:
|
||||
self.move_window(-n)
|
||||
|
||||
def move_window_forward(self) -> None:
|
||||
self.move_window()
|
||||
@@ -737,13 +736,16 @@ class TabManager: # {{{
|
||||
for t in self.tabs:
|
||||
title = (t.name or t.title or appname).strip()
|
||||
needs_attention = False
|
||||
has_activity_since_last_focus = False
|
||||
for w in t:
|
||||
if w.needs_attention:
|
||||
needs_attention = True
|
||||
break
|
||||
if w.has_activity_since_last_focus:
|
||||
has_activity_since_last_focus = True
|
||||
ans.append(TabBarData(
|
||||
title, t is at, needs_attention,
|
||||
len(t), t.current_layout.name or ''
|
||||
len(t), t.current_layout.name or '',
|
||||
has_activity_since_last_focus
|
||||
))
|
||||
return ans
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user