Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
627b91ffba | ||
|
|
9ad65f2e1d | ||
|
|
a6a606ff3d | ||
|
|
63317f9c78 | ||
|
|
46d9dfb614 | ||
|
|
be3b33af06 | ||
|
|
04e1b8b1ae | ||
|
|
213768b6e0 | ||
|
|
aff1abdb26 | ||
|
|
44a413df81 | ||
|
|
4f12c91fff | ||
|
|
b6188bf436 | ||
|
|
b9a14e0e4a | ||
|
|
6882c23f43 | ||
|
|
80301d465b | ||
|
|
32632264ee | ||
|
|
409bd37db5 | ||
|
|
5fd4bc965f | ||
|
|
e86e5ce1d8 | ||
|
|
5faa649452 | ||
|
|
2ddc7e8c80 | ||
|
|
ed700ff830 | ||
|
|
33ed873997 | ||
|
|
9125c00f96 | ||
|
|
73fe3293be | ||
|
|
804c4fbe19 | ||
|
|
d6c82ba21c | ||
|
|
67308f2ca1 | ||
|
|
4a17a946fb | ||
|
|
1ae7ae4a1d | ||
|
|
404ca23de3 | ||
|
|
d385952fef | ||
|
|
f03cac8a9d | ||
|
|
4b501fd694 | ||
|
|
872d39eb2c | ||
|
|
0341cae922 | ||
|
|
5357a9a1cc | ||
|
|
3695538a11 | ||
|
|
7780d52930 | ||
|
|
6a23b4ee47 | ||
|
|
f48b770036 | ||
|
|
81b5d2c4f2 | ||
|
|
d43d6ab0a9 | ||
|
|
5a4e883360 | ||
|
|
eb93cd4048 | ||
|
|
d83e3fddb0 | ||
|
|
80ca25e879 | ||
|
|
c86b34aacd | ||
|
|
8da426039b | ||
|
|
9184e9a28f | ||
|
|
578d537d5f | ||
|
|
236c92b2b4 | ||
|
|
66a46279f4 | ||
|
|
eb35edbe20 | ||
|
|
7cf2404331 | ||
|
|
9ccc6bf835 | ||
|
|
025a46bb35 | ||
|
|
677e8a1ae9 | ||
|
|
03702772d3 | ||
|
|
3cd744b247 | ||
|
|
88523b2692 | ||
|
|
dcb39eb210 | ||
|
|
102823809b | ||
|
|
23f2b29069 | ||
|
|
7b33a87725 | ||
|
|
6dde573ed9 | ||
|
|
566e641be1 | ||
|
|
08d5448523 | ||
|
|
c11a20f1ec | ||
|
|
e5b9bd2e5a | ||
|
|
6ad49bd7fb | ||
|
|
5d082d8d5a | ||
|
|
977575da73 | ||
|
|
bced6f31b1 | ||
|
|
6bf04e22cc | ||
|
|
2f9784809d | ||
|
|
3e85497c2c | ||
|
|
3b551a1dee | ||
|
|
d2fdb1c1eb | ||
|
|
db92af68e9 | ||
|
|
815087460c | ||
|
|
b4cb9d7c4d | ||
|
|
c6d10c6a23 | ||
|
|
464c7a80e2 | ||
|
|
4f56cce727 | ||
|
|
7dea6e0ef4 | ||
|
|
8c143db381 | ||
|
|
0cea6d5a1b | ||
|
|
eff881f4bb | ||
|
|
c64fd1760c | ||
|
|
9e361700d2 | ||
|
|
1f976644bf | ||
|
|
f3cb68ee40 | ||
|
|
85a3da057f | ||
|
|
c551384369 | ||
|
|
ce230b071b | ||
|
|
13d5f04ad9 | ||
|
|
3b861de3d2 | ||
|
|
1ef010689d | ||
|
|
3523ab283f | ||
|
|
5b2a9b101d | ||
|
|
fdcfcfa1b9 | ||
|
|
88e9c21a3b | ||
|
|
973352a98d | ||
|
|
f06f871dfc | ||
|
|
e03c713294 | ||
|
|
aa107956f9 | ||
|
|
d725578e1b | ||
|
|
239eb8202b | ||
|
|
58d7439719 | ||
|
|
cc145ec3dd | ||
|
|
847ce37160 | ||
|
|
0305009766 | ||
|
|
fe1b056371 | ||
|
|
0c4804db79 | ||
|
|
f7e84df332 | ||
|
|
bd654d22f4 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -3,6 +3,7 @@ kitty/emoji.h linguist-generated=true
|
||||
kitty/keys.h linguist-generated=true
|
||||
kitty/charsets.c linguist-generated=true
|
||||
kitty/key_encoding.py linguist-generated=true
|
||||
kitty/unicode-data.c
|
||||
kitty/rgb.py linguist-generated=true
|
||||
kitty/gl-wrapper.* linguist-generated=true
|
||||
kitty/khrplatform.h linguist-generated=true
|
||||
|
||||
@@ -95,7 +95,7 @@ install: |
|
||||
if [[ "$USE_BREW" == "1" ]]; then
|
||||
brew update;
|
||||
brew install python3;
|
||||
brew install harfbuzz --without-glib --without-gobject-introspection --without-graphite2 --without-icu4c --without-freetype --without-cairo;
|
||||
brew install harfbuzz --without-glib --without-gobject-introspection --without-graphite2 --without-icu4c --without-freetype;
|
||||
else
|
||||
mkdir -p $SW;
|
||||
curl https://download.calibre-ebook.com/travis/kitty/osx.tar.xz | tar xJ -C $SW;
|
||||
|
||||
@@ -3,6 +3,51 @@ Changelog
|
||||
|
||||
kitty is a feature full, cross-platform, *fast*, GPU based terminal emulator.
|
||||
|
||||
version 0.7.0 [2018-01-24]
|
||||
---------------------------
|
||||
|
||||
- Allow controlling kitty from the shell prompt/scripts. You can
|
||||
open/close/rename windows and tabs and even send input to specific windows.
|
||||
See the README for details.
|
||||
|
||||
- Add option to put tab bar at the top instead of the bottom
|
||||
|
||||
- Add option to override the default shell
|
||||
|
||||
- Add "Horizontal" and "Vertical" window layouts
|
||||
|
||||
- Sessions: Allow setting titles and working directories for individual windows
|
||||
|
||||
- Option to copy to clipboard on mouse select
|
||||
|
||||
- Fix incorrect reporting of mouse move events when using the SGR protocol
|
||||
|
||||
- Make alt+backspace delete the previous word
|
||||
|
||||
- Take the mouse wheel multiplier option in to account when generating fake key
|
||||
scroll events
|
||||
|
||||
- macOS: Fix closing top-level window does not transfer focus to other
|
||||
top-level windows.
|
||||
|
||||
- macOS: Fix alt+arrow keys not working when disabling the macos_option_as_alt
|
||||
config option.
|
||||
|
||||
- kitty icat: Workaround for bug in ImageMagick that would cause some images
|
||||
to fail to display at certain sizes.
|
||||
|
||||
- Fix rendering of text with ligature fonts that do not use dummy glyphs
|
||||
|
||||
- Fix a regression that caused copying of the selection to clipboard to only
|
||||
copy the visible part of the selection
|
||||
|
||||
- Fix incorrect handling of some unicode combining marks that are not re-ordered
|
||||
|
||||
- Fix handling on non-BMP combining characters
|
||||
|
||||
- Drop the dependency on libunistring
|
||||
|
||||
|
||||
version 0.6.1 [2017-12-28]
|
||||
---------------------------
|
||||
|
||||
|
||||
8
Makefile
8
Makefile
@@ -21,3 +21,11 @@ debug:
|
||||
# Build with the ASAN and UBSAN sanitizers
|
||||
asan:
|
||||
python3 setup.py build $(VVAL) --debug --sanitize
|
||||
|
||||
logo/kitty.iconset/icon_256x256.png: logo/kitty.svg logo/make.py
|
||||
logo/make.py
|
||||
|
||||
rendered_logo: logo/kitty.iconset/icon_256x256.png
|
||||
|
||||
app: rendered_logo
|
||||
python3 setup.py app $(VVAL)
|
||||
|
||||
@@ -51,19 +51,21 @@ image::https://travis-ci.org/kovidgoyal/kitty.svg?branch=master[Build status, li
|
||||
|
||||
== Major Features
|
||||
|
||||
* Supports tiling multiple terminal windows side by side in different
|
||||
layouts without needing to use an extra program like tmux
|
||||
* Uses OpenGL for rendering, offloads rendering to the GPU for lower system
|
||||
load and buttery smooth scrolling. Uses threaded rendering to minimize input
|
||||
latency.
|
||||
|
||||
* Supports all modern terminal features: graphics (images), unicode,
|
||||
true-color, OpenType ligatures, mouse protocol, focus tracking, bracketed paste and so on.
|
||||
|
||||
* Supports tiling multiple terminal windows side by side in different
|
||||
layouts without needing to use an extra program like tmux
|
||||
|
||||
* Can be controlled from scripts or the shell prompt, even over SSH.
|
||||
|
||||
* Supports startup sessions which allow you to specify the window/tab layout,
|
||||
working directories and programs to run on startup.
|
||||
|
||||
* Uses OpenGL for rendering, offloads rendering to the GPU for lower system
|
||||
load and buttery smooth scrolling. Uses threaded rendering to minimize input
|
||||
latency.
|
||||
|
||||
* Cross-platform support: kitty currently works on Linux and macOS, but because
|
||||
it uses only OpenGL for rendering, it should be trivial to port to other
|
||||
platforms.
|
||||
@@ -73,8 +75,6 @@ window using arbitrary programs of your choice. This is useful for browsing
|
||||
the history comfortably in a pager or editor and also for doing things like
|
||||
opening URLs using only the keyboard.
|
||||
|
||||
* Easily hackable (UI layer written in python, inner loops in C for
|
||||
speed). Less than fifteen thousand lines of code.
|
||||
|
||||
image::screenshot.png?raw=true[Screenshot, showing three programs in the "Tall" layout]
|
||||
|
||||
@@ -90,7 +90,6 @@ the following dependencies are installed first.
|
||||
|
||||
* python >= 3.5
|
||||
* harfbuzz >= 1.5.0
|
||||
* libunistring
|
||||
* zlib
|
||||
* libpng
|
||||
* freetype (not needed on macOS)
|
||||
@@ -98,6 +97,7 @@ the following dependencies are installed first.
|
||||
* ImageMagick (optional, needed to use the `kitty icat` tool to display images in the terminal)
|
||||
* gcc or clang (required only for building)
|
||||
* pkg-config (required only for building)
|
||||
* For building on Linux in addition to the above dependencies you might also need to install the `-dev` packages for `xcursor`, `xrandr`, `xinerama` and `xkbcommon-x11`, if they are not already installed by your distro.
|
||||
|
||||
=== Install and run from source
|
||||
|
||||
@@ -230,6 +230,15 @@ windows are:
|
||||
|===
|
||||
|
||||
|
||||
== Controlling kitty from scripts
|
||||
|
||||
You can control kitty from the command line/scripts by sending it messages.
|
||||
You can tell kitty to open/close/rename tabs and windows. You can even send
|
||||
arbitrary input/text to any specified window. Messages can be sent using
|
||||
`kitty @`. Note that you must set `allow_remote_control yes` in your
|
||||
kitty.conf to use this feature. It even works over SSH connections. This
|
||||
feature is best illustrated with a link:remote-control.asciidoc[tutorial].
|
||||
|
||||
|
||||
== The scrollback buffer
|
||||
|
||||
@@ -256,8 +265,10 @@ hold down `ctrl+shift` and click on a URL to open it in a browser.
|
||||
Currently, there are three layouts available,
|
||||
|
||||
* Stack -- Only a single maximized window is shown at a time
|
||||
* Tall -- One window is shown full height on the left, the rest of the windows are shown one below the other on the right.
|
||||
* Tall -- One window is shown full height on the left, the rest of the windows are shown one below the other on the right
|
||||
* Grid -- All windows are shown in a grid
|
||||
* Horizontal -- All windows are shown side-by-side
|
||||
* Vertical -- All windows are shown one below the other
|
||||
|
||||
You can switch between layouts using the {sc_next_layout} key combination.
|
||||
|
||||
@@ -285,11 +296,13 @@ command line flag. For example:
|
||||
....
|
||||
# Set the window layout for the current tab
|
||||
layout tall
|
||||
# Set the working directory for the current tab
|
||||
# Set the working directory for windows in the current tab
|
||||
cd ~
|
||||
# Create a window and run the specified command in it
|
||||
launch zsh
|
||||
launch vim
|
||||
# Set the title for the next window
|
||||
title Chat with x
|
||||
launch irssi --profile x
|
||||
|
||||
# Create a new tab (the part after new_tab is the optional tab name which will
|
||||
@@ -419,33 +432,21 @@ the approach using ligatures.
|
||||
|
||||
=== How do I build kitty.app on macOS?
|
||||
|
||||
You dont need to build kitty.app to run kitty from source. Simply install the dependencies
|
||||
listed above using brew/MacPorts or similar and then compile and run with:
|
||||
Install `optipng` and `librsvg` using `brew` or similar (needed for the logo generation step).
|
||||
|
||||
And run:
|
||||
```
|
||||
make
|
||||
python3 .
|
||||
make app
|
||||
```
|
||||
|
||||
You can also create a shell script to run kitty:
|
||||
This `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.
|
||||
|
||||
```
|
||||
#!/bin/sh
|
||||
exec python3 /path/to/kitty/source/dir
|
||||
```
|
||||
|
||||
And using something like link:https://mathiasbynens.be/notes/shell-script-mac-apps[app-wrapper-for-shell-script]
|
||||
you can create a simple launcher app to run the shell script.
|
||||
|
||||
If you only want to make some small changes to a python file, you can do that
|
||||
and simply replace the corresponding file inside an existing kitty.app. (Note
|
||||
that kitty.app will contain `.pyo` files which are compiled versions of `.py`
|
||||
files, delete the `.pyo` file and replace it with a `.py` file).
|
||||
|
||||
Finally, note that the released kitty.dmg is built automatically by using the
|
||||
`kitty` branch of
|
||||
Note that the released kitty.dmg includes all dependencies, unlike the
|
||||
`kitty.app` built above and is built automatically by using the `kitty` branch of
|
||||
link:https://github.com/kovidgoyal/build-calibre[build-calibre] however, that
|
||||
is designed to run on linux and is not for the faint of heart.
|
||||
is designed to run on Linux and is not for the faint of heart.
|
||||
|
||||
== A tribute
|
||||
|
||||
|
||||
52
__main__.py
52
__main__.py
@@ -4,12 +4,52 @@
|
||||
|
||||
import sys
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == 'icat':
|
||||
|
||||
def icat(args):
|
||||
from kitty.icat import main
|
||||
main(sys.argv[1:])
|
||||
elif len(sys.argv) > 1 and sys.argv[1] == 'list-fonts':
|
||||
main(args)
|
||||
|
||||
|
||||
def list_fonts(args):
|
||||
from kitty.fonts.list import main
|
||||
main(sys.argv[1:])
|
||||
else:
|
||||
from kitty.main import main
|
||||
main(args)
|
||||
|
||||
|
||||
def remote_control(args):
|
||||
from kitty.remote_control import main
|
||||
main(args)
|
||||
|
||||
|
||||
def namespaced(args):
|
||||
func = namespaced_entry_points[args[1]]
|
||||
func(args[1:])
|
||||
|
||||
|
||||
entry_points = {
|
||||
# These two are here for backwards compat
|
||||
'icat': icat,
|
||||
'list-fonts': list_fonts,
|
||||
|
||||
'@': remote_control,
|
||||
'+': namespaced,
|
||||
}
|
||||
namespaced_entry_points = {k: v for k, v in entry_points.items() if k[0] not in '+@'}
|
||||
|
||||
|
||||
def main():
|
||||
first_arg = '' if len(sys.argv) < 2 else sys.argv[1]
|
||||
func = entry_points.get(first_arg)
|
||||
if func is None:
|
||||
if first_arg.startswith('@'):
|
||||
remote_control(['@', first_arg[1:]] + sys.argv[2:])
|
||||
elif first_arg.startswith('+'):
|
||||
namespaced(['+', first_arg[1:]] + sys.argv[2:])
|
||||
else:
|
||||
from kitty.main import main
|
||||
main()
|
||||
else:
|
||||
func(sys.argv[1:])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#!/bin/bash
|
||||
cloc --exclude-list-file <(echo -e 'kitty/wcwidth-std.h\nkitty/glfw.c\nkitty/keys.h\nkitty/charsets.c\nkitty/key_encoding.py\nkitty/rgb.py\nkitty/gl.h\nkitty/gl-wrapper.h\nkitty/gl-wrapper.c\nkitty/khrplatform.h\nkitty/glfw-wrapper.h\nkitty/glfw-wrapper.c\nkitty/emoji.h') kitty
|
||||
cloc --exclude-list-file <(echo -e 'kitty/wcwidth-std.h\nkitty/glfw.c\nkitty/keys.h\nkitty/charsets.c\nkitty/unicode-data.c\nkitty/key_encoding.py\nkitty/rgb.py\nkitty/gl.h\nkitty/gl-wrapper.h\nkitty/gl-wrapper.c\nkitty/khrplatform.h\nkitty/glfw-wrapper.h\nkitty/glfw-wrapper.c\nkitty/emoji.h') kitty
|
||||
|
||||
@@ -34,7 +34,7 @@ def get_data(fname, folder='UCD'):
|
||||
|
||||
# Map of class names to set of codepoints in class
|
||||
class_maps = {}
|
||||
combining_codepoints = set()
|
||||
marks = set()
|
||||
not_assigned = set(range(0, sys.maxunicode))
|
||||
|
||||
|
||||
@@ -57,9 +57,8 @@ def parse_ucd():
|
||||
for codepoint in codepoints:
|
||||
s.add(codepoint)
|
||||
not_assigned.discard(codepoint)
|
||||
cc = parts[3]
|
||||
if cc and cc != '0':
|
||||
combining_codepoints.add(codepoint)
|
||||
if category.startswith('M'):
|
||||
marks.add(codepoint)
|
||||
|
||||
|
||||
def split_two(line):
|
||||
@@ -131,7 +130,8 @@ def create_header(path):
|
||||
p = partial(print, file=f)
|
||||
p('// unicode data, built from the unicode standard on:', date.today())
|
||||
p('// see gen-wcwidth.py')
|
||||
p('#pragma once')
|
||||
if path.endswith('.h'):
|
||||
p('#pragma once')
|
||||
p('#include "data-types.h"\n')
|
||||
p('START_ALLOW_CASE_RANGE')
|
||||
p()
|
||||
@@ -161,13 +161,56 @@ def gen_emoji():
|
||||
p('\treturn false;\n}')
|
||||
|
||||
|
||||
def category_test(name, p, classes, comment, static=False):
|
||||
static = 'static inline ' if static else ''
|
||||
chars = set()
|
||||
for c in classes:
|
||||
chars |= class_maps[c]
|
||||
p(f'{static}bool\n{name}(char_type code) {{')
|
||||
p(f'\t// {comment} ({len(chars)} codepoints)' + ' {{' '{')
|
||||
p('\tswitch(code) {')
|
||||
for spec in get_ranges(list(chars)):
|
||||
write_case(spec, p)
|
||||
p(f'\t\t\treturn true;')
|
||||
p('\t} // }}}\n')
|
||||
p('\treturn false;\n}\n')
|
||||
|
||||
|
||||
def gen_ucd():
|
||||
with create_header('kitty/unicode-data.c') as p:
|
||||
p('#include "unicode-data.h"')
|
||||
category_test('is_combining_char', p, {c for c in class_maps if c.startswith('M')}, 'M category (marks)')
|
||||
category_test('is_ignored_char', p, 'Cc Cf Cs'.split(), 'Control characters (Cc Cf Cs)')
|
||||
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, {c for c in class_maps if c[0] in 'CZ'}, 'C and Z categories')
|
||||
category_test('is_P_category', p, {c for c in class_maps if c[0] == 'P'}, 'P category (punctuation)')
|
||||
mark_map = [0] + list(sorted(marks))
|
||||
p('char_type codepoint_for_mark(combining_type m) {')
|
||||
p(f'\tstatic char_type map[{len(mark_map)}] =', '{', ', '.join(map(str, mark_map)), '}; // {{{ mapping }}}')
|
||||
p('\tif (m < arraysz(map)) return map[m];')
|
||||
p('\treturn 0;')
|
||||
p('}\n')
|
||||
p('combining_type mark_for_codepoint(char_type c) {')
|
||||
p('\tswitch(c) { // {{{')
|
||||
rmap = {c: m for m, c in enumerate(mark_map)}
|
||||
for spec in get_ranges(mark_map):
|
||||
if isinstance(spec, tuple):
|
||||
s = rmap[spec[0]]
|
||||
p(f'\t\tcase {spec[0]} ... {spec[1]}: return {s} + c - {spec[0]};')
|
||||
else:
|
||||
p(f'\t\tcase {spec}: return {rmap[spec]};')
|
||||
p('default: return 0;')
|
||||
p('\t} // }}}')
|
||||
p('}\n')
|
||||
|
||||
|
||||
def gen_wcwidth():
|
||||
seen = set()
|
||||
|
||||
def add(p, comment, chars_, ret):
|
||||
chars = chars_ - seen
|
||||
seen.update(chars)
|
||||
p(f'\t\t// {comment} ({len(chars)} codepoints)' + ' {{{')
|
||||
p(f'\t\t// {comment} ({len(chars)} codepoints)' + ' {{' '{')
|
||||
for spec in get_ranges(list(chars)):
|
||||
write_case(spec, p)
|
||||
p(f'\t\t\treturn {ret};')
|
||||
@@ -179,7 +222,7 @@ def gen_wcwidth():
|
||||
|
||||
non_printing = class_maps['Cc'] | class_maps['Cf'] | class_maps['Cs']
|
||||
add(p, 'Non-printing characters', non_printing, -1)
|
||||
add(p, 'Combining characters', combining_codepoints, -1)
|
||||
add(p, 'Marks', marks, -1)
|
||||
add(p, 'Private use', class_maps['Co'], -3)
|
||||
add(p, 'East Asian ambiguous width', ambiguous, -2)
|
||||
add(p, 'East Asian double width', doublewidth, 2)
|
||||
@@ -195,5 +238,6 @@ def gen_wcwidth():
|
||||
parse_ucd()
|
||||
parse_emoji()
|
||||
parse_eaw()
|
||||
gen_ucd()
|
||||
gen_wcwidth()
|
||||
gen_emoji()
|
||||
|
||||
3
glfw/cocoa_platform.h
vendored
3
glfw/cocoa_platform.h
vendored
@@ -88,9 +88,10 @@ typedef struct _GLFWwindowNS
|
||||
|
||||
GLFWbool maximized;
|
||||
|
||||
// Cached window and framebuffer sizes used to filter out duplicate events
|
||||
// Cached window properties to filter out duplicate events
|
||||
int width, height;
|
||||
int fbWidth, fbHeight;
|
||||
float xscale, yscale;
|
||||
|
||||
// The total sum of the distances the cursor has been warped
|
||||
// since the last cursor motion event was processed
|
||||
|
||||
@@ -574,6 +574,16 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
|
||||
window->ns.fbHeight = fbRect.size.height;
|
||||
_glfwInputFramebufferSize(window, fbRect.size.width, fbRect.size.height);
|
||||
}
|
||||
|
||||
const float xscale = fbRect.size.width / contentRect.size.width;
|
||||
const float yscale = fbRect.size.height / contentRect.size.height;
|
||||
|
||||
if (xscale != window->ns.xscale || yscale != window->ns.yscale)
|
||||
{
|
||||
window->ns.xscale = xscale;
|
||||
window->ns.yscale = yscale;
|
||||
_glfwInputWindowContentScale(window, xscale, yscale);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)drawRect:(NSRect)rect
|
||||
@@ -1478,6 +1488,20 @@ int _glfwPlatformWindowMaximized(_GLFWwindow* window)
|
||||
return [window->ns.object isZoomed];
|
||||
}
|
||||
|
||||
int _glfwPlatformWindowHovered(_GLFWwindow* window)
|
||||
{
|
||||
const NSPoint point = [NSEvent mouseLocation];
|
||||
|
||||
if ([NSWindow windowNumberAtPoint:point belowWindowWithWindowNumber:0] !=
|
||||
[window->ns.object windowNumber])
|
||||
{
|
||||
return GLFW_FALSE;
|
||||
}
|
||||
|
||||
return NSPointInRect(point,
|
||||
[window->ns.object convertRectToScreen:[window->ns.view bounds]]);
|
||||
}
|
||||
|
||||
int _glfwPlatformFramebufferTransparent(_GLFWwindow* window)
|
||||
{
|
||||
return ![window->ns.object isOpaque] && ![window->ns.view isOpaque];
|
||||
|
||||
16
glfw/context.c
vendored
16
glfw/context.c
vendored
@@ -321,10 +321,11 @@ const _GLFWfbconfig* _glfwChooseFBConfig(const _GLFWfbconfig* desired,
|
||||
return closest;
|
||||
}
|
||||
|
||||
GLFWbool _glfwRefreshContextAttribs(const _GLFWctxconfig* ctxconfig)
|
||||
GLFWbool _glfwRefreshContextAttribs(_GLFWwindow* window,
|
||||
const _GLFWctxconfig* ctxconfig)
|
||||
{
|
||||
int i;
|
||||
_GLFWwindow* window;
|
||||
_GLFWwindow* previous;
|
||||
const char* version;
|
||||
const char* prefixes[] =
|
||||
{
|
||||
@@ -334,11 +335,12 @@ GLFWbool _glfwRefreshContextAttribs(const _GLFWctxconfig* ctxconfig)
|
||||
NULL
|
||||
};
|
||||
|
||||
window = _glfwPlatformGetTls(&_glfw.contextSlot);
|
||||
|
||||
window->context.source = ctxconfig->source;
|
||||
window->context.client = GLFW_OPENGL_API;
|
||||
|
||||
previous = _glfwPlatformGetTls(&_glfw.contextSlot);;
|
||||
glfwMakeContextCurrent((GLFWwindow*) window);
|
||||
|
||||
window->context.GetIntegerv = (PFNGLGETINTEGERVPROC)
|
||||
window->context.getProcAddress("glGetIntegerv");
|
||||
window->context.GetString = (PFNGLGETSTRINGPROC)
|
||||
@@ -346,6 +348,7 @@ GLFWbool _glfwRefreshContextAttribs(const _GLFWctxconfig* ctxconfig)
|
||||
if (!window->context.GetIntegerv || !window->context.GetString)
|
||||
{
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR, "Entry point retrieval is broken");
|
||||
glfwMakeContextCurrent((GLFWwindow*) previous);
|
||||
return GLFW_FALSE;
|
||||
}
|
||||
|
||||
@@ -363,6 +366,7 @@ GLFWbool _glfwRefreshContextAttribs(const _GLFWctxconfig* ctxconfig)
|
||||
"OpenGL ES version string retrieval is broken");
|
||||
}
|
||||
|
||||
glfwMakeContextCurrent((GLFWwindow*) previous);
|
||||
return GLFW_FALSE;
|
||||
}
|
||||
|
||||
@@ -394,6 +398,7 @@ GLFWbool _glfwRefreshContextAttribs(const _GLFWctxconfig* ctxconfig)
|
||||
"No version found in OpenGL ES version string");
|
||||
}
|
||||
|
||||
glfwMakeContextCurrent((GLFWwindow*) previous);
|
||||
return GLFW_FALSE;
|
||||
}
|
||||
|
||||
@@ -423,6 +428,7 @@ GLFWbool _glfwRefreshContextAttribs(const _GLFWctxconfig* ctxconfig)
|
||||
window->context.major, window->context.minor);
|
||||
}
|
||||
|
||||
glfwMakeContextCurrent((GLFWwindow*) previous);
|
||||
return GLFW_FALSE;
|
||||
}
|
||||
|
||||
@@ -438,6 +444,7 @@ GLFWbool _glfwRefreshContextAttribs(const _GLFWctxconfig* ctxconfig)
|
||||
{
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR,
|
||||
"Entry point retrieval is broken");
|
||||
glfwMakeContextCurrent((GLFWwindow*) previous);
|
||||
return GLFW_FALSE;
|
||||
}
|
||||
}
|
||||
@@ -544,6 +551,7 @@ GLFWbool _glfwRefreshContextAttribs(const _GLFWctxconfig* ctxconfig)
|
||||
window->context.swapBuffers(window);
|
||||
}
|
||||
|
||||
glfwMakeContextCurrent((GLFWwindow*) previous);
|
||||
return GLFW_TRUE;
|
||||
}
|
||||
|
||||
|
||||
13
glfw/glfw.py
13
glfw/glfw.py
@@ -52,7 +52,7 @@ def init_env(env, pkg_config, at_least_version, module='x11'):
|
||||
ans.ldpaths.extend(('-framework', f))
|
||||
|
||||
elif module == 'wayland':
|
||||
at_least_version('wayland-protocols', 1, 6)
|
||||
at_least_version('wayland-protocols', *sinfo['wayland_protocols'])
|
||||
ans.wayland_packagedir = os.path.abspath(pkg_config('wayland-protocols', '--variable=pkgdatadir')[0])
|
||||
ans.wayland_scanner = os.path.abspath(pkg_config('wayland-scanner', '--variable=wayland_scanner')[0])
|
||||
ans.wayland_protocols = tuple(sinfo[module]['protocols'])
|
||||
@@ -82,6 +82,7 @@ def build_wayland_protocols(env, run_tool, emphasis, newer, dest_dir):
|
||||
|
||||
def collect_source_information():
|
||||
raw = open('src/CMakeLists.txt').read()
|
||||
mraw = open('CMakeLists.txt').read()
|
||||
|
||||
def extract_sources(group, start_pos=0):
|
||||
for which in 'HEADERS SOURCES'.split():
|
||||
@@ -93,8 +94,11 @@ def collect_source_information():
|
||||
).group(1).strip().split()
|
||||
)
|
||||
|
||||
wayland_protocols = re.search('WaylandProtocols\s+(\S+)\s+', mraw).group(1)
|
||||
wayland_protocols = list(map(int, wayland_protocols.split('.')))
|
||||
ans = {
|
||||
'common': dict(extract_sources('common')),
|
||||
'wayland_protocols': wayland_protocols,
|
||||
}
|
||||
for group in 'cocoa win32 x11 wayland osmesa'.split():
|
||||
m = re.search('_GLFW_' + group.upper(), raw)
|
||||
@@ -244,9 +248,10 @@ def main():
|
||||
sinfo = collect_source_information()
|
||||
files_to_copy = set()
|
||||
for x in sinfo.values():
|
||||
headers, sources = x['headers'], x['sources']
|
||||
for name in headers + sources:
|
||||
files_to_copy.add(os.path.abspath(os.path.join('src', name)))
|
||||
if isinstance(x, dict):
|
||||
headers, sources = x['headers'], x['sources']
|
||||
for name in headers + sources:
|
||||
files_to_copy.add(os.path.abspath(os.path.join('src', name)))
|
||||
glfw_header = os.path.abspath('include/GLFW/glfw3.h')
|
||||
glfw_native_header = os.path.abspath('include/GLFW/glfw3native.h')
|
||||
os.chdir(base)
|
||||
|
||||
44
glfw/glfw3.h
vendored
44
glfw/glfw3.h
vendored
@@ -814,6 +814,7 @@ extern "C" {
|
||||
* [window attribute](@ref GLFW_TRANSPARENT_FRAMEBUFFER_attrib).
|
||||
*/
|
||||
#define GLFW_TRANSPARENT_FRAMEBUFFER 0x0002000A
|
||||
#define GLFW_HOVERED 0x0002000B
|
||||
|
||||
/*! @brief Framebuffer bit depth hint.
|
||||
*
|
||||
@@ -1278,6 +1279,24 @@ typedef void (* GLFWwindowmaximizefun)(GLFWwindow*,int);
|
||||
*/
|
||||
typedef void (* GLFWframebuffersizefun)(GLFWwindow*,int,int);
|
||||
|
||||
/*! @brief The function signature for window content scale callbacks.
|
||||
*
|
||||
* This is the function signature for window content scale callback
|
||||
* functions.
|
||||
*
|
||||
* @param[in] window The window whose content scale changed.
|
||||
* @param[in] xscale The new x-axis content scale of the window.
|
||||
* @param[in] yscale The new y-axis content scale of the window.
|
||||
*
|
||||
* @sa @ref window_scale
|
||||
* @sa @ref glfwSetWindowContentScaleCallback
|
||||
*
|
||||
* @since Added in version 3.3.
|
||||
*
|
||||
* @ingroup window
|
||||
*/
|
||||
typedef void (* GLFWwindowcontentscalefun)(GLFWwindow*,float,float);
|
||||
|
||||
/*! @brief The function signature for mouse button callbacks.
|
||||
*
|
||||
* This is the function signature for mouse button callback functions.
|
||||
@@ -2913,6 +2932,7 @@ GLFWAPI void glfwGetWindowFrameSize(GLFWwindow* window, int* left, int* top, int
|
||||
* @thread_safety This function must only be called from the main thread.
|
||||
*
|
||||
* @sa @ref window_scale
|
||||
* @sa @ref glfwSetWindowContentScaleCallback
|
||||
* @sa @ref glfwGetMonitorContentScale
|
||||
*
|
||||
* @since Added in version 3.3.
|
||||
@@ -3602,6 +3622,30 @@ GLFWAPI GLFWwindowmaximizefun glfwSetWindowMaximizeCallback(GLFWwindow* window,
|
||||
*/
|
||||
GLFWAPI GLFWframebuffersizefun glfwSetFramebufferSizeCallback(GLFWwindow* window, GLFWframebuffersizefun cbfun);
|
||||
|
||||
/*! @brief Sets the window content scale callback for the specified window.
|
||||
*
|
||||
* This function sets the window content scale callback of the specified window,
|
||||
* which is called when the content scale of the specified window changes.
|
||||
*
|
||||
* @param[in] window The window whose callback to set.
|
||||
* @param[in] cbfun The new callback, or `NULL` to remove the currently set
|
||||
* callback.
|
||||
* @return The previously set callback, or `NULL` if no callback was set or the
|
||||
* library had not been [initialized](@ref intro_init).
|
||||
*
|
||||
* @errors Possible errors include @ref GLFW_NOT_INITIALIZED.
|
||||
*
|
||||
* @thread_safety This function must only be called from the main thread.
|
||||
*
|
||||
* @sa @ref window_scale
|
||||
* @sa @ref glfwGetWindowContentScale
|
||||
*
|
||||
* @since Added in version 3.3.
|
||||
*
|
||||
* @ingroup window
|
||||
*/
|
||||
GLFWAPI GLFWwindowcontentscalefun glfwSetWindowContentScaleCallback(GLFWwindow* window, GLFWwindowcontentscalefun cbfun);
|
||||
|
||||
/*! @brief Processes all pending events.
|
||||
*
|
||||
* This function processes only those events that are already in the event
|
||||
|
||||
13
glfw/internal.h
vendored
13
glfw/internal.h
vendored
@@ -437,6 +437,7 @@ struct _GLFWwindow
|
||||
GLFWwindowiconifyfun iconify;
|
||||
GLFWwindowmaximizefun maximize;
|
||||
GLFWframebuffersizefun fbsize;
|
||||
GLFWwindowcontentscalefun scale;
|
||||
GLFWmousebuttonfun mouseButton;
|
||||
GLFWcursorposfun cursorPos;
|
||||
GLFWcursorenterfun cursorEnter;
|
||||
@@ -690,6 +691,7 @@ int _glfwPlatformWindowFocused(_GLFWwindow* window);
|
||||
int _glfwPlatformWindowIconified(_GLFWwindow* window);
|
||||
int _glfwPlatformWindowVisible(_GLFWwindow* window);
|
||||
int _glfwPlatformWindowMaximized(_GLFWwindow* window);
|
||||
int _glfwPlatformWindowHovered(_GLFWwindow* window);
|
||||
int _glfwPlatformFramebufferTransparent(_GLFWwindow* window);
|
||||
float _glfwPlatformGetWindowOpacity(_GLFWwindow* window);
|
||||
void _glfwPlatformSetWindowResizable(_GLFWwindow* window, GLFWbool enabled);
|
||||
@@ -755,6 +757,14 @@ void _glfwInputWindowSize(_GLFWwindow* window, int width, int height);
|
||||
*/
|
||||
void _glfwInputFramebufferSize(_GLFWwindow* window, int width, int height);
|
||||
|
||||
/*! @brief Notifies shared code that a window content scale has changed.
|
||||
* @param[in] window The window that received the event.
|
||||
* @param[in] xscale The new x-axis content scale of the window.
|
||||
* @param[in] yscale The new y-axis content scale of the window.
|
||||
* @ingroup event
|
||||
*/
|
||||
void _glfwInputWindowContentScale(_GLFWwindow* window, float xscale, float yscale);
|
||||
|
||||
/*! @brief Notifies shared code that a window has been iconified or restored.
|
||||
* @param[in] window The window that received the event.
|
||||
* @param[in] iconified `GLFW_TRUE` if the window was iconified, or
|
||||
@@ -955,7 +965,8 @@ const _GLFWfbconfig* _glfwChooseFBConfig(const _GLFWfbconfig* desired,
|
||||
* unusable.
|
||||
* @ingroup utility
|
||||
*/
|
||||
GLFWbool _glfwRefreshContextAttribs(const _GLFWctxconfig* ctxconfig);
|
||||
GLFWbool _glfwRefreshContextAttribs(_GLFWwindow* window,
|
||||
const _GLFWctxconfig* ctxconfig);
|
||||
|
||||
/*! @brief Checks whether the desired context attributes are valid.
|
||||
* @param[in] ctxconfig The context attributes to check.
|
||||
|
||||
5
glfw/null_window.c
vendored
5
glfw/null_window.c
vendored
@@ -165,6 +165,11 @@ int _glfwPlatformWindowMaximized(_GLFWwindow* window)
|
||||
return GLFW_FALSE;
|
||||
}
|
||||
|
||||
int _glfwPlatformWindowHovered(_GLFWwindow* window)
|
||||
{
|
||||
return GLFW_FALSE;
|
||||
}
|
||||
|
||||
int _glfwPlatformFramebufferTransparent(_GLFWwindow* window)
|
||||
{
|
||||
return GLFW_FALSE;
|
||||
|
||||
@@ -79,6 +79,10 @@
|
||||
"osmesa_context.c"
|
||||
]
|
||||
},
|
||||
"wayland_protocols": [
|
||||
1,
|
||||
6
|
||||
],
|
||||
"win32": {
|
||||
"headers": [
|
||||
"win32_platform.h",
|
||||
|
||||
91
glfw/wgl_context.c
vendored
91
glfw/wgl_context.c
vendored
@@ -329,21 +329,52 @@ static void destroyContextWGL(_GLFWwindow* window)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize WGL-specific extensions
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
////// GLFW internal API //////
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Initialize WGL
|
||||
//
|
||||
static void loadWGLExtensions(void)
|
||||
GLFWbool _glfwInitWGL(void)
|
||||
{
|
||||
PIXELFORMATDESCRIPTOR pfd;
|
||||
HGLRC rc;
|
||||
HDC dc = GetDC(_glfw.win32.helperWindowHandle);;
|
||||
HGLRC prc, rc;
|
||||
HDC pdc, dc;
|
||||
|
||||
_glfw.wgl.extensionsLoaded = GLFW_TRUE;
|
||||
if (_glfw.wgl.instance)
|
||||
return GLFW_TRUE;
|
||||
|
||||
_glfw.wgl.instance = LoadLibraryA("opengl32.dll");
|
||||
if (!_glfw.wgl.instance)
|
||||
{
|
||||
_glfwInputErrorWin32(GLFW_PLATFORM_ERROR,
|
||||
"WGL: Failed to load opengl32.dll");
|
||||
return GLFW_FALSE;
|
||||
}
|
||||
|
||||
_glfw.wgl.CreateContext = (PFN_wglCreateContext)
|
||||
GetProcAddress(_glfw.wgl.instance, "wglCreateContext");
|
||||
_glfw.wgl.DeleteContext = (PFN_wglDeleteContext)
|
||||
GetProcAddress(_glfw.wgl.instance, "wglDeleteContext");
|
||||
_glfw.wgl.GetProcAddress = (PFN_wglGetProcAddress)
|
||||
GetProcAddress(_glfw.wgl.instance, "wglGetProcAddress");
|
||||
_glfw.wgl.GetCurrentDC = (PFN_wglGetCurrentDC)
|
||||
GetProcAddress(_glfw.wgl.instance, "wglGetCurrentDC");
|
||||
_glfw.wgl.GetCurrentContext = (PFN_wglGetCurrentContext)
|
||||
GetProcAddress(_glfw.wgl.instance, "wglGetCurrentContext");
|
||||
_glfw.wgl.MakeCurrent = (PFN_wglMakeCurrent)
|
||||
GetProcAddress(_glfw.wgl.instance, "wglMakeCurrent");
|
||||
_glfw.wgl.ShareLists = (PFN_wglShareLists)
|
||||
GetProcAddress(_glfw.wgl.instance, "wglShareLists");
|
||||
|
||||
// NOTE: A dummy context has to be created for opengl32.dll to load the
|
||||
// OpenGL ICD, from which we can then query WGL extensions
|
||||
// NOTE: This code will accept the Microsoft GDI ICD; accelerated context
|
||||
// creation failure occurs during manual pixel format enumeration
|
||||
|
||||
dc = GetDC(_glfw.win32.helperWindowHandle);;
|
||||
|
||||
ZeroMemory(&pfd, sizeof(pfd));
|
||||
pfd.nSize = sizeof(pfd);
|
||||
pfd.nVersion = 1;
|
||||
@@ -355,7 +386,7 @@ static void loadWGLExtensions(void)
|
||||
{
|
||||
_glfwInputErrorWin32(GLFW_PLATFORM_ERROR,
|
||||
"WGL: Failed to set pixel format for dummy context");
|
||||
return;
|
||||
return GLFW_FALSE;
|
||||
}
|
||||
|
||||
rc = wglCreateContext(dc);
|
||||
@@ -363,15 +394,19 @@ static void loadWGLExtensions(void)
|
||||
{
|
||||
_glfwInputErrorWin32(GLFW_PLATFORM_ERROR,
|
||||
"WGL: Failed to create dummy context");
|
||||
return;
|
||||
return GLFW_FALSE;
|
||||
}
|
||||
|
||||
pdc = wglGetCurrentDC();
|
||||
prc = wglGetCurrentContext();
|
||||
|
||||
if (!wglMakeCurrent(dc, rc))
|
||||
{
|
||||
_glfwInputErrorWin32(GLFW_PLATFORM_ERROR,
|
||||
"WGL: Failed to make dummy context current");
|
||||
wglMakeCurrent(pdc, prc);
|
||||
wglDeleteContext(rc);
|
||||
return;
|
||||
return GLFW_FALSE;
|
||||
}
|
||||
|
||||
// NOTE: Functions must be loaded first as they're needed to retrieve the
|
||||
@@ -414,43 +449,8 @@ static void loadWGLExtensions(void)
|
||||
_glfw.wgl.ARB_context_flush_control =
|
||||
extensionSupportedWGL("WGL_ARB_context_flush_control");
|
||||
|
||||
wglMakeCurrent(dc, NULL);
|
||||
wglMakeCurrent(pdc, prc);
|
||||
wglDeleteContext(rc);
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
////// GLFW internal API //////
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Initialize WGL
|
||||
//
|
||||
GLFWbool _glfwInitWGL(void)
|
||||
{
|
||||
if (_glfw.wgl.instance)
|
||||
return GLFW_TRUE;
|
||||
|
||||
_glfw.wgl.instance = LoadLibraryA("opengl32.dll");
|
||||
if (!_glfw.wgl.instance)
|
||||
{
|
||||
_glfwInputErrorWin32(GLFW_PLATFORM_ERROR,
|
||||
"WGL: Failed to load opengl32.dll");
|
||||
return GLFW_FALSE;
|
||||
}
|
||||
|
||||
_glfw.wgl.CreateContext = (PFN_wglCreateContext)
|
||||
GetProcAddress(_glfw.wgl.instance, "wglCreateContext");
|
||||
_glfw.wgl.DeleteContext = (PFN_wglDeleteContext)
|
||||
GetProcAddress(_glfw.wgl.instance, "wglDeleteContext");
|
||||
_glfw.wgl.GetProcAddress = (PFN_wglGetProcAddress)
|
||||
GetProcAddress(_glfw.wgl.instance, "wglGetProcAddress");
|
||||
_glfw.wgl.GetCurrentDC = (PFN_wglGetCurrentDC)
|
||||
GetProcAddress(_glfw.wgl.instance, "wglGetCurrentDC");
|
||||
_glfw.wgl.MakeCurrent = (PFN_wglMakeCurrent)
|
||||
GetProcAddress(_glfw.wgl.instance, "wglMakeCurrent");
|
||||
_glfw.wgl.ShareLists = (PFN_wglShareLists)
|
||||
GetProcAddress(_glfw.wgl.instance, "wglShareLists");
|
||||
|
||||
return GLFW_TRUE;
|
||||
}
|
||||
|
||||
@@ -480,9 +480,6 @@ GLFWbool _glfwCreateContextWGL(_GLFWwindow* window,
|
||||
PIXELFORMATDESCRIPTOR pfd;
|
||||
HGLRC share = NULL;
|
||||
|
||||
if (!_glfw.wgl.extensionsLoaded)
|
||||
loadWGLExtensions();
|
||||
|
||||
if (ctxconfig->share)
|
||||
share = ctxconfig->share->context.wgl.handle;
|
||||
|
||||
|
||||
5
glfw/wgl_context.h
vendored
5
glfw/wgl_context.h
vendored
@@ -86,6 +86,7 @@ typedef HGLRC (WINAPI * PFN_wglCreateContext)(HDC);
|
||||
typedef BOOL (WINAPI * PFN_wglDeleteContext)(HGLRC);
|
||||
typedef PROC (WINAPI * PFN_wglGetProcAddress)(LPCSTR);
|
||||
typedef HDC (WINAPI * PFN_wglGetCurrentDC)(void);
|
||||
typedef HGLRC (WINAPI * PFN_wglGetCurrentContext)(void);
|
||||
typedef BOOL (WINAPI * PFN_wglMakeCurrent)(HDC,HGLRC);
|
||||
typedef BOOL (WINAPI * PFN_wglShareLists)(HGLRC,HGLRC);
|
||||
|
||||
@@ -94,6 +95,7 @@ typedef BOOL (WINAPI * PFN_wglShareLists)(HGLRC,HGLRC);
|
||||
#define wglDeleteContext _glfw.wgl.DeleteContext
|
||||
#define wglGetProcAddress _glfw.wgl.GetProcAddress
|
||||
#define wglGetCurrentDC _glfw.wgl.GetCurrentDC
|
||||
#define wglGetCurrentContext _glfw.wgl.GetCurrentContext
|
||||
#define wglMakeCurrent _glfw.wgl.MakeCurrent
|
||||
#define wglShareLists _glfw.wgl.ShareLists
|
||||
|
||||
@@ -124,11 +126,10 @@ typedef struct _GLFWlibraryWGL
|
||||
PFN_wglDeleteContext DeleteContext;
|
||||
PFN_wglGetProcAddress GetProcAddress;
|
||||
PFN_wglGetCurrentDC GetCurrentDC;
|
||||
PFN_wglGetCurrentContext GetCurrentContext;
|
||||
PFN_wglMakeCurrent MakeCurrent;
|
||||
PFN_wglShareLists ShareLists;
|
||||
|
||||
GLFWbool extensionsLoaded;
|
||||
|
||||
PFNWGLSWAPINTERVALEXTPROC SwapIntervalEXT;
|
||||
PFNWGLGETPIXELFORMATATTRIBIVARBPROC GetPixelFormatAttribivARB;
|
||||
PFNWGLGETEXTENSIONSSTRINGEXTPROC GetExtensionsStringEXT;
|
||||
|
||||
10
glfw/win32_init.c
vendored
10
glfw/win32_init.c
vendored
@@ -351,9 +351,10 @@ static HWND createHelperWindow(void)
|
||||
dbi.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
|
||||
dbi.dbcc_classguid = GUID_DEVINTERFACE_HID;
|
||||
|
||||
RegisterDeviceNotificationW(window,
|
||||
(DEV_BROADCAST_HDR*) &dbi,
|
||||
DEVICE_NOTIFY_WINDOW_HANDLE);
|
||||
_glfw.win32.deviceNotificationHandle =
|
||||
RegisterDeviceNotificationW(window,
|
||||
(DEV_BROADCAST_HDR*) &dbi,
|
||||
DEVICE_NOTIFY_WINDOW_HANDLE);
|
||||
}
|
||||
|
||||
while (PeekMessageW(&msg, _glfw.win32.helperWindowHandle, 0, 0, PM_REMOVE))
|
||||
@@ -543,6 +544,9 @@ int _glfwPlatformInit(void)
|
||||
|
||||
void _glfwPlatformTerminate(void)
|
||||
{
|
||||
if (_glfw.win32.deviceNotificationHandle)
|
||||
UnregisterDeviceNotification(_glfw.win32.deviceNotificationHandle);
|
||||
|
||||
if (_glfw.win32.helperWindowHandle)
|
||||
DestroyWindow(_glfw.win32.helperWindowHandle);
|
||||
|
||||
|
||||
1
glfw/win32_platform.h
vendored
1
glfw/win32_platform.h
vendored
@@ -288,6 +288,7 @@ typedef struct _GLFWwindowWin32
|
||||
typedef struct _GLFWlibraryWin32
|
||||
{
|
||||
HWND helperWindowHandle;
|
||||
HDEVNOTIFY deviceNotificationHandle;
|
||||
DWORD foregroundLockTimeout;
|
||||
int acquiredMonitorCount;
|
||||
char* clipboardString;
|
||||
|
||||
15
glfw/win32_window.c
vendored
15
glfw/win32_window.c
vendored
@@ -996,6 +996,14 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg,
|
||||
return 0;
|
||||
}
|
||||
|
||||
case WM_DPICHANGED:
|
||||
{
|
||||
const float xscale = HIWORD(wParam) / 96.f;
|
||||
const float yscale = LOWORD(wParam) / 96.f;
|
||||
_glfwInputWindowContentScale(window, xscale, yscale);
|
||||
break;
|
||||
}
|
||||
|
||||
case WM_SETCURSOR:
|
||||
{
|
||||
if (LOWORD(lParam) == HTCLIENT)
|
||||
@@ -1451,7 +1459,7 @@ void _glfwPlatformMaximizeWindow(_GLFWwindow* window)
|
||||
|
||||
void _glfwPlatformShowWindow(_GLFWwindow* window)
|
||||
{
|
||||
ShowWindow(window->win32.handle, SW_SHOW);
|
||||
ShowWindow(window->win32.handle, SW_SHOWNA);
|
||||
}
|
||||
|
||||
void _glfwPlatformHideWindow(_GLFWwindow* window)
|
||||
@@ -1575,6 +1583,11 @@ int _glfwPlatformWindowMaximized(_GLFWwindow* window)
|
||||
return IsZoomed(window->win32.handle);
|
||||
}
|
||||
|
||||
int _glfwPlatformWindowHovered(_GLFWwindow* window)
|
||||
{
|
||||
return cursorInClientArea(window);
|
||||
}
|
||||
|
||||
int _glfwPlatformFramebufferTransparent(_GLFWwindow* window)
|
||||
{
|
||||
return window->win32.transparent && _glfwIsCompositionEnabledWin32();
|
||||
|
||||
35
glfw/window.c
vendored
35
glfw/window.c
vendored
@@ -94,6 +94,12 @@ void _glfwInputFramebufferSize(_GLFWwindow* window, int width, int height)
|
||||
window->callbacks.fbsize((GLFWwindow*) window, width, height);
|
||||
}
|
||||
|
||||
void _glfwInputWindowContentScale(_GLFWwindow* window, float xscale, float yscale)
|
||||
{
|
||||
if (window->callbacks.scale)
|
||||
window->callbacks.scale((GLFWwindow*) window, xscale, yscale);
|
||||
}
|
||||
|
||||
void _glfwInputWindowDamage(_GLFWwindow* window)
|
||||
{
|
||||
if (window->callbacks.refresh)
|
||||
@@ -127,7 +133,6 @@ GLFWAPI GLFWwindow* glfwCreateWindow(int width, int height,
|
||||
_GLFWctxconfig ctxconfig;
|
||||
_GLFWwndconfig wndconfig;
|
||||
_GLFWwindow* window;
|
||||
_GLFWwindow* previous;
|
||||
|
||||
assert(title != NULL);
|
||||
assert(width >= 0);
|
||||
@@ -191,33 +196,20 @@ GLFWAPI GLFWwindow* glfwCreateWindow(int width, int height,
|
||||
window->numer = GLFW_DONT_CARE;
|
||||
window->denom = GLFW_DONT_CARE;
|
||||
|
||||
// Save the currently current context so it can be restored later
|
||||
previous = _glfwPlatformGetTls(&_glfw.contextSlot);
|
||||
if (ctxconfig.client != GLFW_NO_API)
|
||||
glfwMakeContextCurrent(NULL);
|
||||
|
||||
// Open the actual window and create its context
|
||||
if (!_glfwPlatformCreateWindow(window, &wndconfig, &ctxconfig, &fbconfig))
|
||||
{
|
||||
glfwMakeContextCurrent((GLFWwindow*) previous);
|
||||
glfwDestroyWindow((GLFWwindow*) window);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (ctxconfig.client != GLFW_NO_API)
|
||||
{
|
||||
window->context.makeCurrent(window);
|
||||
|
||||
// Retrieve the actual (as opposed to requested) context attributes
|
||||
if (!_glfwRefreshContextAttribs(&ctxconfig))
|
||||
if (!_glfwRefreshContextAttribs(window, &ctxconfig))
|
||||
{
|
||||
glfwMakeContextCurrent((GLFWwindow*) previous);
|
||||
glfwDestroyWindow((GLFWwindow*) window);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Restore the previously current context (or NULL)
|
||||
glfwMakeContextCurrent((GLFWwindow*) previous);
|
||||
}
|
||||
|
||||
if (!window->monitor)
|
||||
@@ -803,6 +795,8 @@ GLFWAPI int glfwGetWindowAttrib(GLFWwindow* handle, int attrib)
|
||||
return _glfwPlatformWindowVisible(window);
|
||||
case GLFW_MAXIMIZED:
|
||||
return _glfwPlatformWindowMaximized(window);
|
||||
case GLFW_HOVERED:
|
||||
return _glfwPlatformWindowHovered(window);
|
||||
case GLFW_TRANSPARENT_FRAMEBUFFER:
|
||||
return _glfwPlatformFramebufferTransparent(window);
|
||||
case GLFW_RESIZABLE:
|
||||
@@ -1037,6 +1031,17 @@ GLFWAPI GLFWframebuffersizefun glfwSetFramebufferSizeCallback(GLFWwindow* handle
|
||||
return cbfun;
|
||||
}
|
||||
|
||||
GLFWAPI GLFWwindowcontentscalefun glfwSetWindowContentScaleCallback(GLFWwindow* handle,
|
||||
GLFWwindowcontentscalefun cbfun)
|
||||
{
|
||||
_GLFWwindow* window = (_GLFWwindow*) handle;
|
||||
assert(window != NULL);
|
||||
|
||||
_GLFW_REQUIRE_INIT_OR_RETURN(NULL);
|
||||
_GLFW_SWAP_POINTERS(window->callbacks.scale, cbfun);
|
||||
return cbfun;
|
||||
}
|
||||
|
||||
GLFWAPI void glfwPollEvents(void)
|
||||
{
|
||||
_GLFW_REQUIRE_INIT();
|
||||
|
||||
23
glfw/wl_init.c
vendored
23
glfw/wl_init.c
vendored
@@ -54,6 +54,8 @@ static void pointerHandleEnter(void* data,
|
||||
_glfw.wl.pointerSerial = serial;
|
||||
_glfw.wl.pointerFocus = window;
|
||||
|
||||
window->wl.hovered = GLFW_TRUE;
|
||||
|
||||
_glfwPlatformSetCursor(window, window->wl.currentCursor);
|
||||
_glfwInputCursorEnter(window, GLFW_TRUE);
|
||||
}
|
||||
@@ -68,6 +70,8 @@ static void pointerHandleLeave(void* data,
|
||||
if (!window)
|
||||
return;
|
||||
|
||||
window->wl.hovered = GLFW_FALSE;
|
||||
|
||||
_glfw.wl.pointerSerial = serial;
|
||||
_glfw.wl.pointerFocus = NULL;
|
||||
_glfwInputCursorEnter(window, GLFW_FALSE);
|
||||
@@ -771,15 +775,22 @@ void _glfwPlatformTerminate(void)
|
||||
_glfwTerminateJoysticksLinux();
|
||||
|
||||
#ifdef HAVE_XKBCOMMON_COMPOSE_H
|
||||
xkb_compose_state_unref(_glfw.wl.xkb.composeState);
|
||||
if (_glfw.wl.xkb.composeState)
|
||||
xkb_compose_state_unref(_glfw.wl.xkb.composeState);
|
||||
#endif
|
||||
|
||||
xkb_keymap_unref(_glfw.wl.xkb.keymap);
|
||||
xkb_state_unref(_glfw.wl.xkb.state);
|
||||
xkb_context_unref(_glfw.wl.xkb.context);
|
||||
if (_glfw.wl.xkb.keymap)
|
||||
xkb_keymap_unref(_glfw.wl.xkb.keymap);
|
||||
if (_glfw.wl.xkb.state)
|
||||
xkb_state_unref(_glfw.wl.xkb.state);
|
||||
if (_glfw.wl.xkb.context)
|
||||
xkb_context_unref(_glfw.wl.xkb.context);
|
||||
|
||||
dlclose(_glfw.wl.xkb.handle);
|
||||
_glfw.wl.xkb.handle = NULL;
|
||||
if (_glfw.wl.xkb.handle)
|
||||
{
|
||||
_glfw_dlclose(_glfw.wl.xkb.handle);
|
||||
_glfw.wl.xkb.handle = NULL;
|
||||
}
|
||||
|
||||
if (_glfw.wl.cursorTheme)
|
||||
wl_cursor_theme_destroy(_glfw.wl.cursorTheme);
|
||||
|
||||
1
glfw/wl_platform.h
vendored
1
glfw/wl_platform.h
vendored
@@ -117,6 +117,7 @@ typedef struct _GLFWwindowWayland
|
||||
int width, height;
|
||||
GLFWbool visible;
|
||||
GLFWbool maximized;
|
||||
GLFWbool hovered;
|
||||
GLFWbool transparent;
|
||||
struct wl_surface* surface;
|
||||
struct wl_egl_window* native;
|
||||
|
||||
6
glfw/wl_window.c
vendored
6
glfw/wl_window.c
vendored
@@ -125,6 +125,7 @@ static void checkScaleChange(_GLFWwindow* window)
|
||||
wl_surface_set_buffer_scale(window->wl.surface, scale);
|
||||
wl_egl_window_resize(window->wl.native, scaledWidth, scaledHeight, 0, 0);
|
||||
_glfwInputFramebufferSize(window, scaledWidth, scaledHeight);
|
||||
_glfwInputWindowContentScale(window, scale, scale);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -700,6 +701,11 @@ int _glfwPlatformWindowMaximized(_GLFWwindow* window)
|
||||
return window->wl.maximized;
|
||||
}
|
||||
|
||||
int _glfwPlatformWindowHovered(_GLFWwindow* window)
|
||||
{
|
||||
return window->wl.hovered;
|
||||
}
|
||||
|
||||
int _glfwPlatformFramebufferTransparent(_GLFWwindow* window)
|
||||
{
|
||||
return window->wl.transparent;
|
||||
|
||||
22
glfw/x11_window.c
vendored
22
glfw/x11_window.c
vendored
@@ -2458,6 +2458,28 @@ int _glfwPlatformWindowMaximized(_GLFWwindow* window)
|
||||
return maximized;
|
||||
}
|
||||
|
||||
int _glfwPlatformWindowHovered(_GLFWwindow* window)
|
||||
{
|
||||
Window w = _glfw.x11.root;
|
||||
while (w)
|
||||
{
|
||||
Window root;
|
||||
int rootX, rootY, childX, childY;
|
||||
unsigned int mask;
|
||||
|
||||
if (!XQueryPointer(_glfw.x11.display, w,
|
||||
&root, &w, &rootX, &rootY, &childX, &childY, &mask))
|
||||
{
|
||||
return GLFW_FALSE;
|
||||
}
|
||||
|
||||
if (w == window->x11.handle)
|
||||
return GLFW_TRUE;
|
||||
}
|
||||
|
||||
return GLFW_FALSE;
|
||||
}
|
||||
|
||||
int _glfwPlatformFramebufferTransparent(_GLFWwindow* window)
|
||||
{
|
||||
if (!window->x11.transparent)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import re
|
||||
from gettext import gettext as _
|
||||
from weakref import WeakValueDictionary
|
||||
|
||||
@@ -11,13 +12,15 @@ from .constants import appname, set_boss, wakeup
|
||||
from .fast_data_types import (
|
||||
ChildMonitor, create_os_window, current_os_window, destroy_global_data,
|
||||
destroy_sprite_map, get_clipboard_string, glfw_post_empty_event,
|
||||
layout_sprite_map, mark_os_window_for_close, set_dpi_from_os_window,
|
||||
show_window, toggle_fullscreen, viewport_for_window
|
||||
layout_sprite_map, mark_os_window_for_close, set_clipboard_string,
|
||||
set_dpi_from_os_window, show_window, toggle_fullscreen,
|
||||
viewport_for_window
|
||||
)
|
||||
from .fonts.render import prerender, resize_fonts, set_font_family
|
||||
from .keys import get_shortcut
|
||||
from .remote_control import handle_cmd
|
||||
from .session import create_session
|
||||
from .tabs import SpecialWindow, TabManager
|
||||
from .tabs import SpecialWindow, SpecialWindowInstance, TabManager
|
||||
from .utils import (
|
||||
end_startup_notification, get_primary_selection, init_startup_notification,
|
||||
open_url, safe_print, set_primary_selection, single_instance
|
||||
@@ -94,6 +97,56 @@ class Boss:
|
||||
if dpi_changed:
|
||||
self.on_dpi_change(os_window_id)
|
||||
|
||||
def list_os_windows(self):
|
||||
for os_window_id, tm in self.os_window_map.items():
|
||||
yield {
|
||||
'id': os_window_id,
|
||||
'tabs': list(tm.list_tabs()),
|
||||
}
|
||||
|
||||
def match_windows(self, match):
|
||||
field, exp = match.split(':', 1)
|
||||
pat = re.compile(exp)
|
||||
for tm in self.os_window_map.values():
|
||||
for tab in tm:
|
||||
for window in tab:
|
||||
if window.matches(field, pat):
|
||||
yield window
|
||||
|
||||
def tab_for_window(self, window):
|
||||
for tm in self.os_window_map.values():
|
||||
for tab in tm:
|
||||
for w in tab:
|
||||
if w.id == window.id:
|
||||
return tab
|
||||
|
||||
def match_tabs(self, match):
|
||||
field, exp = match.split(':', 1)
|
||||
pat = re.compile(exp)
|
||||
tms = tuple(self.os_window_map.values())
|
||||
found = False
|
||||
if field in ('title', 'id'):
|
||||
for tm in tms:
|
||||
for tab in tm:
|
||||
if tab.matches(field, pat):
|
||||
yield tab
|
||||
found = True
|
||||
if not found:
|
||||
tabs = {self.tab_for_window(w) for w in self.match_windows(match)}
|
||||
for tab in tabs:
|
||||
if tab:
|
||||
yield tab
|
||||
|
||||
def set_active_window(self, window):
|
||||
for tm in self.os_window_map.values():
|
||||
for tab in tm:
|
||||
for w in tab:
|
||||
if w.id == window.id:
|
||||
if tab is not self.active_tab:
|
||||
tm.set_active_tab(tab)
|
||||
tab.set_active_window(w)
|
||||
return
|
||||
|
||||
def _new_os_window(self, args, cwd_from=None):
|
||||
sw = self.args_to_special_window(args, cwd_from) if args else None
|
||||
startup_session = create_session(self.opts, special_window=sw, cwd_from=cwd_from)
|
||||
@@ -125,6 +178,20 @@ class Boss:
|
||||
else:
|
||||
safe_print('Unknown message received from peer, ignoring')
|
||||
|
||||
def handle_remote_cmd(self, cmd, window=None):
|
||||
response = None
|
||||
if self.opts.allow_remote_control:
|
||||
try:
|
||||
response = handle_cmd(self, window, cmd)
|
||||
except Exception as err:
|
||||
import traceback
|
||||
response = {'ok': False, 'error': str(err), 'tb': traceback.format_exc()}
|
||||
else:
|
||||
response = {'ok': False, 'error': 'Remote control is disabled. Add allow_remote_control yes to your kitty.conf'}
|
||||
if response is not None:
|
||||
if window is not None:
|
||||
window.send_cmd_response(response)
|
||||
|
||||
def on_child_death(self, window_id):
|
||||
window = self.window_id_map.pop(window_id, None)
|
||||
if window is None:
|
||||
@@ -349,6 +416,8 @@ class Boss:
|
||||
text = w.text_for_selection()
|
||||
if text:
|
||||
set_primary_selection(text)
|
||||
if self.opts.copy_on_select:
|
||||
set_clipboard_string(text)
|
||||
|
||||
def goto_tab(self, tab_num):
|
||||
tm = self.active_tab_manager
|
||||
@@ -396,7 +465,10 @@ class Boss:
|
||||
def _new_tab(self, args, cwd_from=None):
|
||||
special_window = None
|
||||
if args:
|
||||
special_window = self.args_to_special_window(args, cwd_from=cwd_from)
|
||||
if isinstance(args, SpecialWindowInstance):
|
||||
special_window = args
|
||||
else:
|
||||
special_window = self.args_to_special_window(args, cwd_from=cwd_from)
|
||||
tm = self.active_tab_manager
|
||||
if tm is not None:
|
||||
tm.new_tab(special_window=special_window, cwd_from=cwd_from)
|
||||
|
||||
@@ -524,7 +524,16 @@ collect_cursor_info(CursorRenderInfo *ans, Window *w, double now, OSWindow *os_w
|
||||
unsigned long mult = MAX(1, screen_current_char_width(rd->screen));
|
||||
double right = left + (ans->shape == CURSOR_BEAM ? cursor_width(1.5, true, os_window) : rd->dx * mult);
|
||||
double bottom = top - rd->dy;
|
||||
if (ans->shape == CURSOR_UNDERLINE) top = bottom + cursor_width(2.0, false, os_window);
|
||||
switch (ans->shape) {
|
||||
case CURSOR_UNDERLINE:
|
||||
top = bottom + cursor_width(2.0, false, os_window);
|
||||
break;
|
||||
case CURSOR_BLOCK:
|
||||
top -= 2.0 / os_window->viewport_height; // 1px adjustment for width of line
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
ans->left = left; ans->right = right; ans->top = top; ans->bottom = bottom;
|
||||
}
|
||||
|
||||
@@ -609,11 +618,11 @@ render(double now) {
|
||||
w->viewport_size_dirty = false;
|
||||
}
|
||||
unsigned int active_window_id = 0;
|
||||
render_os_window(w, now, &active_window_id);
|
||||
Tab *active_tab = w->tabs + w->active_tab;
|
||||
BorderRects *br = &active_tab->border_rects;
|
||||
draw_borders(br->vao_idx, br->num_border_rects, br->rect_buf, br->is_dirty, w->viewport_width, w->viewport_height);
|
||||
if (TD.screen && w->num_tabs > 1) draw_cells(TD.vao_idx, 0, TD.xstart, TD.ystart, TD.dx, TD.dy, TD.screen, w, true);
|
||||
render_os_window(w, now, &active_window_id);
|
||||
swap_window_buffers(w);
|
||||
w->last_active_tab = w->active_tab; w->last_num_tabs = w->num_tabs; w->last_active_window_id = active_window_id;
|
||||
br->is_dirty = false;
|
||||
|
||||
113
kitty/child.c
Normal file
113
kitty/child.c
Normal file
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* child.c
|
||||
* Copyright (C) 2018 Kovid Goyal <kovid at kovidgoyal.net>
|
||||
*
|
||||
* Distributed under terms of the GPL3 license.
|
||||
*/
|
||||
|
||||
#include "data-types.h"
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
|
||||
static inline char**
|
||||
serialize_string_tuple(PyObject *src) {
|
||||
Py_ssize_t sz = PyTuple_GET_SIZE(src);
|
||||
char **ans = calloc(sz + 1, sizeof(char*));
|
||||
if (!ans) fatal("Out of memory");
|
||||
for (Py_ssize_t i = 0; i < sz; i++) ans[i] = PyUnicode_AsUTF8(PyTuple_GET_ITEM(src, i));
|
||||
return ans;
|
||||
}
|
||||
|
||||
extern char **environ;
|
||||
|
||||
static inline void
|
||||
write_to_stderr(const char *text) {
|
||||
size_t sz = strlen(text);
|
||||
size_t written = 0;
|
||||
while(written < sz) {
|
||||
ssize_t amt = write(2, text + written, sz - written);
|
||||
if (amt == 0) break;
|
||||
if (amt < 0) {
|
||||
if (errno == EAGAIN || errno == EINTR) continue;
|
||||
break;
|
||||
}
|
||||
written += amt;
|
||||
}
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
spawn(PyObject *self UNUSED, PyObject *args) {
|
||||
PyObject *argv_p, *env_p;
|
||||
int master, slave, stdin_read_fd, stdin_write_fd;
|
||||
char* cwd;
|
||||
if (!PyArg_ParseTuple(args, "sO!O!iiii", &cwd, &PyTuple_Type, &argv_p, &PyTuple_Type, &env_p, &master, &slave, &stdin_read_fd, &stdin_write_fd)) return NULL;
|
||||
char name[2048] = {0};
|
||||
if (ttyname_r(slave, name, sizeof(name) - 1) != 0) { PyErr_SetFromErrno(PyExc_OSError); return NULL; }
|
||||
char **argv = serialize_string_tuple(argv_p);
|
||||
char **env = serialize_string_tuple(env_p);
|
||||
|
||||
#define exit_on_err(m) { write_to_stderr(m); write_to_stderr(": "); write_to_stderr(strerror(errno)); exit(EXIT_FAILURE); }
|
||||
pid_t pid = fork();
|
||||
switch(pid) {
|
||||
case 0:
|
||||
// child
|
||||
// Use only signal-safe functions (man 7 signal-safety)
|
||||
if (chdir(cwd) != 0) { if (chdir("/") != 0) {} }; // ignore failure to chdir to /
|
||||
if (setsid() == -1) exit_on_err("setsid() in child process failed");
|
||||
if (dup2(slave, 1) == -1) exit_on_err("dup2() failed for fd number 1");
|
||||
if (dup2(slave, 2) == -1) exit_on_err("dup2() failed for fd number 2");
|
||||
if (stdin_read_fd > -1) {
|
||||
if (dup2(stdin_read_fd, 0) == -1) exit_on_err("dup2() failed for fd number 0");
|
||||
close(stdin_read_fd);
|
||||
close(stdin_write_fd);
|
||||
} else {
|
||||
if (dup2(slave, 0) == -1) exit_on_err("dup2() failed for fd number 0");
|
||||
}
|
||||
close(slave);
|
||||
close(master);
|
||||
for (int c = 3; c < 201; c++) close(c);
|
||||
|
||||
// Establish the controlling terminal (see man 7 credentials)
|
||||
int tfd = open(name, O_RDWR);
|
||||
if (tfd == -1) exit_on_err("Failed to open controlling terminal");
|
||||
close(tfd);
|
||||
|
||||
environ = env;
|
||||
execvp(argv[0], argv);
|
||||
// Report the failure and exec a shell instead, so that we are not left
|
||||
// with a forked but not exec'ed process
|
||||
write_to_stderr("Failed to launch child: ");
|
||||
write_to_stderr(argv[0]);
|
||||
write_to_stderr("\nWith error: ");
|
||||
write_to_stderr(strerror(errno));
|
||||
write_to_stderr("\nPress Enter to exit.\n");
|
||||
execlp("sh", "sh", "-c", "read w", NULL);
|
||||
exit(EXIT_FAILURE);
|
||||
break;
|
||||
case -1:
|
||||
PyErr_SetFromErrno(PyExc_OSError);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
#undef exit_on_err
|
||||
free(argv);
|
||||
free(env);
|
||||
if (PyErr_Occurred()) return NULL;
|
||||
return PyLong_FromLong(pid);
|
||||
}
|
||||
|
||||
static PyMethodDef module_methods[] = {
|
||||
METHODB(spawn, METH_VARARGS),
|
||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
|
||||
bool
|
||||
init_child(PyObject *module) {
|
||||
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
|
||||
return true;
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
import fcntl
|
||||
import os
|
||||
import sys
|
||||
|
||||
import kitty.fast_data_types as fast_data_types
|
||||
|
||||
@@ -20,6 +19,13 @@ def cwd_of_process(pid):
|
||||
return os.path.realpath(ans)
|
||||
|
||||
|
||||
def cmdline_of_process(pid):
|
||||
if is_macos:
|
||||
# TODO: macOS implementation, see DarwinProcess.c in htop for inspiration
|
||||
raise NotImplementedError()
|
||||
return open('/proc/{}/cmdline'.format(pid), 'rb').read().decode('utf-8').split('\0')
|
||||
|
||||
|
||||
def remove_cloexec(fd):
|
||||
fcntl.fcntl(fd, fcntl.F_SETFD, fcntl.fcntl(fd, fcntl.F_GETFD) & ~fcntl.FD_CLOEXEC)
|
||||
|
||||
@@ -55,44 +61,34 @@ class Child:
|
||||
if stdin is not None:
|
||||
stdin_read_fd, stdin_write_fd = os.pipe()
|
||||
remove_cloexec(stdin_read_fd)
|
||||
env = self.env
|
||||
pid = os.fork()
|
||||
if pid == 0: # child
|
||||
try:
|
||||
os.chdir(self.cwd)
|
||||
except EnvironmentError:
|
||||
os.chdir('/')
|
||||
os.setsid()
|
||||
for i in range(3):
|
||||
if stdin is not None and i == 0:
|
||||
os.dup2(stdin_read_fd, i)
|
||||
os.close(stdin_read_fd), os.close(stdin_write_fd)
|
||||
else:
|
||||
os.dup2(slave, i)
|
||||
os.close(slave), os.close(master)
|
||||
os.closerange(3, 200)
|
||||
# Establish the controlling terminal (see man 7 credentials)
|
||||
os.close(os.open(os.ttyname(1), os.O_RDWR))
|
||||
os.environ.update(env)
|
||||
os.environ['TERM'] = self.opts.term
|
||||
os.environ['COLORTERM'] = 'truecolor'
|
||||
if os.path.isdir(terminfo_dir):
|
||||
os.environ['TERMINFO'] = terminfo_dir
|
||||
try:
|
||||
os.execvp(self.argv[0], self.argv)
|
||||
except Exception as err:
|
||||
# Report he failure and exec a shell instead so that
|
||||
# we are not left with a forked but not execed process
|
||||
print('Could not launch:', self.argv[0])
|
||||
print('\t', err)
|
||||
print('\nPress Enter to exit:', end=' ')
|
||||
sys.stdout.flush()
|
||||
os.execvp('/bin/sh', ['/bin/sh', '-c', 'read w'])
|
||||
else: # master
|
||||
os.close(slave)
|
||||
self.pid = pid
|
||||
self.child_fd = master
|
||||
if stdin is not None:
|
||||
os.close(stdin_read_fd)
|
||||
fast_data_types.thread_write(stdin_write_fd, stdin)
|
||||
return pid
|
||||
else:
|
||||
stdin_read_fd = stdin_write_fd = -1
|
||||
env = os.environ.copy()
|
||||
env.update(self.env)
|
||||
env['TERM'] = self.opts.term
|
||||
env['COLORTERM'] = 'truecolor'
|
||||
if os.path.isdir(terminfo_dir):
|
||||
env['TERMINFO'] = terminfo_dir
|
||||
env = tuple('{}={}'.format(k, v) for k, v in env.items())
|
||||
pid = fast_data_types.spawn(self.cwd, tuple(self.argv), env, master, slave, stdin_read_fd, stdin_write_fd)
|
||||
os.close(slave)
|
||||
self.pid = pid
|
||||
self.child_fd = master
|
||||
if stdin is not None:
|
||||
os.close(stdin_read_fd)
|
||||
fast_data_types.thread_write(stdin_write_fd, stdin)
|
||||
return pid
|
||||
|
||||
@property
|
||||
def cmdline(self):
|
||||
try:
|
||||
return cmdline_of_process(self.pid)
|
||||
except Exception:
|
||||
return list(self.argv)
|
||||
|
||||
@property
|
||||
def current_cwd(self):
|
||||
try:
|
||||
return cwd_of_process(self.pid)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
15
kitty/cli.py
15
kitty/cli.py
@@ -206,7 +206,7 @@ def prettify(text):
|
||||
def sub(m):
|
||||
t = m.group(2)
|
||||
for ch in m.group(1):
|
||||
t = {'C': cyan, '_': italic, '*': bold}[ch](t)
|
||||
t = {'C': cyan, '_': italic, '*': bold, 'G': green, 'T': title}[ch](t)
|
||||
return t
|
||||
|
||||
text = re.sub(r'[|]([a-zA-Z_*]+?) (.+?)[|]', sub, text)
|
||||
@@ -263,18 +263,25 @@ def print_help_for_seq(seq, usage, message, appname):
|
||||
if leading_indent is None:
|
||||
leading_indent = indent
|
||||
j = '\n' + (' ' * indent)
|
||||
a((' ' * leading_indent) + j.join(wrap(text, limit=linesz - indent)))
|
||||
lines = []
|
||||
for l in text.splitlines():
|
||||
if l:
|
||||
lines.extend(wrap(l, limit=linesz - indent))
|
||||
else:
|
||||
lines.append('')
|
||||
a((' ' * leading_indent) + j.join(lines))
|
||||
|
||||
usage = usage or '[program-to-run ...]'
|
||||
a('{}: {} [options] {}'.format(title('Usage'), bold(yellow(appname)), usage))
|
||||
a('')
|
||||
message = message or (
|
||||
'Run the |_ {appname}| terminal emulator. You can also specify the |_ program| to run inside |_ {appname}| as normal'
|
||||
'Run the |G {appname}| terminal emulator. You can also specify the |_ program| to run inside |_ {appname}| as normal'
|
||||
' arguments following the |_ options|. For example: {appname} /bin/sh'
|
||||
).format(appname=appname)
|
||||
wa(prettify(message))
|
||||
a('')
|
||||
a('{}:'.format(title('Options')))
|
||||
if seq:
|
||||
a('{}:'.format(title('Options')))
|
||||
for opt in seq:
|
||||
if isinstance(opt, str):
|
||||
a('{}:'.format(title(opt)))
|
||||
|
||||
@@ -12,13 +12,15 @@ import tempfile
|
||||
from collections import namedtuple
|
||||
|
||||
from . import fast_data_types as defines
|
||||
from .config_utils import (
|
||||
init_config, parse_config_base, positive_float, positive_int, to_bool,
|
||||
to_color, unit_float
|
||||
)
|
||||
from .constants import config_dir
|
||||
from .fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE
|
||||
from .layout import all_layouts
|
||||
from .rgb import to_color
|
||||
from .utils import safe_print
|
||||
|
||||
key_pat = re.compile(r'([a-zA-Z][a-zA-Z0-9_-]*)\s+(.+)$')
|
||||
MINIMUM_FONT_SIZE = 4
|
||||
|
||||
|
||||
@@ -44,14 +46,10 @@ def to_cursor_shape(x):
|
||||
)
|
||||
|
||||
|
||||
def to_bool(x):
|
||||
return x.lower() in 'y yes true'.split()
|
||||
|
||||
|
||||
def parse_mods(parts):
|
||||
|
||||
def map_mod(m):
|
||||
return {'CTRL': 'CONTROL', 'CMD': 'CONTROL'}.get(m, m)
|
||||
return {'CTRL': 'CONTROL', 'CMD': 'SUPER', 'OPTION': 'ALT'}.get(m, m)
|
||||
|
||||
mods = 0
|
||||
for m in parts:
|
||||
@@ -174,7 +172,7 @@ def parse_symbol_map(val):
|
||||
|
||||
|
||||
def parse_send_text_bytes(text):
|
||||
return ast.literal_eval("'''" + text + "'''").encode('utf-8')
|
||||
return ast.literal_eval("'''" + text.replace("'''", "'\\''") + "'''").encode('utf-8')
|
||||
|
||||
|
||||
def parse_send_text(val, keymap):
|
||||
@@ -209,18 +207,6 @@ def to_layout_names(raw):
|
||||
return parts
|
||||
|
||||
|
||||
def positive_int(x):
|
||||
return max(0, int(x))
|
||||
|
||||
|
||||
def positive_float(x):
|
||||
return max(0, float(x))
|
||||
|
||||
|
||||
def unit_float(x):
|
||||
return max(0, min(float(x), 1))
|
||||
|
||||
|
||||
def adjust_line_height(x):
|
||||
if x.endswith('%'):
|
||||
return float(x[:-1].strip()) / 100.0
|
||||
@@ -240,7 +226,7 @@ def tab_separator(x):
|
||||
x = x[1:-1]
|
||||
break
|
||||
if not x.strip():
|
||||
x = defaults.tab_separator
|
||||
x = ('\xa0' * len(x)) if x else defaults.tab_separator
|
||||
return x
|
||||
|
||||
|
||||
@@ -248,6 +234,10 @@ def tab_font_style(x):
|
||||
return {'bold-italic': (True, True), 'bold': (True, False), 'italic': (False, True)}.get(x.lower().replace('_', '-'), (False, False))
|
||||
|
||||
|
||||
def tab_bar_edge(x):
|
||||
return {'top': 1, 'bottom': 3}.get(x.lower(), 3)
|
||||
|
||||
|
||||
def url_style(x):
|
||||
return url_style.map.get(x, url_style.map['curly'])
|
||||
|
||||
@@ -256,6 +246,7 @@ url_style.map = dict(((v, i) for i, v in enumerate('none single double curly'.sp
|
||||
|
||||
|
||||
type_map = {
|
||||
'allow_remote_control': to_bool,
|
||||
'adjust_line_height': adjust_line_height,
|
||||
'scrollback_lines': positive_int,
|
||||
'scrollback_pager': shlex.split,
|
||||
@@ -294,18 +285,37 @@ type_map = {
|
||||
'inactive_text_alpha': unit_float,
|
||||
'url_style': url_style,
|
||||
'prefer_color_emoji': to_bool,
|
||||
'copy_on_select': to_bool,
|
||||
'tab_bar_edge': tab_bar_edge,
|
||||
}
|
||||
|
||||
for name in (
|
||||
'foreground background cursor active_border_color inactive_border_color'
|
||||
' selection_foreground selection_background url_color'
|
||||
).split():
|
||||
type_map[name] = lambda x: to_color(x, validate=True)
|
||||
type_map[name] = to_color
|
||||
for i in range(16):
|
||||
type_map['color%d' % i] = lambda x: to_color(x, validate=True)
|
||||
type_map['color%d' % i] = to_color
|
||||
for a in ('active', 'inactive'):
|
||||
for b in ('foreground', 'background'):
|
||||
type_map['%s_tab_%s' % (a, b)] = lambda x: to_color(x, validate=True)
|
||||
type_map['%s_tab_%s' % (a, b)] = to_color
|
||||
|
||||
|
||||
def special_handling(key, val, ans):
|
||||
if key == 'map':
|
||||
parse_key(val, ans['keymap'])
|
||||
return True
|
||||
if key == 'symbol_map':
|
||||
ans['symbol_map'].update(parse_symbol_map(val))
|
||||
return True
|
||||
if key == 'send_text':
|
||||
# For legacy compatibility
|
||||
parse_send_text(val, ans['keymap'])
|
||||
return True
|
||||
|
||||
|
||||
defaults = None
|
||||
default_config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'kitty.conf')
|
||||
|
||||
|
||||
def parse_config(lines, check_keys=True):
|
||||
@@ -313,45 +323,11 @@ def parse_config(lines, check_keys=True):
|
||||
'keymap': {},
|
||||
'symbol_map': {},
|
||||
}
|
||||
if check_keys:
|
||||
all_keys = defaults._asdict()
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
m = key_pat.match(line)
|
||||
if m is not None:
|
||||
key, val = m.groups()
|
||||
if key == 'map':
|
||||
parse_key(val, ans['keymap'])
|
||||
continue
|
||||
if key == 'symbol_map':
|
||||
ans['symbol_map'].update(parse_symbol_map(val))
|
||||
continue
|
||||
if key == 'send_text':
|
||||
# For legacy compatibility
|
||||
parse_send_text(val, ans['keymap'])
|
||||
continue
|
||||
if check_keys:
|
||||
if key not in all_keys:
|
||||
safe_print(
|
||||
'Ignoring unknown config key: {}'.format(key),
|
||||
file=sys.stderr
|
||||
)
|
||||
continue
|
||||
tm = type_map.get(key)
|
||||
if tm is not None:
|
||||
val = tm(val)
|
||||
ans[key] = val
|
||||
parse_config_base(lines, defaults, type_map, special_handling, ans, check_keys=check_keys)
|
||||
return ans
|
||||
|
||||
|
||||
with open(
|
||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'kitty.conf'), 'rb'
|
||||
) as f:
|
||||
defaults = parse_config(f.read().decode('utf-8').splitlines(), check_keys=False)
|
||||
Options = namedtuple('Defaults', ','.join(defaults.keys()))
|
||||
defaults = Options(**defaults)
|
||||
Options, defaults = init_config(default_config_path, parse_config)
|
||||
actions = frozenset(a.func for a in defaults.keymap.values()) | frozenset(
|
||||
'combine send_text goto_tab new_tab_with_cwd new_window_with_cwd new_os_window_with_cwd'.split()
|
||||
)
|
||||
|
||||
67
kitty/config_utils.py
Normal file
67
kitty/config_utils.py
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import re
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
|
||||
from .utils import safe_print
|
||||
from .rgb import to_color as as_color
|
||||
|
||||
key_pat = re.compile(r'([a-zA-Z][a-zA-Z0-9_-]*)\s+(.+)$')
|
||||
|
||||
|
||||
def to_color(x):
|
||||
return as_color(x, validate=True)
|
||||
|
||||
|
||||
def positive_int(x):
|
||||
return max(0, int(x))
|
||||
|
||||
|
||||
def positive_float(x):
|
||||
return max(0, float(x))
|
||||
|
||||
|
||||
def unit_float(x):
|
||||
return max(0, min(float(x), 1))
|
||||
|
||||
|
||||
def to_bool(x):
|
||||
return x.lower() in 'y yes true'.split()
|
||||
|
||||
|
||||
def parse_config_base(
|
||||
lines, defaults, type_map, special_handling, ans, check_keys=True
|
||||
):
|
||||
if check_keys:
|
||||
all_keys = defaults._asdict()
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
m = key_pat.match(line)
|
||||
if m is not None:
|
||||
key, val = m.groups()
|
||||
if special_handling(key, val, ans):
|
||||
continue
|
||||
if check_keys:
|
||||
if key not in all_keys:
|
||||
safe_print(
|
||||
'Ignoring unknown config key: {}'.format(key),
|
||||
file=sys.stderr
|
||||
)
|
||||
continue
|
||||
tm = type_map.get(key)
|
||||
if tm is not None:
|
||||
val = tm(val)
|
||||
ans[key] = val
|
||||
|
||||
|
||||
def init_config(defaults_path, parse_config):
|
||||
with open(defaults_path, encoding='utf-8') as f:
|
||||
defaults = parse_config(f.read().splitlines(), check_keys=False)
|
||||
Options = namedtuple('Defaults', ','.join(defaults.keys()))
|
||||
defaults = Options(**defaults)
|
||||
return Options, defaults
|
||||
@@ -10,7 +10,7 @@ from collections import namedtuple
|
||||
from .fast_data_types import set_boss as set_c_boss
|
||||
|
||||
appname = 'kitty'
|
||||
version = (0, 6, 1)
|
||||
version = (0, 7, 0)
|
||||
str_version = '.'.join(map(str, version))
|
||||
_plat = sys.platform.lower()
|
||||
is_macos = 'darwin' in _plat
|
||||
|
||||
@@ -180,7 +180,7 @@ find_substitute_face(CFStringRef str, CTFontRef old_font) {
|
||||
PyObject*
|
||||
create_fallback_face(PyObject *base_face, Cell* cell, bool UNUSED bold, bool UNUSED italic) {
|
||||
CTFace *self = (CTFace*)base_face;
|
||||
char text[128] = {0};
|
||||
char text[256] = {0};
|
||||
cell_as_utf8(cell, true, text, ' ');
|
||||
CFStringRef str = CFStringCreateWithCString(NULL, text, kCFStringEncodingUTF8);
|
||||
if (str == NULL) return PyErr_NoMemory();
|
||||
@@ -200,6 +200,15 @@ glyph_id_for_codepoint(PyObject *s, char_type ch) {
|
||||
return glyphs[0];
|
||||
}
|
||||
|
||||
bool
|
||||
is_glyph_empty(PyObject *s, glyph_index g) {
|
||||
CTFace *self = (CTFace*)s;
|
||||
CGGlyph gg = g;
|
||||
CGRect bounds;
|
||||
CTFontGetBoundingRectsForGlyphs(self->ct_font, kCTFontOrientationHorizontal, &gg, &bounds, 1);
|
||||
return bounds.size.width <= 0;
|
||||
}
|
||||
|
||||
static inline float
|
||||
scaled_point_sz() {
|
||||
return ((global_state.logical_dpi_x + global_state.logical_dpi_y) / 144.0) * global_state.font_sz_in_pts;
|
||||
|
||||
@@ -180,10 +180,12 @@ extern bool init_fontconfig_library(PyObject*);
|
||||
extern bool init_desktop(PyObject*);
|
||||
extern bool init_fonts(PyObject*);
|
||||
extern bool init_glfw(PyObject *m);
|
||||
extern bool init_child(PyObject *m);
|
||||
extern bool init_state(PyObject *module);
|
||||
extern bool init_keys(PyObject *module);
|
||||
extern bool init_graphics(PyObject *module);
|
||||
extern bool init_shaders(PyObject *module);
|
||||
extern bool init_mouse(PyObject *module);
|
||||
#ifdef __APPLE__
|
||||
extern int init_CoreText(PyObject *);
|
||||
extern bool init_cocoa(PyObject *module);
|
||||
@@ -211,10 +213,12 @@ PyInit_fast_data_types(void) {
|
||||
if (!init_ColorProfile(m)) return NULL;
|
||||
if (!init_Screen(m)) return NULL;
|
||||
if (!init_glfw(m)) return NULL;
|
||||
if (!init_child(m)) return NULL;
|
||||
if (!init_state(m)) return NULL;
|
||||
if (!init_keys(m)) return NULL;
|
||||
if (!init_graphics(m)) return NULL;
|
||||
if (!init_shaders(m)) return NULL;
|
||||
if (!init_mouse(m)) return NULL;
|
||||
#ifdef __APPLE__
|
||||
if (!init_CoreText(m)) return NULL;
|
||||
if (!init_cocoa(m)) return NULL;
|
||||
@@ -227,7 +231,7 @@ PyInit_fast_data_types(void) {
|
||||
|
||||
#define OOF(n) #n, offsetof(Cell, n)
|
||||
if (PyModule_AddObject(m, "CELL", Py_BuildValue("{sI sI sI sI sI sI sI sI sI}",
|
||||
OOF(ch), OOF(fg), OOF(bg), OOF(decoration_fg), OOF(cc), OOF(sprite_x), OOF(sprite_y), OOF(sprite_z), "size", sizeof(Cell))) != 0) return NULL;
|
||||
OOF(ch), OOF(fg), OOF(bg), OOF(decoration_fg), OOF(cc_idx), OOF(sprite_x), OOF(sprite_y), OOF(sprite_z), "size", sizeof(Cell))) != 0) return NULL;
|
||||
#undef OOF
|
||||
PyModule_AddIntConstant(m, "BOLD", BOLD_SHIFT);
|
||||
PyModule_AddIntConstant(m, "ITALIC", ITALIC_SHIFT);
|
||||
|
||||
@@ -25,12 +25,13 @@
|
||||
#define MIN(x, y) (((x) > (y)) ? (y) : (x))
|
||||
#define xstr(s) str(s)
|
||||
#define str(s) #s
|
||||
#define arraysz(x) (sizeof(x)/sizeof(x[0]))
|
||||
#define fatal(...) { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); exit(EXIT_FAILURE); }
|
||||
|
||||
typedef unsigned long long id_type;
|
||||
typedef uint32_t char_type;
|
||||
typedef uint32_t color_type;
|
||||
typedef uint32_t combining_type;
|
||||
typedef uint16_t combining_type;
|
||||
typedef uint32_t pixel;
|
||||
typedef unsigned int index_type;
|
||||
typedef uint16_t sprite_index;
|
||||
@@ -55,8 +56,6 @@ typedef enum MouseShapes { BEAM, HAND, ARROW } MouseShape;
|
||||
#define REVERSE_SHIFT 6
|
||||
#define STRIKE_SHIFT 7
|
||||
#define COL_MASK 0xFFFFFFFF
|
||||
#define CC_MASK 0xFFFF
|
||||
#define CC_SHIFT 16
|
||||
#define UTF8_ACCEPT 0
|
||||
#define UTF8_REJECT 1
|
||||
#define DECORATION_FG_CODE 58
|
||||
@@ -133,11 +132,12 @@ typedef struct {
|
||||
} Region;
|
||||
|
||||
typedef struct {
|
||||
char_type ch;
|
||||
color_type fg, bg, decoration_fg;
|
||||
combining_type cc;
|
||||
sprite_index sprite_x, sprite_y, sprite_z;
|
||||
attrs_type attrs;
|
||||
// The following are only needed on the CPU, not the GPU
|
||||
char_type ch;
|
||||
combining_type cc_idx[2];
|
||||
} Cell;
|
||||
|
||||
typedef struct {
|
||||
@@ -278,7 +278,7 @@ void mouse_event(int, int);
|
||||
void focus_in_event();
|
||||
void wakeup_io_loop(bool);
|
||||
void scroll_event(double, double);
|
||||
void fake_scroll(bool);
|
||||
void fake_scroll(int, bool);
|
||||
void set_special_key_combo(int glfw_key, int mods);
|
||||
void on_text_input(unsigned int codepoint, int mods);
|
||||
void on_key_input(int key, int scancode, int action, int mods);
|
||||
|
||||
2
kitty/emoji.h
generated
2
kitty/emoji.h
generated
@@ -1,4 +1,4 @@
|
||||
// unicode data, built from the unicode standard on: 2017-12-20
|
||||
// unicode data, built from the unicode standard on: 2018-01-18
|
||||
// see gen-wcwidth.py
|
||||
#pragma once
|
||||
#include "data-types.h"
|
||||
|
||||
338
kitty/fonts.c
338
kitty/fonts.c
@@ -9,11 +9,11 @@
|
||||
#include "fonts.h"
|
||||
#include "state.h"
|
||||
#include "emoji.h"
|
||||
#include "unicode-data.h"
|
||||
|
||||
#define MISSING_GLYPH 4
|
||||
#define MAX_NUM_EXTRA_GLYPHS 8
|
||||
|
||||
typedef uint16_t glyph_index;
|
||||
typedef void (*send_sprite_to_gpu_func)(unsigned int, unsigned int, unsigned int, pixel*);
|
||||
send_sprite_to_gpu_func current_send_sprite_to_gpu = NULL;
|
||||
static PyObject *python_send_to_gpu_impl = NULL;
|
||||
@@ -37,11 +37,15 @@ struct SpritePosition {
|
||||
ExtraGlyphs extra_glyphs;
|
||||
};
|
||||
|
||||
#define SPECIAL_FILLED_MASK 1
|
||||
#define SPECIAL_VALUE_MASK 2
|
||||
#define EMPTY_FILLED_MASK 4
|
||||
#define EMPTY_VALUE_MASK 8
|
||||
|
||||
struct SpecialGlyphCache {
|
||||
SpecialGlyphCache *next;
|
||||
glyph_index glyph;
|
||||
bool is_special, filled;
|
||||
uint8_t data;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
@@ -52,7 +56,7 @@ typedef struct {
|
||||
|
||||
static GPUSpriteTracker sprite_tracker = {0};
|
||||
static hb_buffer_t *harfbuzz_buffer = NULL;
|
||||
static char_type shape_buffer[2048] = {0};
|
||||
static char_type shape_buffer[4096] = {0};
|
||||
|
||||
|
||||
typedef struct {
|
||||
@@ -151,13 +155,13 @@ sprite_position_for(Font *font, glyph_index glyph, ExtraGlyphs *extra_glyphs, ui
|
||||
return s;
|
||||
}
|
||||
|
||||
static SpecialGlyphCache*
|
||||
special_glyph_cache_for(Font *font, glyph_index glyph) {
|
||||
static inline SpecialGlyphCache*
|
||||
special_glyph_cache_for(Font *font, glyph_index glyph, uint8_t mask) {
|
||||
SpecialGlyphCache *s = font->special_glyph_cache + (glyph & 0x3ff);
|
||||
// Optimize for the common case of glyph under 1024 already in the cache
|
||||
if (LIKELY(s->glyph == glyph && s->filled)) return s; // Cache hit
|
||||
if (LIKELY(s->glyph == glyph && s->data & mask)) return s; // Cache hit
|
||||
while(true) {
|
||||
if (s->filled) {
|
||||
if (s->data & mask) {
|
||||
if (s->glyph == glyph) return s; // Cache hit
|
||||
} else {
|
||||
break;
|
||||
@@ -212,7 +216,7 @@ clear_sprite_map(Font *font) {
|
||||
|
||||
void
|
||||
clear_special_glyph_cache(Font *font) {
|
||||
#define CLEAR(s) s->filled = false; s->glyph = 0;
|
||||
#define CLEAR(s) s->data = 0; s->glyph = 0;
|
||||
SpecialGlyphCache *s;
|
||||
for (size_t i = 0; i < sizeof(font->special_glyph_cache)/sizeof(font->special_glyph_cache[0]); i++) {
|
||||
s = font->special_glyph_cache + i;
|
||||
@@ -324,10 +328,8 @@ face_has_codepoint(PyObject* face, char_type cp) {
|
||||
static inline bool
|
||||
has_cell_text(Font *self, Cell *cell) {
|
||||
if (!face_has_codepoint(self->face, cell->ch)) return false;
|
||||
if (cell->cc) {
|
||||
if (!face_has_codepoint(self->face, cell->cc & CC_MASK)) return false;
|
||||
char_type cc = cell->cc >> 16;
|
||||
if (cc && !face_has_codepoint(self->face, cc)) return false;
|
||||
for (unsigned i = 0; i < arraysz(cell->cc_idx) && cell->cc_idx[i]; i++) {
|
||||
if (!face_has_codepoint(self->face, codepoint_for_mark(cell->cc_idx[i]))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -491,14 +493,12 @@ load_hb_buffer(Cell *first_cell, index_type num_cells) {
|
||||
hb_buffer_clear_contents(harfbuzz_buffer);
|
||||
while (num_cells) {
|
||||
attrs_type prev_width = 0;
|
||||
for (num = 0; num_cells && num < sizeof(shape_buffer)/sizeof(shape_buffer[0]) - 20; first_cell++, num_cells--) {
|
||||
for (num = 0; num_cells && num < arraysz(shape_buffer) - 20 - arraysz(first_cell->cc_idx); first_cell++, num_cells--) {
|
||||
if (prev_width == 2) { prev_width = 0; continue; }
|
||||
shape_buffer[num++] = first_cell->ch;
|
||||
prev_width = first_cell->attrs & WIDTH_MASK;
|
||||
if (first_cell->cc) {
|
||||
shape_buffer[num++] = first_cell->cc & CC_MASK;
|
||||
combining_type cc2 = first_cell->cc >> 16;
|
||||
if (cc2) shape_buffer[num++] = cc2 & CC_MASK;
|
||||
for (unsigned i = 0; i < arraysz(first_cell->cc_idx) && first_cell->cc_idx[i]; i++) {
|
||||
shape_buffer[num++] = codepoint_for_mark(first_cell->cc_idx[i]);
|
||||
}
|
||||
}
|
||||
hb_buffer_add_utf32(harfbuzz_buffer, shape_buffer, num, 0, num);
|
||||
@@ -557,28 +557,84 @@ typedef struct {
|
||||
char_type current_codepoint;
|
||||
} CellData;
|
||||
|
||||
typedef struct {
|
||||
unsigned int first_glyph_idx, first_cell_idx, num_glyphs, num_cells;
|
||||
} Group;
|
||||
|
||||
typedef struct {
|
||||
uint32_t previous_cluster;
|
||||
bool prev_was_special, prev_was_empty;
|
||||
CellData current_cell_data;
|
||||
Group *groups;
|
||||
size_t groups_capacity, group_idx, glyph_idx, cell_idx, num_cells, num_glyphs;
|
||||
Cell *first_cell, *last_cell;
|
||||
hb_glyph_info_t *info;
|
||||
hb_glyph_position_t *positions;
|
||||
} GroupState;
|
||||
|
||||
static GroupState group_state = {0};
|
||||
|
||||
static inline unsigned int
|
||||
num_codepoints_in_cell(Cell *cell) {
|
||||
unsigned int ans = 1;
|
||||
for (unsigned i = 0; i < arraysz(cell->cc_idx) && cell->cc_idx[i]; i++) ans++;
|
||||
return ans;
|
||||
}
|
||||
|
||||
static inline void
|
||||
shape(Cell *first_cell, index_type num_cells, hb_font_t *font) {
|
||||
if (group_state.groups_capacity <= 2 * num_cells) {
|
||||
group_state.groups_capacity = MAX(128, 2 * num_cells); // avoid unnecessary reallocs
|
||||
group_state.groups = realloc(group_state.groups, sizeof(Group) * group_state.groups_capacity);
|
||||
if (!group_state.groups) fatal("Out of memory");
|
||||
}
|
||||
group_state.previous_cluster = UINT32_MAX;
|
||||
group_state.prev_was_special = false;
|
||||
group_state.prev_was_empty = false;
|
||||
group_state.current_cell_data.cell = first_cell; group_state.current_cell_data.num_codepoints = num_codepoints_in_cell(first_cell); group_state.current_cell_data.codepoints_consumed = 0; group_state.current_cell_data.current_codepoint = first_cell->ch;
|
||||
memset(group_state.groups, 0, sizeof(Group) * group_state.groups_capacity);
|
||||
group_state.group_idx = 0;
|
||||
group_state.glyph_idx = 0;
|
||||
group_state.cell_idx = 0;
|
||||
group_state.num_cells = num_cells;
|
||||
group_state.first_cell = first_cell;
|
||||
group_state.last_cell = first_cell + (num_cells ? num_cells - 1 : 0);
|
||||
load_hb_buffer(first_cell, num_cells);
|
||||
hb_shape(font, harfbuzz_buffer, NULL, 0);
|
||||
unsigned int info_length, positions_length;
|
||||
group_state.info = hb_buffer_get_glyph_infos(harfbuzz_buffer, &info_length);
|
||||
group_state.positions = hb_buffer_get_glyph_positions(harfbuzz_buffer, &positions_length);
|
||||
if (!group_state.info || !group_state.positions) group_state.num_glyphs = 0;
|
||||
else group_state.num_glyphs = MIN(info_length, positions_length);
|
||||
}
|
||||
|
||||
static inline bool
|
||||
is_special_glyph(glyph_index glyph_id, Font *font, CellData* cell_data) {
|
||||
// A glyph is special if the codepoint it corresponds to matches a
|
||||
// different glyph in the font
|
||||
SpecialGlyphCache *s = special_glyph_cache_for(font, glyph_id);
|
||||
SpecialGlyphCache *s = special_glyph_cache_for(font, glyph_id, SPECIAL_FILLED_MASK);
|
||||
if (s == NULL) return false;
|
||||
if (!s->filled) {
|
||||
s->is_special = cell_data->current_codepoint ? (
|
||||
if (!(s->data & SPECIAL_FILLED_MASK)) {
|
||||
bool is_special = cell_data->current_codepoint ? (
|
||||
glyph_id != glyph_id_for_codepoint(font->face, cell_data->current_codepoint) ? true : false)
|
||||
:
|
||||
false;
|
||||
s->filled = true;
|
||||
uint8_t val = is_special ? SPECIAL_VALUE_MASK : 0;
|
||||
s->data |= val | SPECIAL_FILLED_MASK;
|
||||
}
|
||||
return s->is_special;
|
||||
return s->data & SPECIAL_VALUE_MASK;
|
||||
}
|
||||
|
||||
static inline unsigned int
|
||||
num_codepoints_in_cell(Cell *cell) {
|
||||
unsigned int ans = 1;
|
||||
if (cell->cc) ans += ((cell->cc >> CC_SHIFT) & CC_MASK) ? 2 : 1;
|
||||
return ans;
|
||||
static inline bool
|
||||
is_empty_glyph(glyph_index glyph_id, Font *font) {
|
||||
// A glyph is empty if its metrics have a width of zero
|
||||
SpecialGlyphCache *s = special_glyph_cache_for(font, glyph_id, EMPTY_FILLED_MASK);
|
||||
if (s == NULL) return false;
|
||||
if (!(s->data & EMPTY_FILLED_MASK)) {
|
||||
uint8_t val = is_glyph_empty(font->face, glyph_id) ? EMPTY_VALUE_MASK : 0;
|
||||
s->data |= val | EMPTY_FILLED_MASK;
|
||||
}
|
||||
return s->data & EMPTY_VALUE_MASK;
|
||||
}
|
||||
|
||||
static inline unsigned int
|
||||
@@ -598,100 +654,140 @@ check_cell_consumed(CellData *cell_data, Cell *last_cell) {
|
||||
case 0:
|
||||
cell_data->current_codepoint = cell_data->cell->ch;
|
||||
break;
|
||||
case 1:
|
||||
cell_data->current_codepoint = cell_data->cell->cc & CC_MASK;
|
||||
break;
|
||||
case 2:
|
||||
cell_data->current_codepoint = (cell_data->cell->cc >> CC_SHIFT) & CC_MASK;
|
||||
break;
|
||||
default:
|
||||
cell_data->current_codepoint = 0;
|
||||
cell_data->current_codepoint = codepoint_for_mark(cell_data->cell->cc_idx[cell_data->codepoints_consumed - 1]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline glyph_index
|
||||
next_group(Font *font, unsigned int *num_group_cells, unsigned int *num_group_glyphs, Cell *cells, hb_glyph_info_t *info, unsigned int max_num_glyphs, unsigned int max_num_cells, ExtraGlyphs *extra_glyphs) {
|
||||
// See https://github.com/behdad/harfbuzz/issues/615 for a discussion of
|
||||
// how to break text into cells. In addition, we have to deal with
|
||||
// monospace ligature fonts that use dummy glyphs of zero size to implement
|
||||
// their ligatures.
|
||||
|
||||
CellData cell_data;
|
||||
cell_data.cell = cells; cell_data.num_codepoints = num_codepoints_in_cell(cells); cell_data.codepoints_consumed = 0; cell_data.current_codepoint = cells->ch;
|
||||
#define LIMIT (MAX_NUM_EXTRA_GLYPHS + 1)
|
||||
glyph_index glyphs_in_group[LIMIT];
|
||||
unsigned int ncells = 0, nglyphs = 0, n;
|
||||
uint32_t previous_cluster = UINT32_MAX, cluster;
|
||||
Cell *last_cell = cells + max_num_cells;
|
||||
unsigned int cell_limit = MIN(max_num_cells, LIMIT + 1), glyph_limit = MIN(LIMIT, max_num_glyphs);
|
||||
bool is_special, prev_was_special = false;
|
||||
|
||||
while(nglyphs < glyph_limit && ncells < cell_limit) {
|
||||
glyph_index glyph_id = info[nglyphs].codepoint;
|
||||
cluster = info[nglyphs].cluster;
|
||||
is_special = is_special_glyph(glyph_id, font, &cell_data);
|
||||
if (prev_was_special && !is_special) break;
|
||||
glyphs_in_group[nglyphs++] = glyph_id;
|
||||
// Soak up a number of codepoints indicated by the difference in cluster numbers.
|
||||
if (cluster > previous_cluster || nglyphs == 1) {
|
||||
n = nglyphs == 1 ? 1 : cluster - previous_cluster;
|
||||
unsigned int before = ncells;
|
||||
while(n-- && ncells < max_num_cells) ncells += check_cell_consumed(&cell_data, last_cell);
|
||||
if (ncells > before && !is_special) break;
|
||||
}
|
||||
previous_cluster = cluster;
|
||||
prev_was_special = is_special;
|
||||
}
|
||||
*num_group_cells = MAX(1, MIN(ncells, cell_limit));
|
||||
*num_group_glyphs = MAX(1, MIN(nglyphs, glyph_limit));
|
||||
memset(extra_glyphs, 0, sizeof(ExtraGlyphs));
|
||||
|
||||
switch(nglyphs) {
|
||||
case 0:
|
||||
case 1:
|
||||
break;
|
||||
default:
|
||||
memcpy(&extra_glyphs->data, glyphs_in_group + 1, sizeof(glyph_index) * MIN(MAX_NUM_EXTRA_GLYPHS, nglyphs - 1));
|
||||
break;
|
||||
}
|
||||
#undef LIMIT
|
||||
return glyphs_in_group[0];
|
||||
}
|
||||
|
||||
static inline unsigned int
|
||||
shape(Cell *first_cell, index_type num_cells, hb_font_t *font, hb_glyph_info_t **info, hb_glyph_position_t **positions) {
|
||||
load_hb_buffer(first_cell, num_cells);
|
||||
hb_shape(font, harfbuzz_buffer, NULL, 0);
|
||||
unsigned int info_length, positions_length;
|
||||
*info = hb_buffer_get_glyph_infos(harfbuzz_buffer, &info_length);
|
||||
*positions = hb_buffer_get_glyph_positions(harfbuzz_buffer, &positions_length);
|
||||
if (!info || !positions) return 0;
|
||||
return MIN(info_length, positions_length);
|
||||
}
|
||||
|
||||
|
||||
static inline void
|
||||
shape_run(Cell *first_cell, index_type num_cells, Font *font) {
|
||||
hb_glyph_info_t *info;
|
||||
hb_glyph_position_t *positions;
|
||||
unsigned int num_glyphs = shape(first_cell, num_cells, harfbuzz_font_for_face(font->face), &info, &positions);
|
||||
shape(first_cell, num_cells, harfbuzz_font_for_face(font->face));
|
||||
#if 0
|
||||
// You can also generate this easily using hb-shape --show-extents --cluster-level=1 --shapers=ot /path/to/font/file text
|
||||
hb_buffer_serialize_glyphs(harfbuzz_buffer, 0, num_glyphs, (char*)canvas, sizeof(pixel) * CELLS_IN_CANVAS * cell_width * cell_height, NULL, harfbuzz_font_for_face(font->face), HB_BUFFER_SERIALIZE_FORMAT_TEXT, HB_BUFFER_SERIALIZE_FLAG_DEFAULT | HB_BUFFER_SERIALIZE_FLAG_GLYPH_EXTENTS);
|
||||
hb_buffer_serialize_glyphs(harfbuzz_buffer, 0, group_state.num_glyphs, (char*)canvas, sizeof(pixel) * CELLS_IN_CANVAS * cell_width * cell_height, NULL, harfbuzz_font_for_face(font->face), HB_BUFFER_SERIALIZE_FORMAT_TEXT, HB_BUFFER_SERIALIZE_FLAG_DEFAULT | HB_BUFFER_SERIALIZE_FLAG_GLYPH_EXTENTS);
|
||||
printf("\n%s\n", (char*)canvas);
|
||||
clear_canvas();
|
||||
#endif
|
||||
unsigned int run_pos = 0, cell_pos = 0, num_group_glyphs, num_group_cells;
|
||||
ExtraGlyphs extra_glyphs;
|
||||
glyph_index first_glyph;
|
||||
while(run_pos < num_glyphs && cell_pos < num_cells) {
|
||||
first_glyph = next_group(font, &num_group_cells, &num_group_glyphs, first_cell + cell_pos, info + run_pos, num_glyphs - run_pos, num_cells - cell_pos, &extra_glyphs);
|
||||
/* printf("Group: num_group_cells: %u num_group_glyphs: %u\n", num_group_cells, num_group_glyphs); */
|
||||
render_group(num_group_cells, num_group_glyphs, first_cell + cell_pos, info + run_pos, positions + run_pos, font, first_glyph, &extra_glyphs);
|
||||
run_pos += num_group_glyphs; cell_pos += num_group_cells;
|
||||
/* Now distribute the glyphs into groups of cells
|
||||
* Considerations to keep in mind:
|
||||
* Group sizes should be as small as possible for best performance
|
||||
* Combining chars can result in multiple glyphs rendered into a single cell
|
||||
* Emoji and East Asian wide chars can cause a single glyph to be rendered over multiple cells
|
||||
* Ligature fonts, take two common approaches:
|
||||
* 1. ABC becomes EMPTY, EMPTY, WIDE GLYPH this means we have to render N glyphs in N cells (example Fira Code)
|
||||
* 2. ABC becomes WIDE GLYPH this means we have to render one glyph in N cells (example Operator Mono Lig)
|
||||
*
|
||||
* We rely on the cluster numbers from harfbuzz to tell us how many unicode codepoints a glyph corresponds to.
|
||||
* Then we check if the glyph is a ligature glyph (is_special_glyph) and if it is an empty glyph. These three
|
||||
* datapoints give us enough information to satisfy the constraint above, for a wide variety of fonts.
|
||||
*/
|
||||
uint32_t cluster, next_cluster;
|
||||
bool add_to_current_group;
|
||||
#define G(x) (group_state.x)
|
||||
#define MAX_GLYPHS_IN_GROUP (MAX_NUM_EXTRA_GLYPHS + 1)
|
||||
while (G(glyph_idx) < G(num_glyphs) && G(cell_idx) < G(num_cells)) {
|
||||
glyph_index glyph_id = G(info)[G(glyph_idx)].codepoint;
|
||||
cluster = G(info)[G(glyph_idx)].cluster;
|
||||
bool is_special = is_special_glyph(glyph_id, font, &G(current_cell_data));
|
||||
bool is_empty = is_special && is_empty_glyph(glyph_id, font);
|
||||
uint32_t num_codepoints_used_by_glyph = 0;
|
||||
bool is_last_glyph = G(glyph_idx) == G(num_glyphs) - 1;
|
||||
Group *current_group = G(groups) + G(group_idx);
|
||||
if (is_last_glyph) {
|
||||
num_codepoints_used_by_glyph = UINT32_MAX;
|
||||
} else {
|
||||
next_cluster = G(info)[G(glyph_idx) + 1].cluster;
|
||||
if (next_cluster > cluster) num_codepoints_used_by_glyph = next_cluster - cluster;
|
||||
}
|
||||
if (!current_group->num_glyphs) {
|
||||
add_to_current_group = true;
|
||||
} else {
|
||||
if (is_special) {
|
||||
add_to_current_group = G(prev_was_empty);
|
||||
} else {
|
||||
add_to_current_group = !G(prev_was_special);
|
||||
}
|
||||
}
|
||||
if (current_group->num_glyphs >= MAX_GLYPHS_IN_GROUP || current_group->num_cells >= MAX_GLYPHS_IN_GROUP) add_to_current_group = false;
|
||||
|
||||
if (!add_to_current_group) { G(group_idx)++; current_group = G(groups) + G(group_idx); }
|
||||
if (!current_group->num_glyphs++) {
|
||||
current_group->first_glyph_idx = G(glyph_idx);
|
||||
current_group->first_cell_idx = G(cell_idx);
|
||||
}
|
||||
#define MOVE_GLYPH_TO_NEXT_GROUP(start_cell_idx) { \
|
||||
current_group->num_glyphs--; \
|
||||
G(group_idx)++; current_group = G(groups) + G(group_idx); \
|
||||
current_group->first_cell_idx = start_cell_idx; \
|
||||
current_group->num_glyphs = 1; \
|
||||
current_group->first_glyph_idx = G(glyph_idx); \
|
||||
}
|
||||
if (is_last_glyph) {
|
||||
// soak up all remaining cells
|
||||
if (G(cell_idx) < G(num_cells)) {
|
||||
unsigned int num_left = G(num_cells) - G(cell_idx);
|
||||
if (current_group->num_cells + num_left > MAX_GLYPHS_IN_GROUP) MOVE_GLYPH_TO_NEXT_GROUP(G(cell_idx));
|
||||
current_group->num_cells += num_left;
|
||||
if (current_group->num_cells > MAX_GLYPHS_IN_GROUP) current_group->num_cells = MAX_GLYPHS_IN_GROUP; // leave any trailing cells empty
|
||||
G(cell_idx) += num_left;
|
||||
}
|
||||
} else {
|
||||
unsigned int num_cells_consumed = 0, start_cell_idx = G(cell_idx);
|
||||
while (num_codepoints_used_by_glyph && G(cell_idx) < G(num_cells)) {
|
||||
unsigned int w = check_cell_consumed(&G(current_cell_data), G(last_cell));
|
||||
G(cell_idx) += w;
|
||||
num_cells_consumed += w;
|
||||
num_codepoints_used_by_glyph--;
|
||||
}
|
||||
if (num_cells_consumed) {
|
||||
if (num_cells_consumed > MAX_GLYPHS_IN_GROUP) {
|
||||
// Nasty, a single glyph used more than MAX_GLYPHS_IN_GROUP cells, we cannot render this case correctly
|
||||
fprintf(stderr, "The glyph: %u needs more than %u cells, cannot render it\n", glyph_id, MAX_GLYPHS_IN_GROUP);
|
||||
current_group->num_glyphs--;
|
||||
while (num_cells_consumed) {
|
||||
G(group_idx)++; current_group = G(groups) + G(group_idx);
|
||||
current_group->num_glyphs = 1; current_group->first_glyph_idx = G(glyph_idx);
|
||||
current_group->num_cells = MIN(num_cells_consumed, MAX_GLYPHS_IN_GROUP);
|
||||
current_group->first_cell_idx = start_cell_idx;
|
||||
start_cell_idx += current_group->num_cells;
|
||||
num_cells_consumed -= current_group->num_cells;
|
||||
}
|
||||
} else {
|
||||
if (num_cells_consumed + current_group->num_cells > MAX_GLYPHS_IN_GROUP) MOVE_GLYPH_TO_NEXT_GROUP(start_cell_idx);
|
||||
current_group->num_cells += num_cells_consumed;
|
||||
if (!is_special) { // not a ligature, end the group
|
||||
G(group_idx)++; current_group = G(groups) + G(group_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
G(prev_was_special) = is_special;
|
||||
G(prev_was_empty) = is_empty;
|
||||
G(previous_cluster) = cluster;
|
||||
G(glyph_idx)++;
|
||||
}
|
||||
#undef MOVE_GLYPH_TO_NEXT_GROUP
|
||||
#undef MAX_GLYPHS_IN_GROUP
|
||||
}
|
||||
|
||||
static inline void
|
||||
render_groups(Font *font) {
|
||||
unsigned idx = 0;
|
||||
ExtraGlyphs ed;
|
||||
while (idx <= G(group_idx)) {
|
||||
Group *group = G(groups) + idx;
|
||||
if (!group->num_cells) break;
|
||||
/* printf("Group: idx: %u num_cells: %u num_glyphs: %u first_glyph_idx: %u first_cell_idx: %u total_num_glyphs: %zu\n", */
|
||||
/* idx, group->num_cells, group->num_glyphs, group->first_glyph_idx, group->first_cell_idx, group_state.num_glyphs); */
|
||||
glyph_index primary = group->num_glyphs ? G(info)[group->first_glyph_idx].codepoint : 0;
|
||||
for (unsigned i = 1; i < MIN(arraysz(ed.data) + 1, group->num_glyphs); i++) ed.data[i-1] = G(info)[group->first_glyph_idx + i].codepoint;
|
||||
render_group(group->num_cells, group->num_glyphs, G(first_cell) + group->first_cell_idx, G(info) + group->first_glyph_idx, G(positions) + group->first_glyph_idx, font, primary, &ed);
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -711,30 +807,32 @@ test_shape(PyObject UNUSED *self, PyObject *args) {
|
||||
font = calloc(1, sizeof(Font));
|
||||
font->face = face;
|
||||
}
|
||||
hb_glyph_info_t *info;
|
||||
hb_glyph_position_t *positions;
|
||||
unsigned int num_glyphs = shape(line->cells, num, harfbuzz_font_for_face(font->face), &info, &positions);
|
||||
shape_run(line->cells, num, font);
|
||||
|
||||
PyObject *ans = PyList_New(0);
|
||||
unsigned int run_pos = 0, cell_pos = 0, num_group_glyphs, num_group_cells;
|
||||
ExtraGlyphs extra_glyphs;
|
||||
unsigned int idx = 0;
|
||||
glyph_index first_glyph;
|
||||
while(run_pos < num_glyphs && cell_pos < num) {
|
||||
first_glyph = next_group(font, &num_group_cells, &num_group_glyphs, line->cells + cell_pos, info + run_pos, num_glyphs - run_pos, num - cell_pos, &extra_glyphs);
|
||||
while (idx <= G(group_idx)) {
|
||||
Group *group = G(groups) + idx;
|
||||
if (!group->num_cells) break;
|
||||
first_glyph = group->num_glyphs ? G(info)[group->first_glyph_idx].codepoint : 0;
|
||||
|
||||
PyObject *eg = PyTuple_New(MAX_NUM_EXTRA_GLYPHS);
|
||||
for (size_t g = 0; g < MAX_NUM_EXTRA_GLYPHS; g++) PyTuple_SET_ITEM(eg, g, Py_BuildValue("H", extra_glyphs.data[g]));
|
||||
PyList_Append(ans, Py_BuildValue("IIIN", num_group_cells, num_group_glyphs, first_glyph, eg));
|
||||
run_pos += num_group_glyphs; cell_pos += num_group_cells;
|
||||
for (size_t g = 0; g < MAX_NUM_EXTRA_GLYPHS; g++) PyTuple_SET_ITEM(eg, g, Py_BuildValue("H", g + 1 < group->num_glyphs ? G(info)[group->first_glyph_idx + g].codepoint : 0));
|
||||
PyList_Append(ans, Py_BuildValue("IIIN", group->num_cells, group->num_glyphs, first_glyph, eg));
|
||||
idx++;
|
||||
}
|
||||
if (face) { Py_CLEAR(face); free(font); }
|
||||
return ans;
|
||||
}
|
||||
#undef G
|
||||
|
||||
static inline void
|
||||
render_run(Cell *first_cell, index_type num_cells, ssize_t font_idx) {
|
||||
switch(font_idx) {
|
||||
default:
|
||||
shape_run(first_cell, num_cells, &fonts.fonts[font_idx]);
|
||||
render_groups(&fonts.fonts[font_idx]);
|
||||
break;
|
||||
case BLANK_FONT:
|
||||
while(num_cells--) set_sprite(first_cell++, 0, 0, 0);
|
||||
@@ -812,6 +910,7 @@ finalize(void) {
|
||||
for (size_t i = 0; i < fonts.fonts_count; i++) del_font(fonts.fonts + i);
|
||||
free(fonts.symbol_maps); free(fonts.fonts);
|
||||
if (harfbuzz_buffer) hb_buffer_destroy(harfbuzz_buffer);
|
||||
free(group_state.groups);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
@@ -945,12 +1044,11 @@ get_fallback_font(PyObject UNUSED *self, PyObject *args) {
|
||||
PyObject *text;
|
||||
int bold, italic;
|
||||
if (!PyArg_ParseTuple(args, "Upp", &text, &bold, &italic)) return NULL;
|
||||
static Py_UCS4 char_buf[16];
|
||||
if (!PyUnicode_AsUCS4(text, char_buf, sizeof(char_buf)/sizeof(char_buf[0]), 1)) return NULL;
|
||||
Cell cell = {0};
|
||||
static Py_UCS4 char_buf[2 + arraysz(cell.cc_idx)];
|
||||
if (!PyUnicode_AsUCS4(text, char_buf, arraysz(char_buf), 1)) return NULL;
|
||||
cell.ch = char_buf[0];
|
||||
if (PyUnicode_GetLength(text) > 1) cell.cc |= char_buf[1] & CC_MASK;
|
||||
if (PyUnicode_GetLength(text) > 2) cell.cc |= (char_buf[2] & CC_MASK) << 16;
|
||||
for (unsigned i = 0; i + 1 < PyUnicode_GetLength(text) && i < arraysz(cell.cc_idx); i++) cell.cc_idx[i] = mark_for_codepoint(char_buf[i + 1]);
|
||||
if (bold) cell.attrs |= 1 << BOLD_SHIFT;
|
||||
if (italic) cell.attrs |= 1 << ITALIC_SHIFT;
|
||||
ssize_t ans = fallback_font(&cell);
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
|
||||
|
||||
// API that font backends need to implement
|
||||
typedef uint16_t glyph_index;
|
||||
unsigned int glyph_id_for_codepoint(PyObject *, char_type);
|
||||
bool is_glyph_empty(PyObject *, glyph_index);
|
||||
hb_font_t* harfbuzz_font_for_face(PyObject*);
|
||||
bool set_size_for_face(PyObject*, unsigned int, bool);
|
||||
void cell_metrics(PyObject*, unsigned int*, unsigned int*, unsigned int*, unsigned int*, unsigned int*);
|
||||
|
||||
@@ -300,6 +300,17 @@ glyph_id_for_codepoint(PyObject *s, char_type cp) {
|
||||
return FT_Get_Char_Index(((Face*)s)->face, cp);
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
is_glyph_empty(PyObject *s, glyph_index g) {
|
||||
Face *self = (Face*)s;
|
||||
if (!load_glyph(self, g, FT_LOAD_DEFAULT)) { PyErr_Print(); return false; }
|
||||
#define M self->face->glyph->metrics
|
||||
/* printf("glyph: %u horiBearingX: %ld horiBearingY: %ld width: %ld height: %ld\n", g, M.horiBearingX, M.horiBearingY, M.width, M.height); */
|
||||
return M.width == 0;
|
||||
#undef M
|
||||
}
|
||||
|
||||
hb_font_t*
|
||||
harfbuzz_font_for_face(PyObject *self) { return ((Face*)self)->harfbuzz_font; }
|
||||
|
||||
|
||||
@@ -59,6 +59,9 @@ gl_init() {
|
||||
ARB_TEST(texture_storage);
|
||||
#undef ARB_TEST
|
||||
glad_loaded = true;
|
||||
if (GLVersion.major < OPENGL_REQUIRED_VERSION_MAJOR || (GLVersion.major == OPENGL_REQUIRED_VERSION_MAJOR && GLVersion.minor < OPENGL_REQUIRED_VERSION_MINOR)) {
|
||||
fatal("OpenGL version is %d.%d, version >= 3.3 required for kitty", GLVersion.major, GLVersion.minor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
3
kitty/glfw-wrapper.c
generated
3
kitty/glfw-wrapper.c
generated
@@ -203,6 +203,9 @@ load_glfw(const char* path) {
|
||||
*(void **) (&glfwSetFramebufferSizeCallback_impl) = dlsym(handle, "glfwSetFramebufferSizeCallback");
|
||||
if (glfwSetFramebufferSizeCallback_impl == NULL) fail("Failed to load glfw function glfwSetFramebufferSizeCallback with error: %s", dlerror());
|
||||
|
||||
*(void **) (&glfwSetWindowContentScaleCallback_impl) = dlsym(handle, "glfwSetWindowContentScaleCallback");
|
||||
if (glfwSetWindowContentScaleCallback_impl == NULL) fail("Failed to load glfw function glfwSetWindowContentScaleCallback with error: %s", dlerror());
|
||||
|
||||
*(void **) (&glfwPollEvents_impl) = dlsym(handle, "glfwPollEvents");
|
||||
if (glfwPollEvents_impl == NULL) fail("Failed to load glfw function glfwPollEvents with error: %s", dlerror());
|
||||
|
||||
|
||||
23
kitty/glfw-wrapper.h
generated
23
kitty/glfw-wrapper.h
generated
@@ -572,6 +572,7 @@
|
||||
* [window attribute](@ref GLFW_TRANSPARENT_FRAMEBUFFER_attrib).
|
||||
*/
|
||||
#define GLFW_TRANSPARENT_FRAMEBUFFER 0x0002000A
|
||||
#define GLFW_HOVERED 0x0002000B
|
||||
|
||||
/*! @brief Framebuffer bit depth hint.
|
||||
*
|
||||
@@ -1036,6 +1037,24 @@ typedef void (* GLFWwindowmaximizefun)(GLFWwindow*,int);
|
||||
*/
|
||||
typedef void (* GLFWframebuffersizefun)(GLFWwindow*,int,int);
|
||||
|
||||
/*! @brief The function signature for window content scale callbacks.
|
||||
*
|
||||
* This is the function signature for window content scale callback
|
||||
* functions.
|
||||
*
|
||||
* @param[in] window The window whose content scale changed.
|
||||
* @param[in] xscale The new x-axis content scale of the window.
|
||||
* @param[in] yscale The new y-axis content scale of the window.
|
||||
*
|
||||
* @sa @ref window_scale
|
||||
* @sa @ref glfwSetWindowContentScaleCallback
|
||||
*
|
||||
* @since Added in version 3.3.
|
||||
*
|
||||
* @ingroup window
|
||||
*/
|
||||
typedef void (* GLFWwindowcontentscalefun)(GLFWwindow*,float,float);
|
||||
|
||||
/*! @brief The function signature for mouse button callbacks.
|
||||
*
|
||||
* This is the function signature for mouse button callback functions.
|
||||
@@ -1620,6 +1639,10 @@ typedef GLFWframebuffersizefun (*glfwSetFramebufferSizeCallback_func)(GLFWwindow
|
||||
glfwSetFramebufferSizeCallback_func glfwSetFramebufferSizeCallback_impl;
|
||||
#define glfwSetFramebufferSizeCallback glfwSetFramebufferSizeCallback_impl
|
||||
|
||||
typedef GLFWwindowcontentscalefun (*glfwSetWindowContentScaleCallback_func)(GLFWwindow*, GLFWwindowcontentscalefun);
|
||||
glfwSetWindowContentScaleCallback_func glfwSetWindowContentScaleCallback_impl;
|
||||
#define glfwSetWindowContentScaleCallback glfwSetWindowContentScaleCallback_impl
|
||||
|
||||
typedef void (*glfwPollEvents_func)();
|
||||
glfwPollEvents_func glfwPollEvents_impl;
|
||||
#define glfwPollEvents glfwPollEvents_impl
|
||||
|
||||
45
kitty/glfw.c
45
kitty/glfw.c
@@ -143,6 +143,32 @@ scroll_callback(GLFWwindow *w, double xoffset, double yoffset) {
|
||||
global_state.callback_os_window = NULL;
|
||||
}
|
||||
|
||||
static struct {
|
||||
id_type entries[16];
|
||||
int next_entry;
|
||||
} focus_history;
|
||||
|
||||
#ifdef __APPLE__
|
||||
static inline id_type
|
||||
pop_focus_history() {
|
||||
int index = --focus_history.next_entry;
|
||||
if (index < 0) {
|
||||
focus_history.next_entry = index = 0;
|
||||
}
|
||||
|
||||
id_type result = focus_history.entries[index];
|
||||
focus_history.entries[index] = 0;
|
||||
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
static inline void
|
||||
push_focus_history(OSWindow *w) {
|
||||
focus_history.entries[focus_history.next_entry++] = w->id;
|
||||
focus_history.next_entry %= (sizeof(focus_history.entries) / sizeof(*(focus_history.entries)));
|
||||
}
|
||||
|
||||
static void
|
||||
window_focus_callback(GLFWwindow *w, int focused) {
|
||||
if (!set_callback_window(w)) return;
|
||||
@@ -150,6 +176,7 @@ window_focus_callback(GLFWwindow *w, int focused) {
|
||||
if (focused) {
|
||||
show_mouse_cursor(w);
|
||||
focus_in_event();
|
||||
push_focus_history(global_state.callback_os_window);
|
||||
}
|
||||
double now = monotonic();
|
||||
global_state.callback_os_window->last_mouse_activity_at = now;
|
||||
@@ -380,6 +407,7 @@ show_window(PyObject UNUSED *self, PyObject *args) {
|
||||
bool first_show = !w->shown_once;
|
||||
glfwShowWindow(w->handle);
|
||||
w->shown_once = true;
|
||||
push_focus_history(w);
|
||||
if (first_show) {
|
||||
double before_x = global_state.logical_dpi_x, before_y = global_state.logical_dpi_y;
|
||||
set_dpi_from_os_window(w);
|
||||
@@ -402,6 +430,23 @@ destroy_os_window(OSWindow *w) {
|
||||
if (current_os_window_ctx == w->handle) current_os_window_ctx = NULL;
|
||||
}
|
||||
w->handle = NULL;
|
||||
#ifdef __APPLE__
|
||||
// On macOS when closing a window, any other existing windows belonging to the same application do not
|
||||
// automatically get focus, so we do it manually.
|
||||
bool change_focus = true;
|
||||
while (change_focus) {
|
||||
id_type new_focus_id = pop_focus_history();
|
||||
if (new_focus_id == 0) break;
|
||||
for (size_t i = 0; i < global_state.num_os_windows; i++) {
|
||||
OSWindow *c = global_state.os_windows + i;
|
||||
if (c->id != w->id && c->handle && c->shown_once && (c->id == new_focus_id)) {
|
||||
glfwFocusWindow(c->handle);
|
||||
change_focus = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Global functions {{{
|
||||
|
||||
@@ -728,9 +728,14 @@ clear_filter_func(ImageRef *ref, Image UNUSED *img, const void UNUSED *data) {
|
||||
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) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
grman_clear(GraphicsManager *self) {
|
||||
filter_refs(self, NULL, true, clear_filter_func);
|
||||
grman_clear(GraphicsManager *self, bool all) {
|
||||
filter_refs(self, NULL, true, all ? clear_all_filter_func : clear_filter_func);
|
||||
}
|
||||
|
||||
static inline bool
|
||||
|
||||
@@ -83,7 +83,7 @@ typedef struct {
|
||||
} ScrollData;
|
||||
|
||||
GraphicsManager* grman_alloc();
|
||||
void grman_clear(GraphicsManager*);
|
||||
void grman_clear(GraphicsManager*, bool);
|
||||
const char* grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_t *payload, Cursor *c, bool *is_dirty);
|
||||
bool grman_update_layers(GraphicsManager *self, unsigned int scrolled_by, float screen_left, float screen_top, float dx, float dy, unsigned int num_cols, unsigned int num_rows);
|
||||
void grman_scroll_images(GraphicsManager *self, const ScrollData*);
|
||||
|
||||
@@ -90,6 +90,12 @@ historybuf_mark_line_dirty(HistoryBuf *self, index_type y) {
|
||||
self->line_attrs[index_of(self, y)] |= TEXT_DIRTY_MASK;
|
||||
}
|
||||
|
||||
inline void
|
||||
historybuf_clear(HistoryBuf *self) {
|
||||
self->count = 0;
|
||||
self->start_of_data = 0;
|
||||
}
|
||||
|
||||
static inline index_type
|
||||
historybuf_push(HistoryBuf *self) {
|
||||
index_type idx = (self->start_of_data + self->count) % self->ynum;
|
||||
|
||||
@@ -7,22 +7,20 @@ import fcntl
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import selectors
|
||||
import signal
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import termios
|
||||
import tty
|
||||
import zlib
|
||||
from base64 import standard_b64encode
|
||||
from collections import namedtuple
|
||||
from math import ceil, floor
|
||||
from tempfile import NamedTemporaryFile
|
||||
from time import monotonic
|
||||
|
||||
from kitty.constants import appname
|
||||
from kitty.cli import parse_args
|
||||
from kitty.utils import read_with_timeout
|
||||
|
||||
try:
|
||||
fsenc = sys.getfilesystemencoding() or 'utf-8'
|
||||
@@ -56,6 +54,13 @@ are in cells (i.e. cursor positions) with the origin |_ (0, 0)| at
|
||||
the top-left corner of the screen.
|
||||
|
||||
|
||||
--scale-up
|
||||
type=bool-set
|
||||
When used in combination with |_ --place| it will cause images that
|
||||
are smaller than the specified area to be scaled up to use as much
|
||||
of the specified area as possible.
|
||||
|
||||
|
||||
--clear
|
||||
type=bool-set
|
||||
Remove all images currently displayed on the screen.
|
||||
@@ -98,14 +103,18 @@ def options_spec():
|
||||
Size = namedtuple('Size', 'rows cols width height')
|
||||
|
||||
|
||||
def screen_size(refresh=False):
|
||||
if refresh or getattr(screen_size, 'ans', None) is None:
|
||||
def screen_size():
|
||||
if screen_size.changed:
|
||||
s = struct.pack('HHHH', 0, 0, 0, 0)
|
||||
x = fcntl.ioctl(1, termios.TIOCGWINSZ, s)
|
||||
screen_size.ans = Size(*struct.unpack('HHHH', x))
|
||||
screen_size.changed = False
|
||||
return screen_size.ans
|
||||
|
||||
|
||||
screen_size.changed = True
|
||||
|
||||
|
||||
def write_gr_cmd(cmd, payload=None):
|
||||
cmd = ','.join('{}={}'.format(k, v) for k, v in cmd.items())
|
||||
w = sys.stdout.buffer.write
|
||||
@@ -131,6 +140,17 @@ def fit_image(width, height, pwidth, pheight):
|
||||
return int(width), int(height)
|
||||
|
||||
|
||||
def calculate_in_cell_x_offset(width, cell_width, align):
|
||||
if align == 'left':
|
||||
return 0
|
||||
extra_pixels = width % cell_width
|
||||
if not extra_pixels:
|
||||
return 0
|
||||
if align == 'right':
|
||||
return cell_width - extra_pixels
|
||||
return (cell_width - extra_pixels) // 2
|
||||
|
||||
|
||||
def set_cursor(cmd, width, height, align):
|
||||
ss = screen_size()
|
||||
cw = int(ss.width / ss.cols)
|
||||
@@ -139,12 +159,9 @@ def set_cursor(cmd, width, height, align):
|
||||
w, h = fit_image(width, height, ss.width, height)
|
||||
ch = int(ss.height / ss.rows)
|
||||
num_of_rows_needed = int(ceil(height / ch))
|
||||
y_off = height % ch
|
||||
cmd['c'], cmd['r'] = ss.cols, num_of_rows_needed
|
||||
cmd['Y'] = y_off
|
||||
else:
|
||||
x_off = width % cw
|
||||
cmd['X'] = x_off
|
||||
cmd['X'] = calculate_in_cell_x_offset(width, cw, align)
|
||||
extra_cells = 0
|
||||
if align == 'center':
|
||||
extra_cells = (ss.cols - num_of_cells_needed) // 2
|
||||
@@ -159,8 +176,7 @@ def set_cursor_for_place(place, cmd, width, height, align):
|
||||
ss = screen_size()
|
||||
cw = int(ss.width / ss.cols)
|
||||
num_of_cells_needed = int(ceil(width / cw))
|
||||
x_off = width % cw
|
||||
cmd['X'] = x_off
|
||||
cmd['X'] = calculate_in_cell_x_offset(width, cw, align)
|
||||
extra_cells = 0
|
||||
if align == 'center':
|
||||
extra_cells = (place.width - num_of_cells_needed) // 2
|
||||
@@ -221,14 +237,29 @@ def identify(path):
|
||||
return ImageData(parts[0].lower(), int(parts[1]), int(parts[2]), mode)
|
||||
|
||||
|
||||
def convert(path, m, available_width, available_height):
|
||||
def convert(path, m, available_width, available_height, scale_up):
|
||||
width, height = m.width, m.height
|
||||
cmd = ['convert', '-background', 'none', path]
|
||||
if scale_up:
|
||||
if width < available_width:
|
||||
r = available_width / width
|
||||
width, height = available_width, int(height * r)
|
||||
if width > available_width or height > available_height:
|
||||
width, height = fit_image(width, height, available_width, available_height)
|
||||
cmd += ['-resize', '{}x{}'.format(width, height)]
|
||||
with NamedTemporaryFile(prefix='icat-', suffix='.' + m.mode, delete=False) as outfile:
|
||||
run_imagemagick(path, cmd + [outfile.name])
|
||||
# ImageMagick sometimes generated rgba images smaller than the specified
|
||||
# size. See https://github.com/kovidgoyal/kitty/issues/276 for examples
|
||||
sz = os.path.getsize(outfile.name)
|
||||
bytes_per_pixel = 3 if m.mode == 'rgb' else 4
|
||||
expected_size = bytes_per_pixel * width * height
|
||||
if sz < expected_size:
|
||||
missing = expected_size - sz
|
||||
if missing % (bytes_per_pixel * width) != 0:
|
||||
raise SystemExit('ImageMagick failed to convert {} correctly, it generated {} < {} of data'.format(path, sz, expected_size))
|
||||
height -= missing // (bytes_per_pixel * width)
|
||||
|
||||
return outfile.name, width, height
|
||||
|
||||
|
||||
@@ -238,6 +269,7 @@ def process(path, args):
|
||||
available_width = args.place.width * (ss.width / ss.cols) if args.place else ss.width
|
||||
available_height = args.place.height * (ss.height / ss.rows) if args.place else 10 * m.height
|
||||
needs_scaling = m.width > available_width or m.height > available_height
|
||||
needs_scaling = needs_scaling or args.scale_up
|
||||
if m.fmt == 'png' and not needs_scaling:
|
||||
outfile = path
|
||||
transmit_mode = 'f'
|
||||
@@ -246,7 +278,7 @@ def process(path, args):
|
||||
else:
|
||||
fmt = 24 if m.mode == 'rgb' else 32
|
||||
transmit_mode = 't'
|
||||
outfile, width, height = convert(path, m, available_width, available_height)
|
||||
outfile, width, height = convert(path, m, available_width, available_height, args.scale_up)
|
||||
show(outfile, width, height, fmt, transmit_mode, align=args.align, place=args.place)
|
||||
if not args.place:
|
||||
print() # ensure cursor is on a new line
|
||||
@@ -261,17 +293,11 @@ def scan(d):
|
||||
|
||||
|
||||
def detect_support(wait_for=10, silent=False):
|
||||
fd = sys.stdin.fileno()
|
||||
old = termios.tcgetattr(fd)
|
||||
oldfl = fcntl.fcntl(fd, fcntl.F_GETFL)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, oldfl | os.O_NONBLOCK)
|
||||
if not silent:
|
||||
print('Checking for graphics ({}s max. wait)...'.format(wait_for), end='\r')
|
||||
sys.stdout.flush()
|
||||
tty.setraw(fd)
|
||||
try:
|
||||
received = b''
|
||||
start_time = monotonic()
|
||||
responses = {}
|
||||
|
||||
def parse_responses():
|
||||
@@ -282,29 +308,20 @@ def detect_support(wait_for=10, silent=False):
|
||||
if iid not in responses:
|
||||
responses[iid] = m.group(2) == b'OK'
|
||||
|
||||
def read():
|
||||
def more_needed(data):
|
||||
nonlocal received
|
||||
d = sys.stdin.buffer.read()
|
||||
if not d: # EOF
|
||||
responses[1] = responses[2] = False
|
||||
return
|
||||
received += d
|
||||
received += data
|
||||
parse_responses()
|
||||
return 1 not in responses or 2 not in responses
|
||||
|
||||
with NamedTemporaryFile() as f:
|
||||
f.write(b'abcd'), f.flush()
|
||||
write_gr_cmd(dict(a='q', s=1, v=1, i=1), standard_b64encode(b'abcd'))
|
||||
write_gr_cmd(dict(a='q', s=1, v=1, i=2, t='f'), standard_b64encode(f.name.encode(fsenc)))
|
||||
sel = selectors.DefaultSelector()
|
||||
sel.register(sys.stdin, selectors.EVENT_READ, read)
|
||||
while monotonic() - start_time < wait_for and 1 not in responses and 2 not in responses:
|
||||
for key, mask in sel.select(0.1):
|
||||
read()
|
||||
read_with_timeout(more_needed, timeout=wait_for)
|
||||
finally:
|
||||
if not silent:
|
||||
sys.stdout.buffer.write(b'\033[J'), sys.stdout.flush()
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, oldfl)
|
||||
detect_support.has_files = bool(responses.get(2))
|
||||
return responses.get(1, False)
|
||||
|
||||
@@ -324,7 +341,7 @@ def main(args=sys.argv):
|
||||
' Directories are scanned recursively for image files.')
|
||||
args, items = parse_args(args[1:], options_spec, 'image-file ...', msg, '{} icat'.format(appname))
|
||||
|
||||
signal.signal(signal.SIGWINCH, lambda: screen_size(refresh=True))
|
||||
signal.signal(signal.SIGWINCH, lambda signum, frame: setattr(screen_size, 'changed', True))
|
||||
if not sys.stdout.isatty() or not sys.stdin.isatty():
|
||||
raise SystemExit(
|
||||
'Must be run in a terminal, stdout and/or stdin is currently not a terminal'
|
||||
|
||||
14
kitty/keys.c
14
kitty/keys.c
@@ -176,7 +176,9 @@ on_key_input(int key, int scancode, int action, int mods) {
|
||||
screen_history_scroll(screen, SCROLL_FULL, false); // scroll back to bottom
|
||||
}
|
||||
#ifdef __APPLE__
|
||||
if (!OPT(macos_option_as_alt) && IS_ALT_MODS(mods)) return;
|
||||
if (!OPT(macos_option_as_alt) && IS_ALT_MODS(mods) && !(
|
||||
key == GLFW_KEY_UP || key == GLFW_KEY_DOWN || key == GLFW_KEY_LEFT || key == GLFW_KEY_RIGHT) // alt+arrow keys do not generate text on macOS
|
||||
) return;
|
||||
#endif
|
||||
if (
|
||||
action == GLFW_PRESS ||
|
||||
@@ -188,12 +190,14 @@ on_key_input(int key, int scancode, int action, int mods) {
|
||||
}
|
||||
|
||||
void
|
||||
fake_scroll(bool upwards) {
|
||||
fake_scroll(int amount, bool upwards) {
|
||||
Window *w = active_window();
|
||||
if (!w) return;
|
||||
Screen *screen = w->render_data.screen;
|
||||
send_key_to_child(w, upwards ? GLFW_KEY_UP : GLFW_KEY_DOWN, 0, GLFW_PRESS);
|
||||
if (screen->modes.mEXTENDED_KEYBOARD) send_key_to_child(w, upwards ? GLFW_KEY_UP : GLFW_KEY_DOWN, 0, GLFW_RELEASE);
|
||||
int key = upwards ? GLFW_KEY_UP : GLFW_KEY_DOWN;
|
||||
while (amount-- > 0) {
|
||||
send_key_to_child(w, key, 0, GLFW_PRESS);
|
||||
send_key_to_child(w, key, 0, GLFW_RELEASE);
|
||||
}
|
||||
}
|
||||
|
||||
#define PYWRAP1(name) static PyObject* py##name(PyObject UNUSED *self, PyObject *args)
|
||||
|
||||
48
kitty/keys.h
generated
48
kitty/keys.h
generated
@@ -760,6 +760,14 @@ key_lookup(uint8_t key, KeyboardMode mode, uint8_t mods, uint8_t action) {
|
||||
} // end switch(key)
|
||||
case 0x3:
|
||||
switch(key & 0x7f) { default: return NULL;
|
||||
case 3: // MINUS
|
||||
return "\x01\x1f";
|
||||
case 5: // SLASH
|
||||
return "\x01\x7f";
|
||||
case 8: // 2
|
||||
return "\x01\x00";
|
||||
case 12: // 6
|
||||
return "\x01\x1e";
|
||||
case 50: // ESCAPE
|
||||
return "\x01\x1b";
|
||||
case 51: // ENTER
|
||||
@@ -920,7 +928,7 @@ key_lookup(uint8_t key, KeyboardMode mode, uint8_t mods, uint8_t action) {
|
||||
case 52: // TAB
|
||||
return "\x02\x1b\x09";
|
||||
case 53: // BACKSPACE
|
||||
return "\x02\x1b\x7f";
|
||||
return "\x01\x17";
|
||||
case 54: // INSERT
|
||||
return "\x06\x1b\x5b\x32\x3b\x33\x7e";
|
||||
case 55: // DELETE
|
||||
@@ -1071,7 +1079,7 @@ key_lookup(uint8_t key, KeyboardMode mode, uint8_t mods, uint8_t action) {
|
||||
case 52: // TAB
|
||||
return "\x03\x1b\x5b\x5a";
|
||||
case 53: // BACKSPACE
|
||||
return "\x02\x1b\x7f";
|
||||
return "\x01\x17";
|
||||
case 54: // INSERT
|
||||
return "\x06\x1b\x5b\x32\x3b\x34\x7e";
|
||||
case 55: // DELETE
|
||||
@@ -1958,6 +1966,14 @@ key_lookup(uint8_t key, KeyboardMode mode, uint8_t mods, uint8_t action) {
|
||||
} // end switch(key)
|
||||
case 0x3:
|
||||
switch(key & 0x7f) { default: return NULL;
|
||||
case 3: // MINUS
|
||||
return "\x01\x1f";
|
||||
case 5: // SLASH
|
||||
return "\x01\x7f";
|
||||
case 8: // 2
|
||||
return "\x01\x00";
|
||||
case 12: // 6
|
||||
return "\x01\x1e";
|
||||
case 50: // ESCAPE
|
||||
return "\x01\x1b";
|
||||
case 51: // ENTER
|
||||
@@ -2118,7 +2134,7 @@ key_lookup(uint8_t key, KeyboardMode mode, uint8_t mods, uint8_t action) {
|
||||
case 52: // TAB
|
||||
return "\x02\x1b\x09";
|
||||
case 53: // BACKSPACE
|
||||
return "\x02\x1b\x7f";
|
||||
return "\x01\x17";
|
||||
case 54: // INSERT
|
||||
return "\x06\x1b\x5b\x32\x3b\x33\x7e";
|
||||
case 55: // DELETE
|
||||
@@ -2269,7 +2285,7 @@ key_lookup(uint8_t key, KeyboardMode mode, uint8_t mods, uint8_t action) {
|
||||
case 52: // TAB
|
||||
return "\x03\x1b\x5b\x5a";
|
||||
case 53: // BACKSPACE
|
||||
return "\x02\x1b\x7f";
|
||||
return "\x01\x17";
|
||||
case 54: // INSERT
|
||||
return "\x06\x1b\x5b\x32\x3b\x34\x7e";
|
||||
case 55: // DELETE
|
||||
@@ -3164,6 +3180,14 @@ key_lookup(uint8_t key, KeyboardMode mode, uint8_t mods, uint8_t action) {
|
||||
} // end switch(key)
|
||||
case 0x3:
|
||||
switch(key & 0x7f) { default: return NULL;
|
||||
case 3: // MINUS
|
||||
return "\x01\x1f";
|
||||
case 5: // SLASH
|
||||
return "\x01\x7f";
|
||||
case 8: // 2
|
||||
return "\x01\x00";
|
||||
case 12: // 6
|
||||
return "\x01\x1e";
|
||||
case 50: // ESCAPE
|
||||
return "\x01\x1b";
|
||||
case 51: // ENTER
|
||||
@@ -3324,7 +3348,7 @@ key_lookup(uint8_t key, KeyboardMode mode, uint8_t mods, uint8_t action) {
|
||||
case 52: // TAB
|
||||
return "\x02\x1b\x09";
|
||||
case 53: // BACKSPACE
|
||||
return "\x02\x1b\x7f";
|
||||
return "\x01\x17";
|
||||
case 54: // INSERT
|
||||
return "\x06\x1b\x5b\x32\x3b\x33\x7e";
|
||||
case 55: // DELETE
|
||||
@@ -3475,7 +3499,7 @@ key_lookup(uint8_t key, KeyboardMode mode, uint8_t mods, uint8_t action) {
|
||||
case 52: // TAB
|
||||
return "\x03\x1b\x5b\x5a";
|
||||
case 53: // BACKSPACE
|
||||
return "\x02\x1b\x7f";
|
||||
return "\x01\x17";
|
||||
case 54: // INSERT
|
||||
return "\x06\x1b\x5b\x32\x3b\x34\x7e";
|
||||
case 55: // DELETE
|
||||
@@ -4362,6 +4386,14 @@ key_lookup(uint8_t key, KeyboardMode mode, uint8_t mods, uint8_t action) {
|
||||
} // end switch(key)
|
||||
case 0x3:
|
||||
switch(key & 0x7f) { default: return NULL;
|
||||
case 3: // MINUS
|
||||
return "\x01\x1f";
|
||||
case 5: // SLASH
|
||||
return "\x01\x7f";
|
||||
case 8: // 2
|
||||
return "\x01\x00";
|
||||
case 12: // 6
|
||||
return "\x01\x1e";
|
||||
case 50: // ESCAPE
|
||||
return "\x01\x1b";
|
||||
case 51: // ENTER
|
||||
@@ -4522,7 +4554,7 @@ key_lookup(uint8_t key, KeyboardMode mode, uint8_t mods, uint8_t action) {
|
||||
case 52: // TAB
|
||||
return "\x02\x1b\x09";
|
||||
case 53: // BACKSPACE
|
||||
return "\x02\x1b\x7f";
|
||||
return "\x01\x17";
|
||||
case 54: // INSERT
|
||||
return "\x06\x1b\x5b\x32\x3b\x33\x7e";
|
||||
case 55: // DELETE
|
||||
@@ -4673,7 +4705,7 @@ key_lookup(uint8_t key, KeyboardMode mode, uint8_t mods, uint8_t action) {
|
||||
case 52: // TAB
|
||||
return "\x03\x1b\x5b\x5a";
|
||||
case 53: // BACKSPACE
|
||||
return "\x02\x1b\x7f";
|
||||
return "\x01\x17";
|
||||
case 54: // INSERT
|
||||
return "\x06\x1b\x5b\x32\x3b\x34\x7e";
|
||||
case 55: // DELETE
|
||||
|
||||
@@ -26,10 +26,17 @@ def modify_complex_key(name, amt):
|
||||
|
||||
control_codes = {}
|
||||
smkx_key_map = {}
|
||||
alt_codes = {defines.GLFW_KEY_TAB: b'\033\t', defines.GLFW_KEY_ENTER: b'\033\r', defines.GLFW_KEY_ESCAPE: b'\033\033', defines.GLFW_KEY_BACKSPACE: b'\033\177'}
|
||||
alt_codes = {
|
||||
defines.GLFW_KEY_TAB: b'\033\t',
|
||||
defines.GLFW_KEY_ENTER: b'\033\r',
|
||||
defines.GLFW_KEY_ESCAPE: b'\033\033',
|
||||
# alt+bs matches iTerm and gnome-terminal
|
||||
defines.GLFW_KEY_BACKSPACE: b'\x17'
|
||||
}
|
||||
shift_alt_codes = alt_codes.copy()
|
||||
shift_alt_codes[defines.GLFW_KEY_TAB] = key_as_bytes('kcbt')
|
||||
alt_mods = (defines.GLFW_MOD_ALT, defines.GLFW_MOD_SHIFT | defines.GLFW_MOD_ALT)
|
||||
ctrl_shift_mod = defines.GLFW_MOD_SHIFT | defines.GLFW_MOD_CONTROL
|
||||
|
||||
for kf, kn in {
|
||||
defines.GLFW_KEY_UP: 'kcuu1',
|
||||
@@ -183,6 +190,18 @@ SHIFTED_PRINTABLE.update(pmap(
|
||||
"{|}~"
|
||||
))
|
||||
|
||||
ASCII_C0_SHIFTED = {
|
||||
# ^@
|
||||
'2': b'\x00',
|
||||
# ^^
|
||||
'6': b'\x1e',
|
||||
# ^_
|
||||
'MINUS': b'\x1f',
|
||||
# ^?
|
||||
'SLASH': b'\x7f',
|
||||
}
|
||||
CTRL_SHIFT_KEYS = {getattr(defines, 'GLFW_KEY_' + k): v for k, v in ASCII_C0_SHIFTED.items()}
|
||||
|
||||
|
||||
def key_to_bytes(key, smkx, extended, mods, action):
|
||||
if extended:
|
||||
@@ -191,6 +210,8 @@ def key_to_bytes(key, smkx, extended, mods, action):
|
||||
if mods == defines.GLFW_MOD_CONTROL and key in control_codes:
|
||||
# Map Ctrl-key to ascii control code
|
||||
data.extend(control_codes[key])
|
||||
elif mods == ctrl_shift_mod and key in CTRL_SHIFT_KEYS:
|
||||
data.extend(CTRL_SHIFT_KEYS[key])
|
||||
elif mods in alt_mods:
|
||||
if key in alt_codes:
|
||||
data.extend((alt_codes if mods == defines.GLFW_MOD_ALT else shift_alt_codes)[key])
|
||||
@@ -275,13 +296,13 @@ def generate_key_table():
|
||||
i += 1
|
||||
ind('case {}:'.format(mode))
|
||||
i += 1
|
||||
ind('switch(action & 3) { case 3: return NULL; ')
|
||||
ind('switch(action & 3) { case 3: return NULL;')
|
||||
for action in (defines.GLFW_RELEASE, defines.GLFW_PRESS, defines.GLFW_REPEAT):
|
||||
i += 1
|
||||
ind('case {}: // {}'.format(action, 'RELEASE PRESS REPEAT'.split()[action]))
|
||||
i += 1
|
||||
if action != defines.GLFW_RELEASE or mode == 'EXTENDED':
|
||||
ind('switch (mods & 0xf) { ')
|
||||
ind('switch (mods & 0xf) {')
|
||||
i += 1
|
||||
for mods in range(16):
|
||||
key_bytes = {}
|
||||
|
||||
@@ -104,6 +104,10 @@ click_interval 0.5
|
||||
# database will be matched.
|
||||
select_by_word_characters :@-./_~?&=%+#
|
||||
|
||||
# The shell program to execute. The default value of . means
|
||||
# to use whatever shell is set as the default shell for the current user.
|
||||
shell .
|
||||
|
||||
# Hide mouse cursor after the specified number of seconds of the mouse not being used. Set to
|
||||
# zero to disable mouse cursor hiding.
|
||||
mouse_hide_wait 3.0
|
||||
@@ -153,6 +157,13 @@ open_url_with default
|
||||
# rectangular block with the mouse)
|
||||
rectangle_select_modifiers ctrl+alt
|
||||
|
||||
|
||||
# Allow other programs to control kitty. If you turn this on other programs can
|
||||
# control all aspects of kitty, including sending text to kitty windows,
|
||||
# opening new windows, closing windows, reading the content of windows, etc.
|
||||
# Note that this even works over ssh connections.
|
||||
allow_remote_control no
|
||||
|
||||
# Choose whether to use the system implementation of wcwidth() (used to
|
||||
# control how many cells a character is rendered in). If you use the system
|
||||
# implementation, then kitty and any programs running in it will agree. The
|
||||
@@ -185,14 +196,19 @@ inactive_border_color #cccccc
|
||||
# zero and one, with 0 being fully faded).
|
||||
inactive_text_alpha 1.0
|
||||
|
||||
# Tab-bar customization
|
||||
# Which edge to show the tab bar on, top or bottom
|
||||
tab_bar_edge bottom
|
||||
|
||||
# The separator between tabs in the tab bar
|
||||
tab_separator " ┇"
|
||||
|
||||
# Tab bar colors and styles
|
||||
active_tab_foreground #000
|
||||
active_tab_background #eee
|
||||
active_tab_font_style bold-italic
|
||||
inactive_tab_foreground #444
|
||||
inactive_tab_background #999
|
||||
inactive_tab_font_style normal
|
||||
tab_separator " ┇"
|
||||
|
||||
|
||||
# The 16 terminal colors. There are 8 basic colors, each color has a dull and
|
||||
@@ -358,6 +374,13 @@ map ctrl+shift+f11 toggle_fullscreen
|
||||
|
||||
# OS specific tweaks
|
||||
|
||||
# Copy to clipboard on select. With this enabled, simply selecting text with
|
||||
# the mouse will cause the text to be copied to clipboard. Useful on platforms
|
||||
# such as macOS/Wayland that do not have the concept of primary selections. Note
|
||||
# that this is a security risk, as all programs, including websites open in your
|
||||
# browser can read the contents of the clipboard.
|
||||
copy_on_select no
|
||||
|
||||
# Hide the kitty window's title bar on macOS.
|
||||
macos_hide_titlebar no
|
||||
|
||||
|
||||
118
kitty/layout.py
118
kitty/layout.py
@@ -6,13 +6,13 @@ from collections import namedtuple
|
||||
from itertools import islice
|
||||
|
||||
from .constants import WindowGeometry
|
||||
from .fast_data_types import pt_to_px, viewport_for_window
|
||||
from .fast_data_types import pt_to_px, viewport_for_window, Region
|
||||
|
||||
viewport_width = viewport_height = available_height = 400
|
||||
central = Region((0, 0, 199, 199, 200, 200))
|
||||
cell_width = cell_height = 20
|
||||
|
||||
|
||||
def layout_dimension(length, cell_length, number_of_windows=1, border_length=0, margin_length=0, padding_length=0, left_align=False):
|
||||
def layout_dimension(start_at, length, cell_length, number_of_windows=1, border_length=0, margin_length=0, padding_length=0, left_align=False):
|
||||
number_of_cells = length // cell_length
|
||||
border_length += padding_length
|
||||
space_needed_for_border = number_of_windows * 2 * border_length
|
||||
@@ -24,7 +24,9 @@ def layout_dimension(length, cell_length, number_of_windows=1, border_length=0,
|
||||
extra = length - number_of_cells * cell_length
|
||||
cells_per_window = number_of_cells // number_of_windows
|
||||
extra -= space_needed
|
||||
pos = 0 if left_align else (extra // 2)
|
||||
pos = start_at
|
||||
if not left_align:
|
||||
pos += extra // 2
|
||||
pos += border_length + margin_length
|
||||
inner_length = cells_per_window * cell_length
|
||||
window_length = 2 * (border_length + margin_length) + inner_length
|
||||
@@ -73,30 +75,30 @@ class Layout:
|
||||
|
||||
def xlayout(self, num):
|
||||
return layout_dimension(
|
||||
viewport_width, cell_width, num, self.border_width,
|
||||
central.left, central.width, cell_width, num, self.border_width,
|
||||
margin_length=self.margin_width, padding_length=self.padding_width)
|
||||
|
||||
def ylayout(self, num, left_align=True):
|
||||
return layout_dimension(
|
||||
available_height, cell_height, num, self.border_width, left_align=left_align,
|
||||
central.top, central.height, cell_height, num, self.border_width, left_align=left_align,
|
||||
margin_length=self.margin_width, padding_length=self.padding_width)
|
||||
|
||||
def simple_blank_rects(self, first_window, last_window):
|
||||
br, vh = self.blank_rects, available_height
|
||||
left_blank_rect(first_window, br, vh), top_blank_rect(first_window, br, vh), right_blank_rect(last_window, br, vh)
|
||||
br = self.blank_rects
|
||||
left_blank_rect(first_window, br), top_blank_rect(first_window, br), right_blank_rect(last_window, br)
|
||||
|
||||
def between_blank_rect(self, left_window, right_window):
|
||||
self.blank_rects.append(Rect(left_window.geometry.right, 0, right_window.geometry.left, available_height))
|
||||
self.blank_rects.append(Rect(left_window.geometry.right, central.top, right_window.geometry.left, central.bottom + 1))
|
||||
|
||||
def bottom_blank_rect(self, window):
|
||||
self.blank_rects.append(Rect(window.geometry.left, window.geometry.bottom, window.geometry.right, available_height))
|
||||
self.blank_rects.append(Rect(window.geometry.left, window.geometry.bottom, window.geometry.right, central.bottom + 1))
|
||||
|
||||
def set_active_window(self, windows, active_window_idx):
|
||||
pass
|
||||
|
||||
def __call__(self, windows, active_window_idx):
|
||||
global viewport_width, viewport_height, cell_width, cell_height, available_height
|
||||
viewport_width, viewport_height, available_height, cell_width, cell_height = viewport_for_window(self.os_window_id)
|
||||
global central, cell_width, cell_height
|
||||
central, tab_bar, vw, vh, cell_width, cell_height = viewport_for_window(self.os_window_id)
|
||||
self.do_layout(windows, active_window_idx)
|
||||
|
||||
def do_layout(self, windows, active_window_idx):
|
||||
@@ -108,35 +110,38 @@ def window_geometry(xstart, xnum, ystart, ynum):
|
||||
|
||||
|
||||
def layout_single_window(margin_length, padding_length):
|
||||
xstart, xnum = next(layout_dimension(viewport_width, cell_width, margin_length=margin_length, padding_length=padding_length))
|
||||
ystart, ynum = next(layout_dimension(available_height, cell_height, margin_length=margin_length, padding_length=padding_length))
|
||||
xstart, xnum = next(layout_dimension(central.left, central.width, cell_width, margin_length=margin_length, padding_length=padding_length))
|
||||
ystart, ynum = next(layout_dimension(central.top, central.height, cell_height, margin_length=margin_length, padding_length=padding_length))
|
||||
return window_geometry(xstart, xnum, ystart, ynum)
|
||||
|
||||
|
||||
def left_blank_rect(w, rects, vh):
|
||||
if w.geometry.left > 0:
|
||||
rects.append(Rect(0, 0, w.geometry.left, vh))
|
||||
def left_blank_rect(w, rects):
|
||||
lt = w.geometry.left
|
||||
if lt > central.left:
|
||||
rects.append(Rect(central.left, central.top, lt, central.bottom + 1))
|
||||
|
||||
|
||||
def right_blank_rect(w, rects, vh):
|
||||
if w.geometry.right < viewport_width:
|
||||
rects.append(Rect(w.geometry.right, 0, viewport_width, vh))
|
||||
def right_blank_rect(w, rects):
|
||||
r = w.geometry.right
|
||||
if r < central.right:
|
||||
rects.append(Rect(r, central.top, central.right + 1, central.bottom + 1))
|
||||
|
||||
|
||||
def top_blank_rect(w, rects, vh):
|
||||
if w.geometry.top > 0:
|
||||
rects.append(Rect(0, 0, viewport_width, w.geometry.top))
|
||||
def top_blank_rect(w, rects):
|
||||
t = w.geometry.top
|
||||
if t > central.top:
|
||||
rects.append(Rect(central.left, central.top, central.right + 1, t))
|
||||
|
||||
|
||||
def bottom_blank_rect(w, rects, vh):
|
||||
if w.geometry.bottom < available_height:
|
||||
rects.append(Rect(0, w.geometry.bottom, viewport_width, vh))
|
||||
def bottom_blank_rect(w, rects):
|
||||
b = w.geometry.bottom
|
||||
if b < central.bottom:
|
||||
rects.append(Rect(central.left, b, central.right + 1, central.bottom + 1))
|
||||
|
||||
|
||||
def blank_rects_for_window(w):
|
||||
ans = []
|
||||
vh = available_height
|
||||
left_blank_rect(w, ans, vh), top_blank_rect(w, ans, vh), right_blank_rect(w, ans, vh), bottom_blank_rect(w, ans, vh)
|
||||
left_blank_rect(w, ans), top_blank_rect(w, ans), right_blank_rect(w, ans), bottom_blank_rect(w, ans)
|
||||
return ans
|
||||
|
||||
|
||||
@@ -237,4 +242,61 @@ class Grid(Tall):
|
||||
self.between_blank_rect(win_col_map[i][0], win_col_map[i + 1][0])
|
||||
|
||||
|
||||
class Vertical(Layout):
|
||||
|
||||
name = 'vertical'
|
||||
|
||||
def do_layout(self, windows, active_window_idx):
|
||||
self.blank_rects = []
|
||||
window_count = len(windows)
|
||||
if window_count == 1:
|
||||
wg = layout_single_window(self.margin_width, self.padding_width)
|
||||
windows[0].set_geometry(0, wg)
|
||||
self.blank_rects = blank_rects_for_window(windows[0])
|
||||
return
|
||||
|
||||
xlayout = self.xlayout(1)
|
||||
xstart, xnum = next(xlayout)
|
||||
ylayout = self.ylayout(window_count)
|
||||
|
||||
for i in range(window_count):
|
||||
ystart, ynum = next(ylayout)
|
||||
windows[i].set_geometry(i, window_geometry(xstart, xnum, ystart, ynum))
|
||||
|
||||
# left, top and right blank rects
|
||||
self.simple_blank_rects(windows[0], windows[-1])
|
||||
# bottom blank rect
|
||||
self.bottom_blank_rect(windows[-1])
|
||||
|
||||
|
||||
class Horizontal(Layout):
|
||||
|
||||
name = 'horizontal'
|
||||
|
||||
def do_layout(self, windows, active_window_idx):
|
||||
self.blank_rects = []
|
||||
window_count = len(windows)
|
||||
if window_count == 1:
|
||||
wg = layout_single_window(self.margin_width, self.padding_width)
|
||||
windows[0].set_geometry(0, wg)
|
||||
self.blank_rects = blank_rects_for_window(windows[0])
|
||||
return
|
||||
|
||||
xlayout = self.xlayout(window_count)
|
||||
ylayout = self.ylayout(1)
|
||||
ystart, ynum = next(ylayout)
|
||||
|
||||
for i in range(window_count):
|
||||
xstart, xnum = next(xlayout)
|
||||
windows[i].set_geometry(i, window_geometry(xstart, xnum, ystart, ynum))
|
||||
if i > 0:
|
||||
# between blank rect
|
||||
self.between_blank_rect(windows[i - 1], windows[i])
|
||||
|
||||
# left, top and right blank rects
|
||||
self.simple_blank_rects(windows[0], windows[-1])
|
||||
# bottom blank rect
|
||||
self.blank_rects.append(Rect(windows[0].geometry.left, windows[0].geometry.bottom, windows[-1].geometry.right, central.bottom + 1))
|
||||
|
||||
|
||||
all_layouts = {o.name: o for o in globals().values() if isinstance(o, type) and issubclass(o, Layout) and o is not Layout}
|
||||
|
||||
@@ -383,7 +383,15 @@ 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};
|
||||
for(index_type i = 0; i < self->ynum; i++) {
|
||||
// remove trailing empty lines
|
||||
index_type ylimit = self->ynum - 1;
|
||||
do {
|
||||
init_line(self, (&l), self->line_map[ylimit]);
|
||||
if (line_as_ansi(&l, t, 5120) != 0) 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);
|
||||
|
||||
63
kitty/line.c
63
kitty/line.c
@@ -35,17 +35,13 @@ line_length(Line *self) {
|
||||
}
|
||||
|
||||
PyObject*
|
||||
line_text_at(char_type ch, combining_type cc) {
|
||||
cell_text(Cell *cell) {
|
||||
PyObject *ans;
|
||||
if (LIKELY(cc == 0)) {
|
||||
ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, &ch, 1);
|
||||
} else {
|
||||
Py_UCS4 buf[3];
|
||||
buf[0] = ch; buf[1] = cc & CC_MASK; buf[2] = cc >> 16;
|
||||
Py_UCS4 normalized = normalize(ch, buf[1], buf[2]);
|
||||
if (normalized) ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, &normalized, 1);
|
||||
else ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, buf, buf[2] ? 3 : 2);
|
||||
}
|
||||
unsigned num = 1;
|
||||
static Py_UCS4 buf[arraysz(cell->cc_idx) + 1];
|
||||
buf[0] = cell->ch;
|
||||
for (unsigned i = 0; i < arraysz(cell->cc_idx) && cell->cc_idx[i]; i++) buf[num++] = codepoint_for_mark(cell->cc_idx[i]);
|
||||
ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, buf, num);
|
||||
return ans;
|
||||
}
|
||||
|
||||
@@ -162,7 +158,7 @@ static PyObject*
|
||||
text_at(Line* self, Py_ssize_t xval) {
|
||||
#define text_at_doc "[x] -> Return the text in the specified cell"
|
||||
if ((unsigned)xval >= self->xnum) { PyErr_SetString(PyExc_IndexError, "Column number out of bounds"); return NULL; }
|
||||
return line_text_at(self->cells[xval].ch, self->cells[xval].cc);
|
||||
return cell_text(self->cells + xval);
|
||||
}
|
||||
|
||||
size_t
|
||||
@@ -170,13 +166,7 @@ cell_as_unicode(Cell *cell, bool include_cc, Py_UCS4 *buf, char_type zero_char)
|
||||
size_t n = 1;
|
||||
buf[0] = cell->ch ? cell->ch : zero_char;
|
||||
if (include_cc) {
|
||||
char_type cc = cell->cc;
|
||||
Py_UCS4 cc1 = cc & CC_MASK, cc2;
|
||||
if (cc1) {
|
||||
buf[1] = cc1; n++;
|
||||
cc2 = cc >> 16;
|
||||
if (cc2) { buf[2] = cc2; n++; }
|
||||
}
|
||||
for (unsigned i = 0; i < arraysz(cell->cc_idx) && cell->cc_idx[i]; i++) buf[n++] = codepoint_for_mark(cell->cc_idx[i]);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
@@ -185,13 +175,7 @@ size_t
|
||||
cell_as_utf8(Cell *cell, bool include_cc, char *buf, char_type zero_char) {
|
||||
size_t n = encode_utf8(cell->ch ? cell->ch : zero_char, buf);
|
||||
if (include_cc) {
|
||||
char_type cc = cell->cc;
|
||||
Py_UCS4 cc1 = cc & CC_MASK, cc2;
|
||||
if (cc1) {
|
||||
n += encode_utf8(cc1, buf + n);
|
||||
cc2 = cc >> 16;
|
||||
if (cc2) { n += encode_utf8(cc2, buf + n); }
|
||||
}
|
||||
for (unsigned i = 0; i < arraysz(cell->cc_idx) && cell->cc_idx[i]; i++) n += encode_utf8(codepoint_for_mark(cell->cc_idx[i]), buf + n);
|
||||
}
|
||||
buf[n] = 0;
|
||||
return n;
|
||||
@@ -204,7 +188,7 @@ unicode_in_range(Line *self, index_type start, index_type limit, bool include_cc
|
||||
static Py_UCS4 buf[4096];
|
||||
if (leading_char) buf[n++] = leading_char;
|
||||
char_type previous_width = 0;
|
||||
for(index_type i = start; i < limit && n < sizeof(buf)/sizeof(buf[0]) - 4; i++) {
|
||||
for(index_type i = start; i < limit && n < arraysz(buf) - 2 - arraysz(self->cells->cc_idx); i++) {
|
||||
char_type ch = self->cells[i].ch;
|
||||
if (ch == 0) {
|
||||
if (previous_width == 2) { previous_width = 0; continue; };
|
||||
@@ -244,6 +228,7 @@ line_as_ansi(Line *self, Py_UCS4 *buf, index_type buflen) {
|
||||
#define WRITE_CH(val) if (i > buflen - 1) return i; buf[i++] = val;
|
||||
|
||||
index_type limit = xlimit_for_line(self), i=0;
|
||||
if (limit == 0) return 0;
|
||||
char_type previous_width = 0;
|
||||
|
||||
WRITE_SGR("0");
|
||||
@@ -264,12 +249,8 @@ line_as_ansi(Line *self, Py_UCS4 *buf, index_type buflen) {
|
||||
t = prev_cursor; prev_cursor = cursor; cursor = t;
|
||||
if (*sgr) WRITE_SGR(sgr);
|
||||
WRITE_CH(ch);
|
||||
char_type cc = self->cells[pos].cc;
|
||||
Py_UCS4 cc1 = cc & CC_MASK;
|
||||
if (cc1) {
|
||||
WRITE_CH(cc1);
|
||||
cc1 = cc >> 16;
|
||||
if (cc1) { WRITE_CH(cc1); }
|
||||
for(unsigned c = 0; c < arraysz(self->cells[pos].cc_idx) && self->cells[pos].cc_idx[c]; c++) {
|
||||
WRITE_CH(codepoint_for_mark(self->cells[pos].cc_idx[c]));
|
||||
}
|
||||
previous_width = attrs & WIDTH_MASK;
|
||||
}
|
||||
@@ -318,10 +299,12 @@ width(Line *self, PyObject *val) {
|
||||
|
||||
void
|
||||
line_add_combining_char(Line *self, uint32_t ch, unsigned int x) {
|
||||
if (!self->cells[x].ch) return; // dont allow adding combining chars to a null cell
|
||||
combining_type c = self->cells[x].cc;
|
||||
if (c & CC_MASK) self->cells[x].cc = (c & CC_MASK) | ( (ch & CC_MASK) << CC_SHIFT );
|
||||
else self->cells[x].cc = ch & CC_MASK;
|
||||
Cell *cell = self->cells + x;
|
||||
if (!cell->ch) return; // dont allow adding combining chars to a null cell
|
||||
for (unsigned i = 0; i < arraysz(cell->cc_idx); i++) {
|
||||
if (!cell->cc_idx[i]) { cell->cc_idx[i] = mark_for_codepoint(ch); return; }
|
||||
}
|
||||
cell->cc_idx[arraysz(cell->cc_idx) - 1] = mark_for_codepoint(ch);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
@@ -371,7 +354,7 @@ set_text(Line* self, PyObject *args) {
|
||||
self->cells[i].fg = fg;
|
||||
self->cells[i].bg = bg;
|
||||
self->cells[i].decoration_fg = dfg;
|
||||
self->cells[i].cc = 0;
|
||||
memset(self->cells[i].cc_idx, 0, sizeof(self->cells[i].cc_idx));
|
||||
}
|
||||
|
||||
Py_RETURN_NONE;
|
||||
@@ -403,7 +386,7 @@ 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->cells[i].ch = ch; self->cells[i].cc = 0; \
|
||||
self->cells[i].ch = ch; memset(self->cells[i].cc_idx, 0, sizeof(self->cells[i].cc_idx)); \
|
||||
self->cells[i].attrs = (self->cells[i].attrs & ATTRS_MASK_WITHOUT_WIDTH) | width; \
|
||||
}
|
||||
if (CHAR_IS_BLANK(ch)) {
|
||||
@@ -433,7 +416,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->cells[i].ch = BLANK_CHAR;
|
||||
self->cells[i].cc = 0;
|
||||
memset(self->cells[i].cc_idx, 0, sizeof(self->cells[i].cc_idx));
|
||||
self->cells[i].attrs = attrs;
|
||||
clear_sprite_position(self->cells[i]);
|
||||
} else {
|
||||
@@ -508,7 +491,7 @@ line_set_char(Line *self, unsigned int at, uint32_t ch, unsigned int width, Curs
|
||||
self->cells[at].decoration_fg = cursor->decoration_fg & COL_MASK;
|
||||
}
|
||||
self->cells[at].ch = ch;
|
||||
self->cells[at].cc = 0;
|
||||
memset(self->cells[at].cc_idx, 0, sizeof(self->cells[at].cc_idx));
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
|
||||
@@ -44,7 +44,6 @@ xlimit_for_line(Line *line) {
|
||||
return xlimit;
|
||||
}
|
||||
|
||||
PyObject* line_text_at(char_type, combining_type);
|
||||
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);
|
||||
void line_set_char(Line *, unsigned int , uint32_t , unsigned int , Cursor *, bool);
|
||||
@@ -79,3 +78,4 @@ void historybuf_init_line(HistoryBuf *self, index_type num, Line *l);
|
||||
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);
|
||||
|
||||
@@ -109,14 +109,20 @@ def main():
|
||||
locale.setlocale(locale.LC_ALL, '')
|
||||
except Exception:
|
||||
print('Failed to set locale with no LANG, ignoring', file=sys.stderr)
|
||||
|
||||
# Ensure kitty is in PATH
|
||||
rpath = getattr(sys, 'bundle_exe_dir', None)
|
||||
if rpath:
|
||||
# Ensure kitty bin directory is in PATH
|
||||
items = frozenset(os.environ['PATH'].split(os.pathsep))
|
||||
if rpath not in items:
|
||||
os.environ['PATH'] += os.pathsep + rpath
|
||||
if os.environ.pop('KITTY_LAUNCHED_BY_LAUNCH_SERVICES',
|
||||
None) == '1' and getattr(sys, 'frozen', True):
|
||||
items = frozenset(os.environ['PATH'].split(os.pathsep))
|
||||
if not rpath:
|
||||
for candidate in items:
|
||||
if os.access(os.path.join(candidate, 'kitty'), os.X_OK):
|
||||
break
|
||||
else:
|
||||
rpath = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'launcher')
|
||||
if rpath and rpath not in items:
|
||||
os.environ['PATH'] += os.pathsep + rpath
|
||||
|
||||
if os.environ.pop('KITTY_LAUNCHED_BY_LAUNCH_SERVICES', None) == '1':
|
||||
os.chdir(os.path.expanduser('~'))
|
||||
if not os.path.isdir(os.getcwd()):
|
||||
os.chdir(os.path.expanduser('~'))
|
||||
|
||||
@@ -42,24 +42,21 @@ button_map(int button) {
|
||||
|
||||
static char mouse_event_buf[64];
|
||||
|
||||
int
|
||||
encode_mouse_event(Window *w, int button, MouseAction action, int mods) {
|
||||
unsigned int x = w->mouse_cell_x + 1, y = w->mouse_cell_y + 1; // 1 based indexing
|
||||
static inline int
|
||||
encode_mouse_event_impl(unsigned int x, unsigned int y, int mouse_tracking_protocol, int button, MouseAction action, int mods) {
|
||||
unsigned int cb = 0;
|
||||
Screen *screen = w->render_data.screen;
|
||||
|
||||
if (action == MOVE) {
|
||||
if (screen->modes.mouse_tracking_protocol != SGR_PROTOCOL) cb = 3;
|
||||
cb = 3;
|
||||
} else {
|
||||
cb = button_map(button);
|
||||
if (cb == UINT_MAX) return 0;
|
||||
}
|
||||
if (action == DRAG || action == MOVE) cb |= MOTION_INDICATOR;
|
||||
else if (action == RELEASE && screen->modes.mouse_tracking_protocol != SGR_PROTOCOL) cb = 3;
|
||||
else if (action == RELEASE && mouse_tracking_protocol != SGR_PROTOCOL) cb = 3;
|
||||
if (mods & GLFW_MOD_SHIFT) cb |= SHIFT_INDICATOR;
|
||||
if (mods & GLFW_MOD_ALT) cb |= ALT_INDICATOR;
|
||||
if (mods & GLFW_MOD_CONTROL) cb |= CONTROL_INDICATOR;
|
||||
switch(screen->modes.mouse_tracking_protocol) {
|
||||
switch(mouse_tracking_protocol) {
|
||||
case SGR_PROTOCOL:
|
||||
return snprintf(mouse_event_buf, sizeof(mouse_event_buf), "<%d;%d;%d%s", cb, x, y, action == RELEASE ? "m" : "M");
|
||||
break;
|
||||
@@ -84,6 +81,14 @@ encode_mouse_event(Window *w, int button, MouseAction action, int mods) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
encode_mouse_event(Window *w, int button, MouseAction action, int mods) {
|
||||
unsigned int x = w->mouse_cell_x + 1, y = w->mouse_cell_y + 1; // 1 based indexing
|
||||
Screen *screen = w->render_data.screen;
|
||||
return encode_mouse_event_impl(x, y, screen->modes.mouse_tracking_protocol, button, action, mods);
|
||||
|
||||
}
|
||||
|
||||
static inline bool
|
||||
contains_mouse(Window *w) {
|
||||
WindowGeometry *g = &w->geometry;
|
||||
@@ -315,9 +320,19 @@ handle_tab_bar_mouse(int button, int UNUSED modifiers) {
|
||||
call_boss(activate_tab_at, "Kd", global_state.callback_os_window->id, global_state.callback_os_window->mouse_x);
|
||||
}
|
||||
|
||||
static inline bool
|
||||
mouse_in_region(Region *r) {
|
||||
if (r->left == r->right) return false;
|
||||
if (global_state.callback_os_window->mouse_y < r->top || global_state.callback_os_window->mouse_y > r->bottom) return false;
|
||||
if (global_state.callback_os_window->mouse_x < r->left || global_state.callback_os_window->mouse_x > r->right) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline Window*
|
||||
window_for_event(unsigned int *window_idx, bool *in_tab_bar) {
|
||||
*in_tab_bar = global_state.callback_os_window->num_tabs > 1 && global_state.callback_os_window->mouse_y >= global_state.callback_os_window->viewport_height - global_state.cell_height;
|
||||
Region central, tab_bar;
|
||||
os_window_regions(global_state.callback_os_window, ¢ral, &tab_bar);
|
||||
*in_tab_bar = mouse_in_region(&tab_bar);
|
||||
if (!*in_tab_bar && global_state.callback_os_window->num_tabs > 0) {
|
||||
Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab;
|
||||
for (unsigned int i = 0; i < t->num_windows; i++) {
|
||||
@@ -375,8 +390,28 @@ scroll_event(double UNUSED xoffset, double yoffset) {
|
||||
int sz = encode_mouse_event(w, upwards ? GLFW_MOUSE_BUTTON_4 : GLFW_MOUSE_BUTTON_5, PRESS, 0);
|
||||
if (sz > 0) { mouse_event_buf[sz] = 0; write_escape_code_to_child(screen, CSI, mouse_event_buf); }
|
||||
} else {
|
||||
fake_scroll(upwards);
|
||||
fake_scroll(abs(s), upwards);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
test_encode_mouse(PyObject *self UNUSED, PyObject *args) {
|
||||
unsigned int x, y;
|
||||
int mouse_tracking_protocol, button, action, mods;
|
||||
if (!PyArg_ParseTuple(args, "IIiiii", &x, &y, &mouse_tracking_protocol, &button, &action, &mods)) return NULL;
|
||||
int sz = encode_mouse_event_impl(x, y, mouse_tracking_protocol, button, action, mods);
|
||||
return PyUnicode_FromStringAndSize(mouse_event_buf, sz);
|
||||
}
|
||||
|
||||
static PyMethodDef module_methods[] = {
|
||||
METHODB(test_encode_mouse, METH_VARARGS),
|
||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
bool
|
||||
init_mouse(PyObject *module) {
|
||||
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -727,6 +727,17 @@ dispatch_csi(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
|
||||
// }}}
|
||||
|
||||
// DCS mode {{{
|
||||
|
||||
static inline bool
|
||||
startswith(const uint32_t *string, size_t sz, const char *prefix) {
|
||||
size_t l = strlen(prefix);
|
||||
if (sz < l) return false;
|
||||
for (size_t i = 0; i < l; i++) {
|
||||
if (string[i] != (unsigned char)prefix[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline void
|
||||
dispatch_dcs(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
|
||||
if (screen->parser_buf_pos < 2) return;
|
||||
@@ -739,11 +750,23 @@ dispatch_dcs(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
|
||||
REPORT_OSC2(screen_request_capabilities, (char)screen->parser_buf[0], string);
|
||||
screen_request_capabilities(screen, (char)screen->parser_buf[0], string);
|
||||
Py_DECREF(string);
|
||||
}
|
||||
} else PyErr_Clear();
|
||||
} else {
|
||||
REPORT_ERROR("Unrecognized DCS %c code: 0x%x", (char)screen->parser_buf[0], screen->parser_buf[1]);
|
||||
}
|
||||
break;
|
||||
case '@':
|
||||
if (startswith(screen->parser_buf + 1, screen->parser_buf_pos - 2, "kitty-cmd{")) {
|
||||
PyObject *cmd = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, screen->parser_buf + 10, screen->parser_buf_pos - 10);
|
||||
if (cmd != NULL) {
|
||||
REPORT_OSC2(screen_handle_cmd, (char)screen->parser_buf[0], cmd);
|
||||
screen_handle_cmd(screen, cmd);
|
||||
Py_DECREF(cmd);
|
||||
} else PyErr_Clear();
|
||||
} else {
|
||||
REPORT_ERROR("Unrecognized DCS @ code: 0x%x", screen->parser_buf[1]);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
REPORT_ERROR("Unrecognized DCS code: 0x%x", screen->parser_buf[0]);
|
||||
break;
|
||||
|
||||
488
kitty/remote_control.py
Normal file
488
kitty/remote_control.py
Normal file
@@ -0,0 +1,488 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import types
|
||||
from functools import partial
|
||||
|
||||
from .cli import emph, parse_args
|
||||
from .config import parse_send_text_bytes
|
||||
from .constants import appname, version
|
||||
from .tabs import SpecialWindow
|
||||
from .utils import non_blocking_read, read_with_timeout
|
||||
|
||||
|
||||
def cmd(short_desc, desc=None, options_spec=None, no_response=False):
|
||||
|
||||
def w(func):
|
||||
func.short_desc = short_desc
|
||||
func.desc = desc or short_desc
|
||||
func.name = func.__name__[4:].replace('_', '-')
|
||||
func.options_spec = options_spec
|
||||
func.is_cmd = True
|
||||
func.impl = lambda: globals()[func.__name__[4:]]
|
||||
func.no_response = no_response
|
||||
return func
|
||||
return w
|
||||
|
||||
|
||||
def parse_subcommand_cli(func, args):
|
||||
opts, items = parse_args(args[1:], (func.options_spec or '\n').format, '...', func.desc, '{} @ {}'.format(appname, func.name))
|
||||
return opts, items
|
||||
|
||||
|
||||
@cmd(
|
||||
'List all tabs/windows',
|
||||
'List all windows. The list is returned as JSON tree. The top-level is a list of'
|
||||
' operating system {appname} windows. Each OS window has an |_ id| and a list'
|
||||
' of |_ tabs|. Each tab has its own |_ id|, a |_ title| and a list of |_ windows|.'
|
||||
' Each window has an |_ id|, |_ title|, |_ current working directory|, |_ process id (PID)| and'
|
||||
' |_ command-line| of the process running in the window.\n\n'
|
||||
'You can use these criteria to select windows/tabs for the other commands.'.format(appname=appname)
|
||||
)
|
||||
def cmd_ls(global_opts, opts, args):
|
||||
pass
|
||||
|
||||
|
||||
def ls(boss, window):
|
||||
data = list(boss.list_os_windows())
|
||||
data = json.dumps(data, indent=2, sort_keys=True)
|
||||
return data
|
||||
|
||||
|
||||
MATCH_WINDOW_OPTION = '''\
|
||||
--match -m
|
||||
The window to match. Match specifications are of the form:
|
||||
|_ field:regexp|. Where field can be one of: id, title, pid, cwd, cmdline.
|
||||
You can use the |_ ls| command to get a list of windows. Note that for
|
||||
numeric fields such as id and pid the expression is interpreted as a number,
|
||||
not a regular expression.
|
||||
'''
|
||||
MATCH_TAB_OPTION = '''\
|
||||
--match -m
|
||||
The tab to match. Match specifications are of the form:
|
||||
|_ field:regexp|. Where field can be one of: id, title, pid, cwd, cmdline.
|
||||
You can use the |_ ls| command to get a list of tabs. Note that for
|
||||
numeric fields such as id and pid the expression is interpreted as a number,
|
||||
not a regular expression. When using title or id, first a matching tab is
|
||||
looked for and if not found a matching window is looked for, and the tab
|
||||
for that window is used.
|
||||
'''
|
||||
|
||||
|
||||
@cmd(
|
||||
'Send arbitrary text to specified windows',
|
||||
'Send arbitrary text to specified windows. The text follows Python'
|
||||
' escaping rules. So you can use escapes like |_ \\x1b| to send control codes'
|
||||
' and |_ \\u21fa| to send unicode characters. If you use the |_ --match| option'
|
||||
' the text will be sent to all matched windows. By default, text is sent to'
|
||||
' only the currently active window.',
|
||||
options_spec=MATCH_WINDOW_OPTION + '''\n
|
||||
--stdin
|
||||
type=bool-set
|
||||
Read the text to be sent from |_ stdin|. Note that in this case the text is sent as is,
|
||||
not interpreted for escapes. If stdin is a terminal, you can press Ctrl-D to end reading.
|
||||
|
||||
|
||||
--from-file
|
||||
Path to a file whose contents you wish to send. Note that in this case the file contents
|
||||
are sent as is, not interpreted for escapes.
|
||||
''',
|
||||
no_response=True
|
||||
)
|
||||
def cmd_send_text(global_opts, opts, args):
|
||||
limit = 1024
|
||||
ret = {'match': opts.match, 'is_binary': False}
|
||||
|
||||
def pipe(src=sys.stdin):
|
||||
ret['is_binary'] = True
|
||||
import select
|
||||
with non_blocking_read() as fd:
|
||||
keep_going = True
|
||||
while keep_going:
|
||||
rd = select.select([fd], [], [])
|
||||
if rd:
|
||||
data = sys.stdin.buffer.read()
|
||||
if not data:
|
||||
break
|
||||
data = data.decode('utf-8')
|
||||
if '\x04' in data:
|
||||
data = data[:data.index('\x04')]
|
||||
keep_going = False
|
||||
while data:
|
||||
ret['text'] = data[:limit]
|
||||
yield ret
|
||||
data = data[limit:]
|
||||
else:
|
||||
break
|
||||
|
||||
def chunks(text):
|
||||
ret['is_binary'] = False
|
||||
while text:
|
||||
ret['text'] = text[:limit]
|
||||
yield ret
|
||||
text = text[limit:]
|
||||
|
||||
def file_pipe(path):
|
||||
ret['is_binary'] = True
|
||||
with open(path, encoding='utf-8') as f:
|
||||
while True:
|
||||
data = f.read(limit)
|
||||
if not data:
|
||||
break
|
||||
ret['text'] = data
|
||||
yield ret
|
||||
|
||||
sources = []
|
||||
if opts.stdin:
|
||||
sources.append(pipe())
|
||||
|
||||
if opts.from_file:
|
||||
sources.append(file_pipe(opts.from_file))
|
||||
|
||||
text = ' '.join(args)
|
||||
sources.append(chunks(text))
|
||||
|
||||
def chain():
|
||||
for src in sources:
|
||||
yield from src
|
||||
return chain()
|
||||
|
||||
|
||||
def send_text(boss, window, payload):
|
||||
windows = [boss.active_window]
|
||||
match = payload['match']
|
||||
if match:
|
||||
windows = tuple(boss.match_windows(match))
|
||||
data = payload['text'].encode('utf-8') if payload['is_binary'] else parse_send_text_bytes(payload['text'])
|
||||
for window in windows:
|
||||
if window is not None:
|
||||
window.write_to_child(data)
|
||||
|
||||
|
||||
@cmd(
|
||||
'Set the window title',
|
||||
'Set the title for the specified window(s). If you use the |_ --match| option'
|
||||
' the title will be set for all matched windows. By default, only the window'
|
||||
' in which the command is run is affected. If you do not specify a title, the'
|
||||
' last title set by the child process running in the window will be used.',
|
||||
options_spec=MATCH_WINDOW_OPTION
|
||||
)
|
||||
def cmd_set_window_title(global_opts, opts, args):
|
||||
return {'title': ' '.join(args), 'match': opts.match}
|
||||
|
||||
|
||||
def set_window_title(boss, window, payload):
|
||||
windows = [window or boss.active_window]
|
||||
match = payload['match']
|
||||
if match:
|
||||
windows = tuple(boss.match_windows(match))
|
||||
if not windows:
|
||||
raise ValueError('No matching windows for expression: {}'.format(match))
|
||||
for window in windows:
|
||||
if window:
|
||||
window.set_title(payload['title'])
|
||||
|
||||
|
||||
@cmd(
|
||||
'Set the tab title',
|
||||
'Set the title for the specified tab(s). If you use the |_ --match| option'
|
||||
' the title will be set for all matched tabs. By default, only the tab'
|
||||
' in which the command is run is affected. If you do not specify a title, the'
|
||||
' title of the currently active window in the tab is used.',
|
||||
options_spec=MATCH_TAB_OPTION
|
||||
)
|
||||
def cmd_set_tab_title(global_opts, opts, args):
|
||||
return {'title': ' '.join(args), 'match': opts.match}
|
||||
|
||||
|
||||
def set_tab_title(boss, window, payload):
|
||||
match = payload['match']
|
||||
if match:
|
||||
tabs = tuple(boss.match_tabs(match))
|
||||
if not tabs:
|
||||
raise ValueError('No matching windows for expression: {}'.format(match))
|
||||
else:
|
||||
tabs = [boss.tab_for_window(window) if window else boss.active_tab]
|
||||
for tab in tabs:
|
||||
if tab:
|
||||
tab.set_title(payload['title'])
|
||||
|
||||
|
||||
@cmd(
|
||||
'Close the specified window(s)',
|
||||
options_spec=MATCH_WINDOW_OPTION + '''\n
|
||||
--self
|
||||
type=bool-set
|
||||
If specified close the window this command is run in, rather than the active window.
|
||||
'''
|
||||
)
|
||||
def cmd_close_window(global_opts, opts, args):
|
||||
return {'match': opts.match, 'self': opts.self}
|
||||
|
||||
|
||||
def close_window(boss, window, payload):
|
||||
match = payload['match']
|
||||
if match:
|
||||
windows = tuple(boss.match_windows(match))
|
||||
if not windows:
|
||||
raise ValueError('No matching windows for expression: {}'.format(match))
|
||||
else:
|
||||
windows = [window if window and payload['self'] else boss.active_window]
|
||||
for window in windows:
|
||||
if window:
|
||||
boss.close_window(window)
|
||||
|
||||
|
||||
@cmd(
|
||||
'Close the specified tab(s)',
|
||||
options_spec=MATCH_TAB_OPTION + '''\n
|
||||
--self
|
||||
type=bool-set
|
||||
If specified close the tab this command is run in, rather than the active tab.
|
||||
'''
|
||||
)
|
||||
def cmd_close_tab(global_opts, opts, args):
|
||||
return {'match': opts.match, 'self': opts.self}
|
||||
|
||||
|
||||
def close_tab(boss, window, payload):
|
||||
match = payload['match']
|
||||
if match:
|
||||
tabs = tuple(boss.match_tabs(match))
|
||||
if not tabs:
|
||||
raise ValueError('No matching windows for expression: {}'.format(match))
|
||||
else:
|
||||
tabs = [boss.tab_for_window(window) if window and payload['self'] else boss.active_tab]
|
||||
for tab in tabs:
|
||||
if window:
|
||||
if tab:
|
||||
boss.close_tab(tab)
|
||||
|
||||
|
||||
@cmd(
|
||||
'Open new window',
|
||||
'Open a new window in the specified tab. If you use the |_ --match| option'
|
||||
' the first matching tab is used. Otherwise the currently active tab is used.'
|
||||
' Prints out the id of the newly opened window. Any command line arguments'
|
||||
' are assumed to be the command line used to run in the new window, if none'
|
||||
' are provided, the default shell is run. For example:\n'
|
||||
'|_ kitty @ new-window --title Email mutt|',
|
||||
options_spec=MATCH_TAB_OPTION + '''\n
|
||||
--title
|
||||
The title for the new window. By default it will use the title set by the
|
||||
program running in it.
|
||||
|
||||
|
||||
--cwd
|
||||
The initial working directory for the new window.
|
||||
|
||||
|
||||
--keep-focus
|
||||
type=bool-set
|
||||
Keep the current window focused instead of switching to the newly opened window
|
||||
|
||||
|
||||
--new-tab
|
||||
type=bool-set
|
||||
Open a new tab
|
||||
|
||||
|
||||
--tab-title
|
||||
When using --new-tab set the title of the tab.
|
||||
'''
|
||||
)
|
||||
def cmd_new_window(global_opts, opts, args):
|
||||
return {'match': opts.match, 'title': opts.title, 'cwd': opts.cwd,
|
||||
'new_tab': opts.new_tab, 'tab_title': opts.tab_title,
|
||||
'keep_focus': opts.keep_focus, 'args': args or []}
|
||||
|
||||
|
||||
def new_window(boss, window, payload):
|
||||
w = SpecialWindow(cmd=payload['args'] or None, override_title=payload['title'], cwd=payload['cwd'])
|
||||
old_window = boss.active_window
|
||||
if payload['new_tab']:
|
||||
boss._new_tab(w)
|
||||
tab = boss.active_tab
|
||||
if payload['tab_title']:
|
||||
tab.set_title(payload['tab_title'])
|
||||
wid = boss.active_window.id
|
||||
if payload['keep_focus'] and old_window:
|
||||
boss.set_active_window(old_window)
|
||||
return str(wid)
|
||||
|
||||
match = payload['match']
|
||||
if match:
|
||||
tabs = tuple(boss.match_tabs(match))
|
||||
if not tabs:
|
||||
raise ValueError('No matching windows for expression: {}'.format(match))
|
||||
else:
|
||||
tabs = [boss.active_tab]
|
||||
tab = tabs[0]
|
||||
w = tab.new_special_window(w)
|
||||
if payload['keep_focus'] and old_window:
|
||||
boss.set_active_window(old_window)
|
||||
return str(w.id)
|
||||
|
||||
|
||||
@cmd(
|
||||
'Focus the specified window',
|
||||
options_spec=MATCH_WINDOW_OPTION
|
||||
)
|
||||
def cmd_focus_window(global_opts, opts, args):
|
||||
return {'match': opts.match}
|
||||
|
||||
|
||||
def focus_window(boss, window, payload):
|
||||
windows = [window or boss.active_window]
|
||||
match = payload['match']
|
||||
if match:
|
||||
windows = tuple(boss.match_windows(match))
|
||||
if not windows:
|
||||
raise ValueError('No matching windows for expression: {}'.format(match))
|
||||
for window in windows:
|
||||
if window:
|
||||
boss.set_active_window(window)
|
||||
break
|
||||
|
||||
|
||||
@cmd(
|
||||
'Get text from the specified window',
|
||||
options_spec=MATCH_WINDOW_OPTION + '''\n
|
||||
--extent
|
||||
default=screen
|
||||
choices=screen, all, selection
|
||||
What text to get. The default of screen means all text currently on the screen. all means
|
||||
all the screen+scrollback and selection means currently selected text.
|
||||
|
||||
|
||||
--ansi
|
||||
type=bool-set
|
||||
By default, only plain text is returned. If you specify this flag, the text will
|
||||
include the formatting escape codes for colors/bold/italic/etc. Note that when
|
||||
getting the current selection, the result is always plain text.
|
||||
|
||||
|
||||
--self
|
||||
type=bool-set
|
||||
If specified get text from the window this command is run in, rather than the active window.
|
||||
'''
|
||||
)
|
||||
def cmd_get_text(global_opts, opts, args):
|
||||
return {'match': opts.match, 'extent': opts.extent, 'ansi': opts.ansi, 'self': opts.self}
|
||||
|
||||
|
||||
def get_text(boss, window, payload):
|
||||
match = payload['match']
|
||||
if match:
|
||||
windows = tuple(boss.match_windows(match))
|
||||
if not windows:
|
||||
raise ValueError('No matching windows for expression: {}'.format(match))
|
||||
else:
|
||||
windows = [window if window and payload['self'] else boss.active_window]
|
||||
window = windows[0]
|
||||
if payload['extent'] == 'selection':
|
||||
ans = window.text_for_selection()
|
||||
else:
|
||||
f = window.buffer_as_ansi if payload['ansi'] else window.buffer_as_text
|
||||
ans = f(add_history=payload['extent'] == 'all')
|
||||
return ans
|
||||
|
||||
|
||||
cmap = {v.name: v for v in globals().values() if hasattr(v, 'is_cmd')}
|
||||
|
||||
|
||||
def handle_cmd(boss, window, cmd):
|
||||
cmd = json.loads(cmd)
|
||||
v = cmd['version']
|
||||
if tuple(v)[:2] > version[:2]:
|
||||
return {'ok': False, 'error': 'The kitty client you are using to send remote commands is newer than this kitty instance. This is not supported.'}
|
||||
c = cmap[cmd['cmd']]
|
||||
func = partial(c.impl(), boss, window)
|
||||
payload = cmd.get('payload')
|
||||
ans = func() if payload is None else func(payload)
|
||||
response = {'ok': True}
|
||||
if ans is not None:
|
||||
response['data'] = ans
|
||||
if not c.no_response:
|
||||
return response
|
||||
|
||||
|
||||
global_options_spec = partial('''\
|
||||
|
||||
'''.format, appname=appname)
|
||||
|
||||
|
||||
def read_from_stdin(send, no_response):
|
||||
send = ('@kitty-cmd' + json.dumps(send)).encode('ascii')
|
||||
out = sys.stdout if sys.stdout.isatty() else sys.stderr
|
||||
if not out.isatty():
|
||||
raise SystemExit('Neither stdout nor stderr is a terminal')
|
||||
out.buffer.write(b'\x1bP' + send + b'\x1b\\')
|
||||
out.flush()
|
||||
if no_response:
|
||||
return {'ok': True}
|
||||
if not sys.stdin.isatty():
|
||||
raise SystemExit('stdin is not a terminal')
|
||||
|
||||
received = b''
|
||||
dcs = re.compile(br'\x1bP@kitty-cmd([^\x1b]+)\x1b\\')
|
||||
match = None
|
||||
|
||||
def more_needed(data):
|
||||
nonlocal received, match
|
||||
received += data
|
||||
match = dcs.search(received)
|
||||
return match is None
|
||||
|
||||
read_with_timeout(more_needed)
|
||||
if match is None:
|
||||
raise SystemExit('Failed to receive response from ' + appname)
|
||||
response = json.loads(match.group(1).decode('ascii'))
|
||||
return response
|
||||
|
||||
|
||||
def main(args):
|
||||
all_commands = tuple(sorted(cmap))
|
||||
cmds = (' |G {}|\n {}'.format(cmap[c].name, cmap[c].short_desc) for c in all_commands)
|
||||
msg = (
|
||||
'Control {appname} by sending it commands. Add'
|
||||
' |_ allow_remote_control yes| to kitty.conf for this'
|
||||
' to work.\n\n|T Commands|:\n{cmds}\n\n'
|
||||
'You can get help for each individual command by using:\n'
|
||||
'{appname} @ |_ command| -h'
|
||||
).format(appname=appname, cmds='\n'.join(cmds))
|
||||
|
||||
global_opts, items = parse_args(args[1:], global_options_spec, 'command ...', msg, '{} @'.format(appname))
|
||||
|
||||
if not items:
|
||||
raise SystemExit('You must specify a command')
|
||||
cmd = items[0]
|
||||
try:
|
||||
func = cmap[cmd]
|
||||
except KeyError:
|
||||
raise SystemExit('{} is not a known command. Known commands are: {}'.format(
|
||||
emph(cmd), ', '.join(all_commands)))
|
||||
opts, items = parse_subcommand_cli(func, items)
|
||||
payload = func(global_opts, opts, items)
|
||||
send = {
|
||||
'cmd': cmd,
|
||||
'version': version,
|
||||
}
|
||||
if func.no_response and isinstance(payload, types.GeneratorType):
|
||||
for item in payload:
|
||||
send['payload'] = item
|
||||
read_from_stdin(send, func.no_response)
|
||||
return
|
||||
if payload is not None:
|
||||
send['payload'] = payload
|
||||
response = read_from_stdin(send, func.no_response)
|
||||
if not response.get('ok'):
|
||||
if response.get('tb'):
|
||||
print(response['tb'], file=sys.stderr)
|
||||
raise SystemExit(response['error'])
|
||||
if 'data' in response:
|
||||
print(response['data'])
|
||||
148
kitty/screen.c
148
kitty/screen.c
@@ -23,6 +23,10 @@
|
||||
|
||||
static const ScreenModes empty_modes = {0, .mDECAWM=true, .mDECTCEM=true, .mDECARM=true};
|
||||
static Selection EMPTY_SELECTION = {0};
|
||||
typedef struct {
|
||||
unsigned int x; int y;
|
||||
} FullSelectionBoundary;
|
||||
|
||||
|
||||
// Constructor/destructor {{{
|
||||
|
||||
@@ -103,7 +107,7 @@ void
|
||||
screen_reset(Screen *self) {
|
||||
if (self->linebuf == self->alt_linebuf) screen_toggle_screen_buffer(self);
|
||||
linebuf_clear(self->linebuf, BLANK_CHAR);
|
||||
grman_clear(self->grman);
|
||||
grman_clear(self->grman, false);
|
||||
self->modes = empty_modes;
|
||||
#define R(name) self->color_profile->overridden.name = 0
|
||||
R(default_fg); R(default_bg); R(cursor_color); R(highlight_fg); R(highlight_bg);
|
||||
@@ -387,28 +391,29 @@ write_to_child(Screen *self, const char *data, size_t sz) {
|
||||
|
||||
void
|
||||
write_escape_code_to_child(Screen *self, unsigned char which, const char *data) {
|
||||
static char buf[512];
|
||||
size_t sz;
|
||||
const char *prefix, *suffix = self->modes.eight_bit_controls ? "\x9c" : "\033\\";
|
||||
switch(which) {
|
||||
case DCS:
|
||||
sz = snprintf(buf, sizeof(buf) - 1, "%s%s%s", self->modes.eight_bit_controls ? "\x90" : "\033P", data, self->modes.eight_bit_controls ? "\x9c" : "\033\\");
|
||||
prefix = self->modes.eight_bit_controls ? "\x90" : "\033P";
|
||||
break;
|
||||
case CSI:
|
||||
sz = snprintf(buf, sizeof(buf) - 1, "%s%s", self->modes.eight_bit_controls ? "\x9b" : "\033[", data);
|
||||
prefix = self->modes.eight_bit_controls ? "\x9b" : "\033["; suffix = "";
|
||||
break;
|
||||
case OSC:
|
||||
sz = snprintf(buf, sizeof(buf) - 1, "%s%s%s", self->modes.eight_bit_controls ? "\x9d" : "\033]", data, self->modes.eight_bit_controls ? "\x9c" : "\033\\");
|
||||
prefix = self->modes.eight_bit_controls ? "\x9d" : "\033]";
|
||||
break;
|
||||
case PM:
|
||||
sz = snprintf(buf, sizeof(buf) - 1, "%s%s%s", self->modes.eight_bit_controls ? "\x9e" : "\033^", data, self->modes.eight_bit_controls ? "\x9c" : "\033\\");
|
||||
prefix = self->modes.eight_bit_controls ? "\x9e" : "\033^";
|
||||
break;
|
||||
case APC:
|
||||
sz = snprintf(buf, sizeof(buf) - 1, "%s%s%s", self->modes.eight_bit_controls ? "\x9f" : "\033_", data, self->modes.eight_bit_controls ? "\x9c" : "\033\\");
|
||||
prefix = self->modes.eight_bit_controls ? "\x9f" : "\033_";
|
||||
break;
|
||||
default:
|
||||
fatal("Unknown escape code to write: %u", which);
|
||||
}
|
||||
write_to_child(self, buf, sz);
|
||||
write_to_child(self, prefix, strlen(prefix));
|
||||
write_to_child(self, data, strlen(data));
|
||||
if (suffix[0]) write_to_child(self, suffix, strlen(suffix));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -430,7 +435,7 @@ screen_handle_graphics_command(Screen *self, const GraphicsCommand *cmd, const u
|
||||
void
|
||||
screen_toggle_screen_buffer(Screen *self) {
|
||||
bool to_alt = self->linebuf == self->main_linebuf;
|
||||
grman_clear(self->alt_grman); // always clear the alt buffer graphics to free up resources, since it has to be cleared when switching back to it anyway
|
||||
grman_clear(self->alt_grman, true); // 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) {
|
||||
linebuf_clear(self->alt_linebuf, BLANK_CHAR);
|
||||
screen_save_cursor(self);
|
||||
@@ -882,6 +887,7 @@ screen_erase_in_display(Screen *self, unsigned int how, bool private) {
|
||||
including cursor position.
|
||||
* ``2`` -- Erases complete display. All lines are erased
|
||||
and changed to single-width. Cursor does not move.
|
||||
* ``3`` -- Erase complete display and scrollback buffer as well.
|
||||
:param bool private: when ``True`` character attributes are left unchanged
|
||||
*/
|
||||
unsigned int a, b;
|
||||
@@ -891,7 +897,8 @@ screen_erase_in_display(Screen *self, unsigned int how, bool private) {
|
||||
case 1:
|
||||
a = 0; b = self->cursor->y; break;
|
||||
case 2:
|
||||
grman_clear(self->grman);
|
||||
case 3:
|
||||
grman_clear(self->grman, how == 3);
|
||||
a = 0; b = self->lines; break;
|
||||
default:
|
||||
return;
|
||||
@@ -911,6 +918,13 @@ screen_erase_in_display(Screen *self, unsigned int how, bool private) {
|
||||
if (how != 2) {
|
||||
screen_erase_in_line(self, how, private);
|
||||
}
|
||||
if (how == 3 && self->linebuf == self->main_linebuf) {
|
||||
historybuf_clear(self->historybuf);
|
||||
if (self->scrolled_by != 0) {
|
||||
self->scrolled_by = 0;
|
||||
self->scroll_changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1132,6 +1146,11 @@ set_color_table_color(Screen *self, unsigned int code, PyObject *color) {
|
||||
else { CALLBACK("set_color_table_color", "IO", code, color); }
|
||||
}
|
||||
|
||||
void
|
||||
screen_handle_cmd(Screen *self, PyObject *cmd) {
|
||||
CALLBACK("handle_remote_cmd", "O", cmd);
|
||||
}
|
||||
|
||||
void
|
||||
screen_request_capabilities(Screen *self, char c, PyObject *q) {
|
||||
static char buf[128];
|
||||
@@ -1219,6 +1238,7 @@ screen_update_cell_data(Screen *self, void *address, size_t UNUSED sz) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static inline bool
|
||||
is_selection_empty(Screen *self, unsigned int start_x, unsigned int start_y, unsigned int end_x, unsigned int end_y) {
|
||||
return (start_x >= self->columns || start_y >= self->lines || end_x >= self->columns || end_y >= self->lines || (start_x == end_x && start_y == end_y)) ? true : false;
|
||||
@@ -1238,6 +1258,13 @@ selection_coord(Screen *self, unsigned int x, unsigned int y, unsigned int ydelt
|
||||
}
|
||||
}
|
||||
|
||||
static inline void
|
||||
full_selection_coord(Screen *self, unsigned int x, unsigned int y, unsigned int ydelta, FullSelectionBoundary *ans) {
|
||||
ans->x = MIN(x, self->columns - 1);
|
||||
ans->y = y - ydelta;
|
||||
if (y >= self->lines) { ans->x = self->columns - 1; ans->y = self->lines - 1; }
|
||||
}
|
||||
|
||||
#define selection_limits_(which, left, right) { \
|
||||
SelectionBoundary a, b; \
|
||||
selection_coord(self, self->which.start_x, self->which.start_y, self->which.start_scrolled_by, &a); \
|
||||
@@ -1246,6 +1273,15 @@ selection_coord(Screen *self, unsigned int x, unsigned int y, unsigned int ydelt
|
||||
else { *(left) = b; *(right) = a; } \
|
||||
}
|
||||
|
||||
#define full_selection_limits_(which, left, right) { \
|
||||
FullSelectionBoundary a, b; \
|
||||
full_selection_coord(self, self->which.start_x, self->which.start_y, self->which.start_scrolled_by, &a); \
|
||||
full_selection_coord(self, self->which.end_x, self->which.end_y, self->which.end_scrolled_by, &b); \
|
||||
if (a.y < b.y || (a.y == b.y && a.x <= b.x)) { *(left) = a; *(right) = b; } \
|
||||
else { *(left) = b; *(right) = a; } \
|
||||
}
|
||||
|
||||
|
||||
static inline Line*
|
||||
visual_line_(Screen *self, index_type y) {
|
||||
if (self->scrolled_by) {
|
||||
@@ -1259,18 +1295,28 @@ visual_line_(Screen *self, index_type y) {
|
||||
return self->linebuf->line;
|
||||
}
|
||||
|
||||
#define iterate_over_rectangle(start, end) { \
|
||||
index_type min_y = MIN(start->y, end->y), max_y = MAX(start->y, end->y); \
|
||||
static inline Line*
|
||||
range_line_(Screen *self, int y) {
|
||||
if (y < 0) {
|
||||
historybuf_init_line(self->historybuf, -y, self->historybuf->line);
|
||||
return self->historybuf->line;
|
||||
}
|
||||
linebuf_init_line(self->linebuf, y);
|
||||
return self->linebuf->line;
|
||||
}
|
||||
|
||||
#define iterate_over_rectangle(start, end, line_func, y_type) { \
|
||||
y_type min_y = MIN(start->y, end->y), max_y = MAX(start->y, end->y); \
|
||||
index_type min_x = MIN(start->x, end->x), max_x = MAX(start->x, end->x); \
|
||||
for (index_type y = min_y; y <= max_y; y++) { \
|
||||
Line *line = visual_line_(self, y); \
|
||||
for (y_type y = min_y; y <= max_y; y++) { \
|
||||
Line *line = line_func(self, y); \
|
||||
index_type xlimit = xlimit_for_line(line); \
|
||||
xlimit = MIN(max_x + 1, xlimit); \
|
||||
index_type x_start = min_x; \
|
||||
index_type x_start = min_x;
|
||||
|
||||
#define iterate_over_region(start, end) { \
|
||||
for (index_type y = start->y; y <= end->y; y++) { \
|
||||
Line *line = visual_line_(self, y); \
|
||||
#define iterate_over_region(start, end, line_func, y_type) { \
|
||||
for (y_type y = start->y; y <= end->y; y++) { \
|
||||
Line *line = line_func(self, y); \
|
||||
index_type xlimit = xlimit_for_line(line); \
|
||||
if (y == end->y) xlimit = MIN(end->x + 1, xlimit); \
|
||||
index_type x_start = y == start->y ? start->x : 0;
|
||||
@@ -1280,12 +1326,12 @@ static inline void
|
||||
apply_selection(Screen *self, uint8_t *data, SelectionBoundary *start, SelectionBoundary *end, uint8_t set_mask, bool rectangle_select) {
|
||||
if (is_selection_empty(self, start->x, start->y, end->x, end->y)) return;
|
||||
if (rectangle_select) {
|
||||
iterate_over_rectangle(start, end)
|
||||
iterate_over_rectangle(start, end, visual_line_, index_type)
|
||||
uint8_t *line_start = data + self->columns * y;
|
||||
for (index_type x = x_start; x < xlimit; x++) line_start[x] |= set_mask;
|
||||
}}
|
||||
} else {
|
||||
iterate_over_region(start, end)
|
||||
iterate_over_region(start, end, visual_line_, index_type)
|
||||
uint8_t *line_start = data + self->columns * y;
|
||||
for (index_type x = x_start; x < xlimit; x++) line_start[x] |= set_mask;
|
||||
}}
|
||||
@@ -1304,35 +1350,35 @@ screen_apply_selection(Screen *self, void *address, size_t size) {
|
||||
apply_selection(self, address, &self->last_rendered_url_start, &self->last_rendered_url_end, 2, false);
|
||||
}
|
||||
|
||||
static inline PyObject*
|
||||
text_for_range(Screen *self, SelectionBoundary start, SelectionBoundary end, bool rectangle_select, bool insert_newlines) {
|
||||
int num_of_lines = end.y - start.y + 1, i = 0;
|
||||
PyObject *ans = PyTuple_New(num_of_lines);
|
||||
if (ans == NULL) return PyErr_NoMemory();
|
||||
#define action \
|
||||
char leading_char = (i > 0 && insert_newlines && !line->continued) ? '\n' : 0; \
|
||||
PyObject *text = unicode_in_range(line, x_start, xlimit, true, leading_char); \
|
||||
if (text == NULL) { Py_DECREF(ans); return PyErr_NoMemory(); } \
|
||||
PyTuple_SET_ITEM(ans, i++, text);
|
||||
|
||||
if (rectangle_select) {
|
||||
iterate_over_rectangle((&start), (&end))
|
||||
action
|
||||
} }} else {
|
||||
iterate_over_region((&start), (&end))
|
||||
action
|
||||
} }}
|
||||
#undef action
|
||||
return ans;
|
||||
#define text_for_range_action(ans, insert_newlines) { \
|
||||
char leading_char = (i > 0 && insert_newlines && !line->continued) ? '\n' : 0; \
|
||||
PyObject *text = unicode_in_range(line, x_start, xlimit, true, leading_char); \
|
||||
if (text == NULL) { Py_DECREF(ans); return PyErr_NoMemory(); } \
|
||||
PyTuple_SET_ITEM(ans, i++, text); \
|
||||
}
|
||||
|
||||
#define text_for_range(ans, start, end, rectangle_select, insert_newlines, line_func, y_type) { \
|
||||
int num_of_lines = end.y - start.y + 1, i = 0; \
|
||||
ans = PyTuple_New(num_of_lines); \
|
||||
if (ans == NULL) return PyErr_NoMemory(); \
|
||||
if (rectangle_select) { \
|
||||
iterate_over_rectangle((&start), (&end), line_func, y_type) \
|
||||
text_for_range_action(ans, insert_newlines); \
|
||||
} }} else { \
|
||||
iterate_over_region((&start), (&end), line_func, y_type) \
|
||||
text_for_range_action(ans, insert_newlines); \
|
||||
} }} \
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
screen_open_url(Screen *self) {
|
||||
SelectionBoundary start, end;
|
||||
selection_limits_(url_range, &start, &end);
|
||||
if (is_selection_empty(self, start.x, start.y, end.x, end.y)) return false;
|
||||
PyObject *text = text_for_range(self, start, end, false, false);
|
||||
if (text) { call_boss(open_url_lines, "(O)", text); Py_CLEAR(text); }
|
||||
PyObject *url;
|
||||
text_for_range(url, start, end, false, false, visual_line_, index_type);
|
||||
if (url) { call_boss(open_url_lines, "(O)", url); Py_CLEAR(url); }
|
||||
else PyErr_Print();
|
||||
return true;
|
||||
}
|
||||
@@ -1343,10 +1389,10 @@ screen_open_url(Screen *self) {
|
||||
#define WRAP0(name) static PyObject* name(Screen *self) { screen_##name(self); Py_RETURN_NONE; }
|
||||
#define WRAP0x(name) static PyObject* xxx_##name(Screen *self) { screen_##name(self); Py_RETURN_NONE; }
|
||||
#define WRAP1(name, defval) static PyObject* name(Screen *self, PyObject *args) { unsigned int v=defval; if(!PyArg_ParseTuple(args, "|I", &v)) return NULL; screen_##name(self, v); Py_RETURN_NONE; }
|
||||
#define WRAP1E(name, defval, ...) static PyObject* name(Screen *self, PyObject *args) { unsigned int v=defval; if(!PyArg_ParseTuple(args, "|I", &v)) return NULL; screen_##name(self, v, __VA_ARGS__); Py_RETURN_NONE; }
|
||||
#define WRAP1B(name, defval) static PyObject* name(Screen *self, PyObject *args) { unsigned int v=defval; int b=false; if(!PyArg_ParseTuple(args, "|Ip", &v, &b)) return NULL; screen_##name(self, v, b); Py_RETURN_NONE; }
|
||||
#define WRAP1E(name, defval, ...) static PyObject* name(Screen *self, PyObject *args) { unsigned int v=defval; if(!PyArg_ParseTuple(args, "|I", &v)) return NULL; screen_##name(self, v, __VA_ARGS__); Py_RETURN_NONE; }
|
||||
#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; }
|
||||
|
||||
static PyObject*
|
||||
refresh_sprite_positions(Screen *self) {
|
||||
@@ -1479,6 +1525,8 @@ WRAP0(carriage_return)
|
||||
WRAP2(resize, 1, 1)
|
||||
WRAP2(set_margins, 1, 1)
|
||||
WRAP2(rescale_images, 1, 1)
|
||||
WRAP2B(start_selection)
|
||||
WRAP2B(update_selection)
|
||||
|
||||
static PyObject*
|
||||
change_scrollback_size(Screen *self, PyObject *args) {
|
||||
@@ -1490,10 +1538,12 @@ change_scrollback_size(Screen *self, PyObject *args) {
|
||||
|
||||
static PyObject*
|
||||
text_for_selection(Screen *self) {
|
||||
SelectionBoundary start, end;
|
||||
selection_limits_(selection, &start, &end);
|
||||
if (is_selection_empty(self, start.x, start.y, end.x, end.y)) return PyTuple_New(0);
|
||||
return text_for_range(self, start, end, self->rectangle_select, true);
|
||||
FullSelectionBoundary start, end;
|
||||
full_selection_limits_(selection, &start, &end);
|
||||
PyObject *ans = NULL;
|
||||
if (start.y == end.y && start.x == end.x) ans = PyTuple_New(0);
|
||||
else text_for_range(ans, start, end, self->rectangle_select, true, range_line_, int);
|
||||
return ans;
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -1694,6 +1744,8 @@ static PyMethodDef methods[] = {
|
||||
MND(carriage_return, METH_NOARGS)
|
||||
MND(set_tab_stop, METH_NOARGS)
|
||||
MND(clear_tab_stop, METH_VARARGS)
|
||||
MND(start_selection, METH_VARARGS)
|
||||
MND(update_selection, METH_VARARGS)
|
||||
MND(reverse_index, METH_NOARGS)
|
||||
MND(mark_as_dirty, METH_NOARGS)
|
||||
MND(resize, METH_VARARGS)
|
||||
|
||||
@@ -106,6 +106,7 @@ void screen_delete_characters(Screen *self, unsigned int count);
|
||||
void screen_erase_characters(Screen *self, unsigned int count);
|
||||
void screen_set_margins(Screen *self, unsigned int top, unsigned int bottom);
|
||||
void screen_change_charset(Screen *, uint32_t to);
|
||||
void screen_handle_cmd(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*);
|
||||
|
||||
@@ -18,6 +18,7 @@ class Tab:
|
||||
self.enabled_layouts = opts.enabled_layouts
|
||||
self.layout = (self.enabled_layouts or ['tall'])[0]
|
||||
self.cwd = None
|
||||
self.next_title = None
|
||||
|
||||
|
||||
class Session:
|
||||
@@ -31,6 +32,9 @@ class Session:
|
||||
del self.tabs[-1]
|
||||
self.tabs.append(Tab(opts, name))
|
||||
|
||||
def set_next_title(self, title):
|
||||
self.tabs[-1].next_title = title.strip()
|
||||
|
||||
def set_layout(self, val):
|
||||
if val not in all_layouts:
|
||||
raise ValueError('{} is not a valid layout'.format(val))
|
||||
@@ -41,7 +45,10 @@ class Session:
|
||||
cmd = shlex.split(cmd) if isinstance(cmd, str) else cmd
|
||||
else:
|
||||
cmd = None
|
||||
self.tabs[-1].windows.append(cmd)
|
||||
from .tabs import SpecialWindow
|
||||
t = self.tabs[-1]
|
||||
t.windows.append(SpecialWindow(cmd, cwd=t.cwd, override_title=t.next_title))
|
||||
t.next_title = None
|
||||
|
||||
def add_special_window(self, sw):
|
||||
self.tabs[-1].windows.append(sw)
|
||||
@@ -57,6 +64,15 @@ class Session:
|
||||
self.tabs[-1].cwd = val
|
||||
|
||||
|
||||
def resolved_shell(opts):
|
||||
ans = opts.shell
|
||||
if ans == '.':
|
||||
ans = [shell_path]
|
||||
else:
|
||||
ans = shlex.split(ans)
|
||||
return ans
|
||||
|
||||
|
||||
def parse_session(raw, opts):
|
||||
ans = Session()
|
||||
ans.add_tab(opts)
|
||||
@@ -77,11 +93,13 @@ def parse_session(raw, opts):
|
||||
ans.set_enabled_layouts(rest)
|
||||
elif cmd == 'cd':
|
||||
ans.set_cwd(rest)
|
||||
elif cmd == 'title':
|
||||
ans.set_next_title(rest)
|
||||
else:
|
||||
raise ValueError('Unknown command in session file: {}'.format(cmd))
|
||||
for t in ans.tabs:
|
||||
if not t.windows:
|
||||
t.windows.append([shell_path])
|
||||
t.windows.append(resolved_shell(opts))
|
||||
return ans
|
||||
|
||||
|
||||
@@ -99,12 +117,11 @@ def create_session(opts, args=None, special_window=None, cwd_from=None):
|
||||
ans.add_tab(opts)
|
||||
ans.tabs[-1].layout = current_layout
|
||||
if special_window is None:
|
||||
cmd = args.args if args and args.args else [shell_path]
|
||||
cmd = args.args if args and args.args else resolved_shell(opts)
|
||||
from kitty.tabs import SpecialWindow
|
||||
k = {'cwd_from': cwd_from}
|
||||
if getattr(args, 'title', None):
|
||||
ans.add_window(SpecialWindow(cmd, override_title=args.title, cwd_from=cwd_from))
|
||||
else:
|
||||
ans.add_window(SpecialWindow(cmd, cwd_from=cwd_from))
|
||||
else:
|
||||
ans.add_special_window(special_window)
|
||||
k['override_title'] = args.title
|
||||
special_window = SpecialWindow(cmd, **k)
|
||||
ans.add_special_window(special_window)
|
||||
return ans
|
||||
|
||||
@@ -242,6 +242,31 @@ add_borders_rect(id_type os_window_id, id_type tab_id, uint32_t left, uint32_t t
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
os_window_regions(OSWindow *os_window, Region *central, Region *tab_bar) {
|
||||
if (os_window->num_tabs > 1) {
|
||||
switch(OPT(tab_bar_edge)) {
|
||||
case TOP_EDGE:
|
||||
central->left = 0; central->top = global_state.cell_height; central->right = os_window->viewport_width - 1;
|
||||
central->bottom = os_window->viewport_height - 1;
|
||||
tab_bar->left = central->left; tab_bar->right = central->right; tab_bar->top = 0;
|
||||
tab_bar->bottom = central->top - 1;
|
||||
break;
|
||||
default:
|
||||
central->left = 0; central->top = 0; central->right = os_window->viewport_width - 1;
|
||||
central->bottom = os_window->viewport_height - global_state.cell_height - 1;
|
||||
tab_bar->left = central->left; tab_bar->right = central->right; tab_bar->top = central->bottom + 1;
|
||||
tab_bar->bottom = os_window->viewport_height - 1;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
memset(tab_bar, 0, sizeof(Region));
|
||||
central->left = 0; central->top = 0; central->right = os_window->viewport_width - 1;
|
||||
central->bottom = os_window->viewport_height - 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Python API {{{
|
||||
#define PYWRAP0(name) static PyObject* py##name(PyObject UNUSED *self)
|
||||
#define PYWRAP1(name) static PyObject* py##name(PyObject UNUSED *self, PyObject *args)
|
||||
@@ -315,6 +340,7 @@ PYWRAP1(set_options) {
|
||||
S(cursor_shape, PyLong_AsLong);
|
||||
S(url_style, PyLong_AsUnsignedLong);
|
||||
S(x11_bell_volume, PyLong_AsLong);
|
||||
S(tab_bar_edge, PyLong_AsLong);
|
||||
S(mouse_hide_wait, PyFloat_AsDouble);
|
||||
S(wheel_scroll_multiplier, PyFloat_AsDouble);
|
||||
S(open_url_modifiers, PyLong_AsUnsignedLong);
|
||||
@@ -364,15 +390,38 @@ PYWRAP1(set_tab_bar_render_data) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyTypeObject RegionType;
|
||||
static PyStructSequence_Field region_fields[] = {
|
||||
{"left", ""}, {"top", ""}, {"right", ""}, {"bottom", ""}, {"width", ""}, {"height", ""}, {NULL, NULL}
|
||||
};
|
||||
static PyStructSequence_Desc region_desc = {"Region", NULL, region_fields, 6};
|
||||
|
||||
static inline PyObject*
|
||||
wrap_region(Region *r) {
|
||||
PyObject *ans = PyStructSequence_New(&RegionType);
|
||||
if (ans) {
|
||||
PyStructSequence_SET_ITEM(ans, 0, PyLong_FromUnsignedLong(r->left));
|
||||
PyStructSequence_SET_ITEM(ans, 1, PyLong_FromUnsignedLong(r->top));
|
||||
PyStructSequence_SET_ITEM(ans, 2, PyLong_FromUnsignedLong(r->right));
|
||||
PyStructSequence_SET_ITEM(ans, 3, PyLong_FromUnsignedLong(r->bottom));
|
||||
PyStructSequence_SET_ITEM(ans, 4, PyLong_FromUnsignedLong(r->right - r->left + 1));
|
||||
PyStructSequence_SET_ITEM(ans, 5, PyLong_FromUnsignedLong(r->bottom - r->top + 1));
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
|
||||
PYWRAP1(viewport_for_window) {
|
||||
id_type os_window_id = 0;
|
||||
int vw = 100, vh = 100;
|
||||
PA("|K", &os_window_id);
|
||||
Region central = {0}, tab_bar = {0};
|
||||
WITH_OS_WINDOW(os_window_id)
|
||||
int available_height = os_window->viewport_height;
|
||||
if (os_window->num_tabs > 1) available_height -= global_state.cell_height;
|
||||
return Py_BuildValue("iiiII", os_window->viewport_width, os_window->viewport_height, available_height, global_state.cell_width, global_state.cell_height);
|
||||
os_window_regions(os_window, ¢ral, &tab_bar);
|
||||
vw = os_window->viewport_width; vh = os_window->viewport_height;
|
||||
goto end;
|
||||
END_WITH_OS_WINDOW
|
||||
return Py_BuildValue("iiII", 400, 400, global_state.cell_width, global_state.cell_height);
|
||||
end:
|
||||
return Py_BuildValue("NNiiII", wrap_region(¢ral), wrap_region(&tab_bar), vw, vh, global_state.cell_width, global_state.cell_height);
|
||||
}
|
||||
|
||||
PYWRAP1(set_dpi_from_os_window) {
|
||||
@@ -514,6 +563,9 @@ bool
|
||||
init_state(PyObject *module) {
|
||||
global_state.cell_width = 1; global_state.cell_height = 1;
|
||||
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
|
||||
if (PyStructSequence_InitType2(&RegionType, ®ion_desc) != 0) return false;
|
||||
Py_INCREF((PyObject *) &RegionType);
|
||||
PyModule_AddObject(module, "Region", (PyObject *) &RegionType);
|
||||
return true;
|
||||
}
|
||||
// }}}
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
#define OPT(name) global_state.opts.name
|
||||
|
||||
typedef enum { LEFT_EDGE, TOP_EDGE, RIGHT_EDGE, BOTTOM_EDGE } Edge;
|
||||
|
||||
typedef struct {
|
||||
double visual_bell_duration, cursor_blink_interval, cursor_stop_blinking_after, mouse_hide_wait, click_interval, wheel_scroll_multiplier;
|
||||
bool enable_audio_bell;
|
||||
@@ -28,6 +30,7 @@ typedef struct {
|
||||
float adjust_line_height_frac;
|
||||
float background_opacity;
|
||||
float inactive_text_alpha;
|
||||
Edge tab_bar_edge;
|
||||
} Options;
|
||||
|
||||
typedef struct {
|
||||
@@ -160,6 +163,7 @@ void set_os_window_title(OSWindow *w, const char *title);
|
||||
OSWindow* os_window_for_kitty_window(id_type);
|
||||
OSWindow* add_os_window();
|
||||
OSWindow* current_os_window();
|
||||
void os_window_regions(OSWindow*, Region *main, Region *tab_bar);
|
||||
bool drag_scroll(Window *, OSWindow*);
|
||||
void draw_borders(ssize_t vao_idx, unsigned int num_border_rects, BorderRect *rect_buf, bool rect_data_is_dirty, uint32_t viewport_width, uint32_t viewport_height);
|
||||
ssize_t create_cell_vao();
|
||||
|
||||
108
kitty/tabs.py
108
kitty/tabs.py
@@ -9,24 +9,23 @@ from functools import partial
|
||||
from .borders import Borders
|
||||
from .child import Child
|
||||
from .config import build_ansi_color_table
|
||||
from .constants import (
|
||||
WindowGeometry, appname, get_boss, is_macos, is_wayland, shell_path
|
||||
)
|
||||
from .constants import WindowGeometry, appname, get_boss, is_macos, is_wayland
|
||||
from .fast_data_types import (
|
||||
DECAWM, Screen, add_tab, glfw_post_empty_event, remove_tab, remove_window,
|
||||
set_active_tab, set_active_window, set_tab_bar_render_data, swap_tabs,
|
||||
swap_windows, viewport_for_window, x11_window_id
|
||||
)
|
||||
from .layout import Rect, all_layouts
|
||||
from .session import resolved_shell
|
||||
from .utils import color_as_int
|
||||
from .window import Window, calculate_gl_geometry
|
||||
|
||||
TabbarData = namedtuple('TabbarData', 'title is_active is_last')
|
||||
SpecialWindowInstance = namedtuple('SpecialWindow', 'cmd stdin override_title cwd_from')
|
||||
SpecialWindowInstance = namedtuple('SpecialWindow', 'cmd stdin override_title cwd_from cwd')
|
||||
|
||||
|
||||
def SpecialWindow(cmd, stdin=None, override_title=None, cwd_from=None):
|
||||
return SpecialWindowInstance(cmd, stdin, override_title, cwd_from)
|
||||
def SpecialWindow(cmd, stdin=None, override_title=None, cwd_from=None, cwd=None):
|
||||
return SpecialWindowInstance(cmd, stdin, override_title, cwd_from, cwd)
|
||||
|
||||
|
||||
class Tab: # {{{
|
||||
@@ -75,6 +74,12 @@ class Tab: # {{{
|
||||
def title(self):
|
||||
return getattr(self.active_window, 'title', appname)
|
||||
|
||||
def set_title(self, title):
|
||||
self.name = title or ''
|
||||
tm = self.tab_manager_ref()
|
||||
if tm is not None:
|
||||
tm.title_changed(self.name)
|
||||
|
||||
def title_changed(self, window):
|
||||
if window is self.active_window:
|
||||
tm = self.tab_manager_ref()
|
||||
@@ -109,12 +114,12 @@ class Tab: # {{{
|
||||
w.set_visible_in_layout(i, True)
|
||||
self.relayout()
|
||||
|
||||
def launch_child(self, use_shell=False, cmd=None, stdin=None, cwd_from=None):
|
||||
def launch_child(self, use_shell=False, cmd=None, stdin=None, cwd_from=None, cwd=None):
|
||||
if cmd is None:
|
||||
if use_shell:
|
||||
cmd = [shell_path]
|
||||
cmd = resolved_shell(self.opts)
|
||||
else:
|
||||
cmd = self.args.args or [shell_path]
|
||||
cmd = self.args.args or resolved_shell(self.opts)
|
||||
env = {}
|
||||
if not is_macos and not is_wayland:
|
||||
try:
|
||||
@@ -122,12 +127,12 @@ class Tab: # {{{
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
ans = Child(cmd, self.cwd, self.opts, stdin, env, cwd_from)
|
||||
ans = Child(cmd, cwd or self.cwd, self.opts, stdin, env, cwd_from)
|
||||
ans.fork()
|
||||
return ans
|
||||
|
||||
def new_window(self, use_shell=True, cmd=None, stdin=None, override_title=None, cwd_from=None):
|
||||
child = self.launch_child(use_shell=use_shell, cmd=cmd, stdin=stdin, cwd_from=cwd_from)
|
||||
def new_window(self, use_shell=True, cmd=None, stdin=None, override_title=None, cwd_from=None, cwd=None):
|
||||
child = self.launch_child(use_shell=use_shell, cmd=cmd, stdin=stdin, cwd_from=cwd_from, cwd=cwd)
|
||||
window = Window(self, child, self.opts, self.args, override_title=override_title)
|
||||
# Must add child before laying out so that resize_pty succeeds
|
||||
get_boss().add_child(window)
|
||||
@@ -138,7 +143,7 @@ class Tab: # {{{
|
||||
return window
|
||||
|
||||
def new_special_window(self, special_window):
|
||||
self.new_window(False, *special_window)
|
||||
return self.new_window(False, *special_window)
|
||||
|
||||
def close_window(self):
|
||||
if self.windows:
|
||||
@@ -202,6 +207,17 @@ class Tab: # {{{
|
||||
def move_window_backward(self):
|
||||
self.move_window(-1)
|
||||
|
||||
def list_windows(self):
|
||||
for w in self:
|
||||
yield w.as_dict()
|
||||
|
||||
def matches(self, field, pat):
|
||||
if field == 'id':
|
||||
return pat.pattern == str(self.id)
|
||||
if field == 'title':
|
||||
return pat.search(self.name or self.title) is not None
|
||||
return False
|
||||
|
||||
def __iter__(self):
|
||||
yield from iter(self.windows)
|
||||
|
||||
@@ -228,7 +244,7 @@ class TabBar: # {{{
|
||||
self.num_tabs = 1
|
||||
self.cell_width = 1
|
||||
self.data_buffer_size = 0
|
||||
self.layout_changed = None
|
||||
self.laid_out_once = False
|
||||
self.dirty = True
|
||||
self.screen = s = Screen(None, 1, 10)
|
||||
s.color_profile.update_ansi_color_table(build_ansi_color_table(opts))
|
||||
@@ -255,25 +271,29 @@ class TabBar: # {{{
|
||||
self.active_bg = as_rgb(color_as_int(opts.active_tab_background))
|
||||
self.active_fg = as_rgb(color_as_int(opts.active_tab_foreground))
|
||||
|
||||
def layout(self, viewport_width, viewport_height, cell_width, cell_height):
|
||||
def layout(self):
|
||||
central, tab_bar, vw, vh, cell_width, cell_height = viewport_for_window(self.os_window_id)
|
||||
if tab_bar.width < 2:
|
||||
return
|
||||
self.cell_width = cell_width
|
||||
s = self.screen
|
||||
viewport_width = tab_bar.width
|
||||
ncells = viewport_width // cell_width
|
||||
s.resize(1, ncells)
|
||||
s.reset_mode(DECAWM)
|
||||
self.layout_changed = True
|
||||
self.laid_out_once = True
|
||||
margin = (viewport_width - ncells * cell_width) // 2
|
||||
self.window_geometry = g = WindowGeometry(
|
||||
margin, viewport_height - cell_height, viewport_width - margin, viewport_height, s.columns, s.lines)
|
||||
margin, tab_bar.top, viewport_width - margin, tab_bar.bottom, s.columns, s.lines)
|
||||
if margin > 0:
|
||||
self.tab_bar_blank_rects = (Rect(0, g.top, g.left, g.bottom), Rect(g.right - 1, g.top, viewport_width, g.bottom))
|
||||
self.blank_rects = (Rect(0, g.top, g.left, g.bottom + 1), Rect(g.right - 1, g.top, viewport_width, g.bottom + 1))
|
||||
else:
|
||||
self.tab_bar_blank_rects = ()
|
||||
self.screen_geometry = sg = calculate_gl_geometry(g, viewport_width, viewport_height, cell_width, cell_height)
|
||||
self.blank_rects = ()
|
||||
self.screen_geometry = sg = calculate_gl_geometry(g, vw, vh, cell_width, cell_height)
|
||||
set_tab_bar_render_data(self.os_window_id, sg.xstart, sg.ystart, sg.dx, sg.dy, self.screen)
|
||||
|
||||
def update(self, data):
|
||||
if self.layout_changed is None:
|
||||
if not self.laid_out_once:
|
||||
return
|
||||
s = self.screen
|
||||
s.cursor.x = 0
|
||||
@@ -321,31 +341,35 @@ class TabManager: # {{{
|
||||
self.opts, self.args = opts, args
|
||||
self.tabs = []
|
||||
self.tab_bar = TabBar(self.os_window_id, opts)
|
||||
self.tab_bar.layout(*self.tab_bar_layout_data)
|
||||
self.active_tab_idx = 0
|
||||
|
||||
for t in startup_session.tabs:
|
||||
self._add_tab(Tab(self, session_tab=t))
|
||||
self._set_active_tab(max(0, min(startup_session.active_tab_idx, len(self.tabs) - 1)))
|
||||
if len(self.tabs) > 1:
|
||||
self.tabbar_visibility_changed()
|
||||
self.update_tab_bar()
|
||||
|
||||
def refresh_sprite_positions(self):
|
||||
self.tab_bar.screen.refresh_sprite_positions()
|
||||
|
||||
def _add_tab(self, tab):
|
||||
before = len(self.tabs)
|
||||
self.tabs.append(tab)
|
||||
if len(self.tabs) > 1 and before < 2:
|
||||
self.tabbar_visibility_changed()
|
||||
|
||||
def _remove_tab(self, tab):
|
||||
before = len(self.tabs)
|
||||
remove_tab(self.os_window_id, tab.id)
|
||||
self.tabs.remove(tab)
|
||||
if len(self.tabs) < 2 and before > 1:
|
||||
self.tabbar_visibility_changed()
|
||||
|
||||
def _set_active_tab(self, idx):
|
||||
self.active_tab_idx = idx
|
||||
set_active_tab(self.os_window_id, idx)
|
||||
|
||||
def tabbar_visibility_changed(self):
|
||||
self.tab_bar.layout()
|
||||
self.resize(only_tabs=True)
|
||||
glfw_post_empty_event()
|
||||
|
||||
@@ -355,23 +379,30 @@ class TabManager: # {{{
|
||||
|
||||
def resize(self, only_tabs=False):
|
||||
if not only_tabs:
|
||||
self.tab_bar.layout(*self.tab_bar_layout_data)
|
||||
self.tab_bar.layout()
|
||||
self.update_tab_bar()
|
||||
for tab in self.tabs:
|
||||
tab.relayout()
|
||||
|
||||
def set_active_tab(self, idx):
|
||||
def set_active_tab_idx(self, idx):
|
||||
self._set_active_tab(idx)
|
||||
self.active_tab.relayout_borders()
|
||||
self.update_tab_bar()
|
||||
|
||||
def set_active_tab(self, tab):
|
||||
try:
|
||||
idx = self.tabs.index(tab)
|
||||
except Exception:
|
||||
return
|
||||
self.set_active_tab_idx(idx)
|
||||
|
||||
def next_tab(self, delta=1):
|
||||
if len(self.tabs) > 1:
|
||||
self.set_active_tab((self.active_tab_idx + len(self.tabs) + delta) % len(self.tabs))
|
||||
self.set_active_tab_idx((self.active_tab_idx + len(self.tabs) + delta) % len(self.tabs))
|
||||
|
||||
def goto_tab(self, tab_num):
|
||||
if tab_num < len(self.tabs) and 0 <= tab_num:
|
||||
self.set_active_tab(tab_num)
|
||||
self.set_active_tab_idx(tab_num)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.tabs)
|
||||
@@ -379,6 +410,14 @@ class TabManager: # {{{
|
||||
def __len__(self):
|
||||
return len(self.tabs)
|
||||
|
||||
def list_tabs(self):
|
||||
for tab in self:
|
||||
yield {
|
||||
'id': tab.id,
|
||||
'title': tab.name or tab.title,
|
||||
'windows': list(tab.list_windows()),
|
||||
}
|
||||
|
||||
@property
|
||||
def active_tab(self):
|
||||
return self.tabs[self.active_tab_idx] if self.tabs else None
|
||||
@@ -402,27 +441,16 @@ class TabManager: # {{{
|
||||
self.update_tab_bar()
|
||||
|
||||
def new_tab(self, special_window=None, cwd_from=None):
|
||||
needs_resize = len(self.tabs) == 1
|
||||
idx = len(self.tabs)
|
||||
self._add_tab(Tab(self, special_window=special_window, cwd_from=cwd_from))
|
||||
self._set_active_tab(idx)
|
||||
self.update_tab_bar()
|
||||
if needs_resize:
|
||||
self.tabbar_visibility_changed()
|
||||
|
||||
def remove(self, tab):
|
||||
needs_resize = len(self.tabs) == 2
|
||||
self._remove_tab(tab)
|
||||
self._set_active_tab(max(0, min(self.active_tab_idx, len(self.tabs) - 1)))
|
||||
self.update_tab_bar()
|
||||
tab.destroy()
|
||||
if needs_resize:
|
||||
self.tabbar_visibility_changed()
|
||||
|
||||
@property
|
||||
def tab_bar_layout_data(self):
|
||||
vw, vh, ah, cw, ch = viewport_for_window(self.os_window_id)
|
||||
return vw, vh, cw, ch
|
||||
|
||||
@property
|
||||
def tab_bar_data(self):
|
||||
@@ -436,7 +464,7 @@ class TabManager: # {{{
|
||||
def activate_tab_at(self, x):
|
||||
i = self.tab_bar.tab_at(x)
|
||||
if i is not None:
|
||||
self.set_active_tab(i)
|
||||
self.set_active_tab_idx(i)
|
||||
|
||||
@property
|
||||
def blank_rects(self):
|
||||
|
||||
2606
kitty/unicode-data.c
Normal file
2606
kitty/unicode-data.c
Normal file
File diff suppressed because one or more lines are too long
@@ -1,41 +1,24 @@
|
||||
#pragma once
|
||||
#include "data-types.h"
|
||||
|
||||
#include <unictype.h>
|
||||
#include <uninorm.h>
|
||||
|
||||
static inline bool
|
||||
is_combining_char(uint32_t ch) {
|
||||
return uc_combining_class(ch) != UC_CCC_NR;
|
||||
}
|
||||
|
||||
|
||||
static inline bool
|
||||
is_ignored_char(uint32_t ch) {
|
||||
return uc_is_general_category_withtable(ch, UC_CATEGORY_MASK_Cc | UC_CATEGORY_MASK_Cf | UC_CATEGORY_MASK_Cs);
|
||||
}
|
||||
|
||||
static inline bool
|
||||
is_word_char(uint32_t ch) {
|
||||
return uc_is_general_category_withtable(ch, UC_CATEGORY_MASK_L | UC_CATEGORY_MASK_N);
|
||||
}
|
||||
bool is_combining_char(char_type ch);
|
||||
bool is_ignored_char(char_type ch);
|
||||
bool is_word_char(char_type ch);
|
||||
bool is_CZ_category(char_type);
|
||||
bool is_P_category(char_type);
|
||||
char_type codepoint_for_mark(combining_type m);
|
||||
combining_type mark_for_codepoint(char_type c);
|
||||
|
||||
static inline bool
|
||||
is_url_char(uint32_t ch) {
|
||||
return ch && !uc_is_general_category_withtable(ch, UC_CATEGORY_MASK_C | UC_CATEGORY_MASK_Z);
|
||||
}
|
||||
|
||||
static inline uint32_t
|
||||
normalize(uint32_t ch, uint32_t cc1, uint32_t cc2) {
|
||||
uint32_t ans = uc_composition(ch, cc1);
|
||||
if (ans && cc2) ans = uc_composition(ans, cc2);
|
||||
return ans;
|
||||
return ch && !is_CZ_category(ch);
|
||||
}
|
||||
|
||||
static inline bool
|
||||
can_strip_from_end_of_url(uint32_t ch) {
|
||||
// remove trailing punctuation
|
||||
return (
|
||||
(uc_is_general_category_withtable(ch, UC_CATEGORY_MASK_P) && ch != '/') ||
|
||||
(is_P_category(ch) && ch != '/') ||
|
||||
ch == '>'
|
||||
) ? true : false;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import shlex
|
||||
import socket
|
||||
import string
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from contextlib import contextmanager
|
||||
from functools import lru_cache
|
||||
@@ -264,3 +265,36 @@ def single_instance(group_id=None):
|
||||
s.set_inheritable(False)
|
||||
atexit.register(remove_socket_file, s)
|
||||
return True
|
||||
|
||||
|
||||
@contextmanager
|
||||
def non_blocking_read(src=sys.stdin):
|
||||
import termios
|
||||
import tty
|
||||
import fcntl
|
||||
fd = src.fileno()
|
||||
if src.isatty():
|
||||
old = termios.tcgetattr(fd)
|
||||
tty.setraw(fd)
|
||||
oldfl = fcntl.fcntl(fd, fcntl.F_GETFL)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, oldfl | os.O_NONBLOCK)
|
||||
yield fd
|
||||
if src.isatty():
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, oldfl)
|
||||
|
||||
|
||||
def read_with_timeout(more_needed, timeout=10, src=sys.stdin):
|
||||
import select
|
||||
start_time = monotonic()
|
||||
with non_blocking_read(src) as fd:
|
||||
while timeout > monotonic() - start_time:
|
||||
rd = select.select([fd], [], [], max(0, timeout - (monotonic() - start_time)))[0]
|
||||
if rd:
|
||||
data = sys.stdin.buffer.read()
|
||||
if not data:
|
||||
break # eof
|
||||
if not more_needed(data):
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
376
kitty/wcwidth-std.h
generated
376
kitty/wcwidth-std.h
generated
@@ -1,4 +1,4 @@
|
||||
// unicode data, built from the unicode standard on: 2017-12-20
|
||||
// unicode data, built from the unicode standard on: 2018-01-18
|
||||
// see gen-wcwidth.py
|
||||
#pragma once
|
||||
#include "data-types.h"
|
||||
@@ -53,12 +53,10 @@ wcwidth_std(int32_t code) {
|
||||
return -1;
|
||||
// }}}
|
||||
|
||||
// Combining characters (826 codepoints) {{{
|
||||
case 0x300 ... 0x34e:
|
||||
// Marks (2177 codepoints) {{{
|
||||
case 0x300 ... 0x36f:
|
||||
return -1;
|
||||
case 0x350 ... 0x36f:
|
||||
return -1;
|
||||
case 0x483 ... 0x487:
|
||||
case 0x483 ... 0x489:
|
||||
return -1;
|
||||
case 0x591 ... 0x5bd:
|
||||
return -1;
|
||||
@@ -88,6 +86,8 @@ wcwidth_std(int32_t code) {
|
||||
return -1;
|
||||
case 0x730 ... 0x74a:
|
||||
return -1;
|
||||
case 0x7a6 ... 0x7b0:
|
||||
return -1;
|
||||
case 0x7eb ... 0x7f3:
|
||||
return -1;
|
||||
case 0x816 ... 0x819:
|
||||
@@ -102,53 +102,149 @@ wcwidth_std(int32_t code) {
|
||||
return -1;
|
||||
case 0x8d4 ... 0x8e1:
|
||||
return -1;
|
||||
case 0x8e3 ... 0x8ff:
|
||||
case 0x8e3 ... 0x903:
|
||||
return -1;
|
||||
case 0x93c:
|
||||
case 0x93a ... 0x93c:
|
||||
return -1;
|
||||
case 0x94d:
|
||||
case 0x93e ... 0x94f:
|
||||
return -1;
|
||||
case 0x951 ... 0x954:
|
||||
case 0x951 ... 0x957:
|
||||
return -1;
|
||||
case 0x962 ... 0x963:
|
||||
return -1;
|
||||
case 0x981 ... 0x983:
|
||||
return -1;
|
||||
case 0x9bc:
|
||||
return -1;
|
||||
case 0x9cd:
|
||||
case 0x9be ... 0x9c4:
|
||||
return -1;
|
||||
case 0x9c7 ... 0x9c8:
|
||||
return -1;
|
||||
case 0x9cb ... 0x9cd:
|
||||
return -1;
|
||||
case 0x9d7:
|
||||
return -1;
|
||||
case 0x9e2 ... 0x9e3:
|
||||
return -1;
|
||||
case 0xa01 ... 0xa03:
|
||||
return -1;
|
||||
case 0xa3c:
|
||||
return -1;
|
||||
case 0xa4d:
|
||||
case 0xa3e ... 0xa42:
|
||||
return -1;
|
||||
case 0xa47 ... 0xa48:
|
||||
return -1;
|
||||
case 0xa4b ... 0xa4d:
|
||||
return -1;
|
||||
case 0xa51:
|
||||
return -1;
|
||||
case 0xa70 ... 0xa71:
|
||||
return -1;
|
||||
case 0xa75:
|
||||
return -1;
|
||||
case 0xa81 ... 0xa83:
|
||||
return -1;
|
||||
case 0xabc:
|
||||
return -1;
|
||||
case 0xacd:
|
||||
case 0xabe ... 0xac5:
|
||||
return -1;
|
||||
case 0xac7 ... 0xac9:
|
||||
return -1;
|
||||
case 0xacb ... 0xacd:
|
||||
return -1;
|
||||
case 0xae2 ... 0xae3:
|
||||
return -1;
|
||||
case 0xafa ... 0xaff:
|
||||
return -1;
|
||||
case 0xb01 ... 0xb03:
|
||||
return -1;
|
||||
case 0xb3c:
|
||||
return -1;
|
||||
case 0xb4d:
|
||||
case 0xb3e ... 0xb44:
|
||||
return -1;
|
||||
case 0xbcd:
|
||||
case 0xb47 ... 0xb48:
|
||||
return -1;
|
||||
case 0xc4d:
|
||||
case 0xb4b ... 0xb4d:
|
||||
return -1;
|
||||
case 0xb56 ... 0xb57:
|
||||
return -1;
|
||||
case 0xb62 ... 0xb63:
|
||||
return -1;
|
||||
case 0xb82:
|
||||
return -1;
|
||||
case 0xbbe ... 0xbc2:
|
||||
return -1;
|
||||
case 0xbc6 ... 0xbc8:
|
||||
return -1;
|
||||
case 0xbca ... 0xbcd:
|
||||
return -1;
|
||||
case 0xbd7:
|
||||
return -1;
|
||||
case 0xc00 ... 0xc03:
|
||||
return -1;
|
||||
case 0xc3e ... 0xc44:
|
||||
return -1;
|
||||
case 0xc46 ... 0xc48:
|
||||
return -1;
|
||||
case 0xc4a ... 0xc4d:
|
||||
return -1;
|
||||
case 0xc55 ... 0xc56:
|
||||
return -1;
|
||||
case 0xc62 ... 0xc63:
|
||||
return -1;
|
||||
case 0xc81 ... 0xc83:
|
||||
return -1;
|
||||
case 0xcbc:
|
||||
return -1;
|
||||
case 0xccd:
|
||||
case 0xcbe ... 0xcc4:
|
||||
return -1;
|
||||
case 0xcc6 ... 0xcc8:
|
||||
return -1;
|
||||
case 0xcca ... 0xccd:
|
||||
return -1;
|
||||
case 0xcd5 ... 0xcd6:
|
||||
return -1;
|
||||
case 0xce2 ... 0xce3:
|
||||
return -1;
|
||||
case 0xd00 ... 0xd03:
|
||||
return -1;
|
||||
case 0xd3b ... 0xd3c:
|
||||
return -1;
|
||||
case 0xd4d:
|
||||
case 0xd3e ... 0xd44:
|
||||
return -1;
|
||||
case 0xd46 ... 0xd48:
|
||||
return -1;
|
||||
case 0xd4a ... 0xd4d:
|
||||
return -1;
|
||||
case 0xd57:
|
||||
return -1;
|
||||
case 0xd62 ... 0xd63:
|
||||
return -1;
|
||||
case 0xd82 ... 0xd83:
|
||||
return -1;
|
||||
case 0xdca:
|
||||
return -1;
|
||||
case 0xe38 ... 0xe3a:
|
||||
case 0xdcf ... 0xdd4:
|
||||
return -1;
|
||||
case 0xe48 ... 0xe4b:
|
||||
case 0xdd6:
|
||||
return -1;
|
||||
case 0xeb8 ... 0xeb9:
|
||||
case 0xdd8 ... 0xddf:
|
||||
return -1;
|
||||
case 0xec8 ... 0xecb:
|
||||
case 0xdf2 ... 0xdf3:
|
||||
return -1;
|
||||
case 0xe31:
|
||||
return -1;
|
||||
case 0xe34 ... 0xe3a:
|
||||
return -1;
|
||||
case 0xe47 ... 0xe4e:
|
||||
return -1;
|
||||
case 0xeb1:
|
||||
return -1;
|
||||
case 0xeb4 ... 0xeb9:
|
||||
return -1;
|
||||
case 0xebb ... 0xebc:
|
||||
return -1;
|
||||
case 0xec8 ... 0xecd:
|
||||
return -1;
|
||||
case 0xf18 ... 0xf19:
|
||||
return -1;
|
||||
@@ -158,85 +254,99 @@ wcwidth_std(int32_t code) {
|
||||
return -1;
|
||||
case 0xf39:
|
||||
return -1;
|
||||
case 0xf71 ... 0xf72:
|
||||
case 0xf3e ... 0xf3f:
|
||||
return -1;
|
||||
case 0xf74:
|
||||
return -1;
|
||||
case 0xf7a ... 0xf7d:
|
||||
return -1;
|
||||
case 0xf80:
|
||||
return -1;
|
||||
case 0xf82 ... 0xf84:
|
||||
case 0xf71 ... 0xf84:
|
||||
return -1;
|
||||
case 0xf86 ... 0xf87:
|
||||
return -1;
|
||||
case 0xf8d ... 0xf97:
|
||||
return -1;
|
||||
case 0xf99 ... 0xfbc:
|
||||
return -1;
|
||||
case 0xfc6:
|
||||
return -1;
|
||||
case 0x1037:
|
||||
case 0x102b ... 0x103e:
|
||||
return -1;
|
||||
case 0x1039 ... 0x103a:
|
||||
case 0x1056 ... 0x1059:
|
||||
return -1;
|
||||
case 0x108d:
|
||||
case 0x105e ... 0x1060:
|
||||
return -1;
|
||||
case 0x1062 ... 0x1064:
|
||||
return -1;
|
||||
case 0x1067 ... 0x106d:
|
||||
return -1;
|
||||
case 0x1071 ... 0x1074:
|
||||
return -1;
|
||||
case 0x1082 ... 0x108d:
|
||||
return -1;
|
||||
case 0x108f:
|
||||
return -1;
|
||||
case 0x109a ... 0x109d:
|
||||
return -1;
|
||||
case 0x135d ... 0x135f:
|
||||
return -1;
|
||||
case 0x1714:
|
||||
case 0x1712 ... 0x1714:
|
||||
return -1;
|
||||
case 0x1734:
|
||||
case 0x1732 ... 0x1734:
|
||||
return -1;
|
||||
case 0x17d2:
|
||||
case 0x1752 ... 0x1753:
|
||||
return -1;
|
||||
case 0x1772 ... 0x1773:
|
||||
return -1;
|
||||
case 0x17b4 ... 0x17d3:
|
||||
return -1;
|
||||
case 0x17dd:
|
||||
return -1;
|
||||
case 0x180b ... 0x180d:
|
||||
return -1;
|
||||
case 0x1885 ... 0x1886:
|
||||
return -1;
|
||||
case 0x18a9:
|
||||
return -1;
|
||||
case 0x1939 ... 0x193b:
|
||||
case 0x1920 ... 0x192b:
|
||||
return -1;
|
||||
case 0x1a17 ... 0x1a18:
|
||||
case 0x1930 ... 0x193b:
|
||||
return -1;
|
||||
case 0x1a60:
|
||||
case 0x1a17 ... 0x1a1b:
|
||||
return -1;
|
||||
case 0x1a75 ... 0x1a7c:
|
||||
case 0x1a55 ... 0x1a5e:
|
||||
return -1;
|
||||
case 0x1a60 ... 0x1a7c:
|
||||
return -1;
|
||||
case 0x1a7f:
|
||||
return -1;
|
||||
case 0x1ab0 ... 0x1abd:
|
||||
case 0x1ab0 ... 0x1abe:
|
||||
return -1;
|
||||
case 0x1b34:
|
||||
case 0x1b00 ... 0x1b04:
|
||||
return -1;
|
||||
case 0x1b44:
|
||||
case 0x1b34 ... 0x1b44:
|
||||
return -1;
|
||||
case 0x1b6b ... 0x1b73:
|
||||
return -1;
|
||||
case 0x1baa ... 0x1bab:
|
||||
case 0x1b80 ... 0x1b82:
|
||||
return -1;
|
||||
case 0x1be6:
|
||||
case 0x1ba1 ... 0x1bad:
|
||||
return -1;
|
||||
case 0x1bf2 ... 0x1bf3:
|
||||
case 0x1be6 ... 0x1bf3:
|
||||
return -1;
|
||||
case 0x1c37:
|
||||
case 0x1c24 ... 0x1c37:
|
||||
return -1;
|
||||
case 0x1cd0 ... 0x1cd2:
|
||||
return -1;
|
||||
case 0x1cd4 ... 0x1ce0:
|
||||
return -1;
|
||||
case 0x1ce2 ... 0x1ce8:
|
||||
case 0x1cd4 ... 0x1ce8:
|
||||
return -1;
|
||||
case 0x1ced:
|
||||
return -1;
|
||||
case 0x1cf4:
|
||||
case 0x1cf2 ... 0x1cf4:
|
||||
return -1;
|
||||
case 0x1cf8 ... 0x1cf9:
|
||||
case 0x1cf7 ... 0x1cf9:
|
||||
return -1;
|
||||
case 0x1dc0 ... 0x1df9:
|
||||
return -1;
|
||||
case 0x1dfb ... 0x1dff:
|
||||
return -1;
|
||||
case 0x20d0 ... 0x20dc:
|
||||
return -1;
|
||||
case 0x20e1:
|
||||
return -1;
|
||||
case 0x20e5 ... 0x20f0:
|
||||
case 0x20d0 ... 0x20f0:
|
||||
return -1;
|
||||
case 0x2cef ... 0x2cf1:
|
||||
return -1;
|
||||
@@ -248,7 +358,7 @@ wcwidth_std(int32_t code) {
|
||||
return -1;
|
||||
case 0x3099 ... 0x309a:
|
||||
return -1;
|
||||
case 0xa66f:
|
||||
case 0xa66f ... 0xa672:
|
||||
return -1;
|
||||
case 0xa674 ... 0xa67d:
|
||||
return -1;
|
||||
@@ -256,19 +366,37 @@ wcwidth_std(int32_t code) {
|
||||
return -1;
|
||||
case 0xa6f0 ... 0xa6f1:
|
||||
return -1;
|
||||
case 0xa802:
|
||||
return -1;
|
||||
case 0xa806:
|
||||
return -1;
|
||||
case 0xa8c4:
|
||||
case 0xa80b:
|
||||
return -1;
|
||||
case 0xa823 ... 0xa827:
|
||||
return -1;
|
||||
case 0xa880 ... 0xa881:
|
||||
return -1;
|
||||
case 0xa8b4 ... 0xa8c5:
|
||||
return -1;
|
||||
case 0xa8e0 ... 0xa8f1:
|
||||
return -1;
|
||||
case 0xa92b ... 0xa92d:
|
||||
case 0xa926 ... 0xa92d:
|
||||
return -1;
|
||||
case 0xa953:
|
||||
case 0xa947 ... 0xa953:
|
||||
return -1;
|
||||
case 0xa9b3:
|
||||
case 0xa980 ... 0xa983:
|
||||
return -1;
|
||||
case 0xa9c0:
|
||||
case 0xa9b3 ... 0xa9c0:
|
||||
return -1;
|
||||
case 0xa9e5:
|
||||
return -1;
|
||||
case 0xaa29 ... 0xaa36:
|
||||
return -1;
|
||||
case 0xaa43:
|
||||
return -1;
|
||||
case 0xaa4c ... 0xaa4d:
|
||||
return -1;
|
||||
case 0xaa7b ... 0xaa7d:
|
||||
return -1;
|
||||
case 0xaab0:
|
||||
return -1;
|
||||
@@ -280,12 +408,18 @@ wcwidth_std(int32_t code) {
|
||||
return -1;
|
||||
case 0xaac1:
|
||||
return -1;
|
||||
case 0xaaf6:
|
||||
case 0xaaeb ... 0xaaef:
|
||||
return -1;
|
||||
case 0xabed:
|
||||
case 0xaaf5 ... 0xaaf6:
|
||||
return -1;
|
||||
case 0xabe3 ... 0xabea:
|
||||
return -1;
|
||||
case 0xabec ... 0xabed:
|
||||
return -1;
|
||||
case 0xfb1e:
|
||||
return -1;
|
||||
case 0xfe00 ... 0xfe0f:
|
||||
return -1;
|
||||
case 0xfe20 ... 0xfe2f:
|
||||
return -1;
|
||||
case 0x101fd:
|
||||
@@ -294,9 +428,11 @@ wcwidth_std(int32_t code) {
|
||||
return -1;
|
||||
case 0x10376 ... 0x1037a:
|
||||
return -1;
|
||||
case 0x10a0d:
|
||||
case 0x10a01 ... 0x10a03:
|
||||
return -1;
|
||||
case 0x10a0f:
|
||||
case 0x10a05 ... 0x10a06:
|
||||
return -1;
|
||||
case 0x10a0c ... 0x10a0f:
|
||||
return -1;
|
||||
case 0x10a38 ... 0x10a3a:
|
||||
return -1;
|
||||
@@ -304,65 +440,105 @@ wcwidth_std(int32_t code) {
|
||||
return -1;
|
||||
case 0x10ae5 ... 0x10ae6:
|
||||
return -1;
|
||||
case 0x11046:
|
||||
case 0x11000 ... 0x11002:
|
||||
return -1;
|
||||
case 0x1107f:
|
||||
case 0x11038 ... 0x11046:
|
||||
return -1;
|
||||
case 0x110b9 ... 0x110ba:
|
||||
case 0x1107f ... 0x11082:
|
||||
return -1;
|
||||
case 0x110b0 ... 0x110ba:
|
||||
return -1;
|
||||
case 0x11100 ... 0x11102:
|
||||
return -1;
|
||||
case 0x11133 ... 0x11134:
|
||||
case 0x11127 ... 0x11134:
|
||||
return -1;
|
||||
case 0x11173:
|
||||
return -1;
|
||||
case 0x111c0:
|
||||
case 0x11180 ... 0x11182:
|
||||
return -1;
|
||||
case 0x111ca:
|
||||
case 0x111b3 ... 0x111c0:
|
||||
return -1;
|
||||
case 0x11235 ... 0x11236:
|
||||
case 0x111ca ... 0x111cc:
|
||||
return -1;
|
||||
case 0x112e9 ... 0x112ea:
|
||||
case 0x1122c ... 0x11237:
|
||||
return -1;
|
||||
case 0x1123e:
|
||||
return -1;
|
||||
case 0x112df ... 0x112ea:
|
||||
return -1;
|
||||
case 0x11300 ... 0x11303:
|
||||
return -1;
|
||||
case 0x1133c:
|
||||
return -1;
|
||||
case 0x1134d:
|
||||
case 0x1133e ... 0x11344:
|
||||
return -1;
|
||||
case 0x11347 ... 0x11348:
|
||||
return -1;
|
||||
case 0x1134b ... 0x1134d:
|
||||
return -1;
|
||||
case 0x11357:
|
||||
return -1;
|
||||
case 0x11362 ... 0x11363:
|
||||
return -1;
|
||||
case 0x11366 ... 0x1136c:
|
||||
return -1;
|
||||
case 0x11370 ... 0x11374:
|
||||
return -1;
|
||||
case 0x11442:
|
||||
case 0x11435 ... 0x11446:
|
||||
return -1;
|
||||
case 0x11446:
|
||||
case 0x114b0 ... 0x114c3:
|
||||
return -1;
|
||||
case 0x114c2 ... 0x114c3:
|
||||
case 0x115af ... 0x115b5:
|
||||
return -1;
|
||||
case 0x115bf ... 0x115c0:
|
||||
case 0x115b8 ... 0x115c0:
|
||||
return -1;
|
||||
case 0x1163f:
|
||||
case 0x115dc ... 0x115dd:
|
||||
return -1;
|
||||
case 0x116b6 ... 0x116b7:
|
||||
case 0x11630 ... 0x11640:
|
||||
return -1;
|
||||
case 0x1172b:
|
||||
case 0x116ab ... 0x116b7:
|
||||
return -1;
|
||||
case 0x11a34:
|
||||
case 0x1171d ... 0x1172b:
|
||||
return -1;
|
||||
case 0x11a01 ... 0x11a0a:
|
||||
return -1;
|
||||
case 0x11a33 ... 0x11a39:
|
||||
return -1;
|
||||
case 0x11a3b ... 0x11a3e:
|
||||
return -1;
|
||||
case 0x11a47:
|
||||
return -1;
|
||||
case 0x11a99:
|
||||
case 0x11a51 ... 0x11a5b:
|
||||
return -1;
|
||||
case 0x11c3f:
|
||||
case 0x11a8a ... 0x11a99:
|
||||
return -1;
|
||||
case 0x11d42:
|
||||
case 0x11c2f ... 0x11c36:
|
||||
return -1;
|
||||
case 0x11d44 ... 0x11d45:
|
||||
case 0x11c38 ... 0x11c3f:
|
||||
return -1;
|
||||
case 0x11c92 ... 0x11ca7:
|
||||
return -1;
|
||||
case 0x11ca9 ... 0x11cb6:
|
||||
return -1;
|
||||
case 0x11d31 ... 0x11d36:
|
||||
return -1;
|
||||
case 0x11d3a:
|
||||
return -1;
|
||||
case 0x11d3c ... 0x11d3d:
|
||||
return -1;
|
||||
case 0x11d3f ... 0x11d45:
|
||||
return -1;
|
||||
case 0x11d47:
|
||||
return -1;
|
||||
case 0x16af0 ... 0x16af4:
|
||||
return -1;
|
||||
case 0x16b30 ... 0x16b36:
|
||||
return -1;
|
||||
case 0x1bc9e:
|
||||
case 0x16f51 ... 0x16f7e:
|
||||
return -1;
|
||||
case 0x16f8f ... 0x16f92:
|
||||
return -1;
|
||||
case 0x1bc9d ... 0x1bc9e:
|
||||
return -1;
|
||||
case 0x1d165 ... 0x1d169:
|
||||
return -1;
|
||||
@@ -376,6 +552,18 @@ wcwidth_std(int32_t code) {
|
||||
return -1;
|
||||
case 0x1d242 ... 0x1d244:
|
||||
return -1;
|
||||
case 0x1da00 ... 0x1da36:
|
||||
return -1;
|
||||
case 0x1da3b ... 0x1da6c:
|
||||
return -1;
|
||||
case 0x1da75:
|
||||
return -1;
|
||||
case 0x1da84:
|
||||
return -1;
|
||||
case 0x1da9b ... 0x1da9f:
|
||||
return -1;
|
||||
case 0x1daa1 ... 0x1daaf:
|
||||
return -1;
|
||||
case 0x1e000 ... 0x1e006:
|
||||
return -1;
|
||||
case 0x1e008 ... 0x1e018:
|
||||
@@ -390,6 +578,8 @@ wcwidth_std(int32_t code) {
|
||||
return -1;
|
||||
case 0x1e944 ... 0x1e94a:
|
||||
return -1;
|
||||
case 0xe0100 ... 0xe01ef:
|
||||
return -1;
|
||||
// }}}
|
||||
|
||||
// Private use (137468 codepoints) {{{
|
||||
@@ -401,7 +591,7 @@ wcwidth_std(int32_t code) {
|
||||
return -3;
|
||||
// }}}
|
||||
|
||||
// East Asian ambiguous width (1159 codepoints) {{{
|
||||
// East Asian ambiguous width (902 codepoints) {{{
|
||||
case 0xa1:
|
||||
return -2;
|
||||
case 0xa4:
|
||||
@@ -508,8 +698,6 @@ wcwidth_std(int32_t code) {
|
||||
return -2;
|
||||
case 0x2df:
|
||||
return -2;
|
||||
case 0x34f:
|
||||
return -2;
|
||||
case 0x391 ... 0x3a1:
|
||||
return -2;
|
||||
case 0x3a3 ... 0x3a9:
|
||||
@@ -736,8 +924,6 @@ wcwidth_std(int32_t code) {
|
||||
return -2;
|
||||
case 0x3248 ... 0x324f:
|
||||
return -2;
|
||||
case 0xfe00 ... 0xfe0f:
|
||||
return -2;
|
||||
case 0xfffd:
|
||||
return -2;
|
||||
case 0x1f100 ... 0x1f10a:
|
||||
@@ -752,8 +938,6 @@ wcwidth_std(int32_t code) {
|
||||
return -2;
|
||||
case 0x1f19b ... 0x1f1ac:
|
||||
return -2;
|
||||
case 0xe0100 ... 0xe01ef:
|
||||
return -2;
|
||||
// }}}
|
||||
|
||||
// East Asian double width (181730 codepoints) {{{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import json
|
||||
import sys
|
||||
import weakref
|
||||
from collections import deque
|
||||
@@ -87,7 +88,7 @@ class Window:
|
||||
|
||||
def __init__(self, tab, child, opts, args, override_title=None):
|
||||
self.override_title = override_title
|
||||
self.title = self.override_title or appname
|
||||
self.child_title = appname
|
||||
self.id = add_window(tab.os_window_id, tab.id, self.title)
|
||||
if not self.id:
|
||||
raise Exception('No tab with id: {} in OS Window: {} was found, or the window counter wrapped'.format(tab.id, tab.os_window_id))
|
||||
@@ -103,9 +104,37 @@ class Window:
|
||||
self.screen = Screen(self, 24, 80, opts.scrollback_lines, self.id)
|
||||
setup_colors(self.screen, opts)
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self.override_title or self.child_title
|
||||
|
||||
def __repr__(self):
|
||||
return 'Window(title={}, id={})'.format(self.title, self.id)
|
||||
|
||||
def as_dict(self):
|
||||
return dict(
|
||||
id=self.id,
|
||||
title=self.override_title or self.title,
|
||||
pid=self.child.pid,
|
||||
cwd=self.child.current_cwd or self.child.cwd, cmdline=self.child.cmdline
|
||||
)
|
||||
|
||||
def matches(self, field, pat):
|
||||
if field == 'id':
|
||||
return pat.pattern == str(self.id)
|
||||
if field == 'pid':
|
||||
return pat.pattern == str(self.child.pid)
|
||||
if field == 'title':
|
||||
return pat.search(self.override_title or self.title) is not None
|
||||
if field in 'cwd':
|
||||
return pat.search(self.child.current_cwd or self.child.cwd) is not None
|
||||
if field == 'cmdline':
|
||||
for x in self.child.cmdline:
|
||||
if pat.search(x) is not None:
|
||||
return True
|
||||
return False
|
||||
return False
|
||||
|
||||
def set_visible_in_layout(self, window_idx, val):
|
||||
val = bool(val)
|
||||
if val is not self.is_visible_in_layout:
|
||||
@@ -119,7 +148,7 @@ class Window:
|
||||
wakeup()
|
||||
|
||||
def update_position(self, window_geometry):
|
||||
vw, vh, ah, cw, ch = viewport_for_window(self.os_window_id)
|
||||
central, tab_bar, vw, vh, cw, ch = viewport_for_window(self.os_window_id)
|
||||
self.screen_geometry = sg = calculate_gl_geometry(window_geometry, vw, vh, cw, ch)
|
||||
return sg
|
||||
|
||||
@@ -163,6 +192,17 @@ class Window:
|
||||
if get_boss().child_monitor.needs_write(self.id, data) is not True:
|
||||
print('Failed to write to child %d as it does not exist' % self.id, file=sys.stderr)
|
||||
|
||||
def title_updated(self):
|
||||
update_window_title(self.os_window_id, self.tab_id, self.id, self.title)
|
||||
t = self.tabref()
|
||||
if t is not None:
|
||||
t.title_changed(self)
|
||||
glfw_post_empty_event()
|
||||
|
||||
def set_title(self, title):
|
||||
self.override_title = title or None
|
||||
self.title_updated()
|
||||
|
||||
# screen callbacks {{{
|
||||
def use_utf8(self, on):
|
||||
get_boss().child_monitor.set_iutf8(self.window_id, on)
|
||||
@@ -176,13 +216,9 @@ class Window:
|
||||
self.screen.send_escape_code_to_child(CSI, 'O')
|
||||
|
||||
def title_changed(self, new_title):
|
||||
self.child_title = sanitize_title(new_title or appname)
|
||||
if self.override_title is None:
|
||||
self.title = sanitize_title(new_title or appname)
|
||||
update_window_title(self.os_window_id, self.tab_id, self.id, self.title)
|
||||
t = self.tabref()
|
||||
if t is not None:
|
||||
t.title_changed(self)
|
||||
glfw_post_empty_event()
|
||||
self.title_updated()
|
||||
|
||||
def icon_changed(self, new_icon):
|
||||
pass # TODO: Implement this
|
||||
@@ -258,6 +294,12 @@ class Window:
|
||||
def request_capabilities(self, q):
|
||||
self.screen.send_escape_code_to_child(DCS, get_capabilities(q))
|
||||
|
||||
def handle_remote_cmd(self, cmd):
|
||||
get_boss().handle_remote_cmd(cmd, self)
|
||||
|
||||
def send_cmd_response(self, response):
|
||||
self.screen.send_escape_code_to_child(DCS, '@kitty-cmd' + json.dumps(response))
|
||||
|
||||
# }}}
|
||||
|
||||
def text_for_selection(self):
|
||||
@@ -270,14 +312,20 @@ class Window:
|
||||
self.screen.reset_callbacks()
|
||||
self.screen = None
|
||||
|
||||
def buffer_as_ansi(self):
|
||||
def buffer_as_ansi(self, add_history=True):
|
||||
data = []
|
||||
self.screen.historybuf.as_ansi(data.append)
|
||||
if add_history:
|
||||
self.screen.historybuf.as_ansi(data.append)
|
||||
self.screen.linebuf.as_ansi(data.append)
|
||||
return ''.join(data)
|
||||
|
||||
def buffer_as_text(self):
|
||||
return str(self.screen.historybuf) + '\n' + str(self.screen.linebuf)
|
||||
def buffer_as_text(self, add_history=True):
|
||||
ans = str(self.screen.linebuf).rstrip('\n')
|
||||
if add_history:
|
||||
h = str(self.screen.historybuf)
|
||||
if h.strip():
|
||||
ans = h + '\n' + ans
|
||||
return ans
|
||||
|
||||
# actions {{{
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ class TestDataTypes(BaseTest):
|
||||
|
||||
lb = filled_line_buf(5, 5, filled_cursor())
|
||||
l0 = lb.line(0)
|
||||
l0.add_combining_char(1, 'a')
|
||||
l0.add_combining_char(1, '\u0300')
|
||||
l0.clear_text(1, 2)
|
||||
self.ae(str(l0), '0 00')
|
||||
self.assertEqualAttributes(l0.cursor_from(1), l0.cursor_from(0))
|
||||
@@ -164,14 +164,14 @@ class TestDataTypes(BaseTest):
|
||||
lb.line(0)[lb.xnum]
|
||||
l0 = lb.line(0)
|
||||
l0.set_text(' ', 0, len(' '), C())
|
||||
l0.add_combining_char(0, '1')
|
||||
self.ae(l0[0], ' 1')
|
||||
l0.add_combining_char(0, '2')
|
||||
self.ae(l0[0], ' 12')
|
||||
l0.add_combining_char(0, '3')
|
||||
self.ae(l0[0], ' 13')
|
||||
l0.add_combining_char(0, '\u0300')
|
||||
self.ae(l0[0], ' \u0300')
|
||||
l0.add_combining_char(0, '\U000e0100')
|
||||
self.ae(l0[0], ' \u0300\U000e0100')
|
||||
l0.add_combining_char(0, '\u0302')
|
||||
self.ae(l0[0], ' \u0300\u0302')
|
||||
self.ae(l0[1], '\0')
|
||||
self.ae(str(l0), ' 13')
|
||||
self.ae(str(l0), ' \u0300\u0302')
|
||||
t = 'Testing with simple text'
|
||||
lb = LineBuf(2, len(t))
|
||||
l0 = lb.line(0)
|
||||
|
||||
@@ -80,6 +80,7 @@ class Rendering(BaseTest):
|
||||
|
||||
self.ae(groups('abcd'), [(1, 1) for i in range(4)])
|
||||
self.ae(groups('A=>>B!=C', path='kitty_tests/FiraCode-Medium.otf'), [(1, 1), (3, 3), (1, 1), (2, 2), (1, 1)])
|
||||
self.ae(groups('==!=<>==<><><>', path='kitty_tests/FiraCode-Medium.otf'), [(2, 2), (2, 2), (2, 2), (2, 2), (2, 2), (2, 2), (2, 2)])
|
||||
colon_glyph = shape_string('9:30', path='kitty_tests/FiraCode-Medium.otf')[1][2]
|
||||
self.assertNotEqual(colon_glyph, shape_string(':', path='kitty_tests/FiraCode-Medium.otf')[0][2])
|
||||
self.ae(colon_glyph, 998)
|
||||
|
||||
@@ -78,3 +78,30 @@ class TestParser(BaseTest):
|
||||
keycode = getattr(defines, 'GLFW_KEY_' + key)
|
||||
base_key = smkx_key_map[keycode]
|
||||
km(modify_key_bytes(base_key, num).decode('ascii')[1:], key)
|
||||
|
||||
def test_encode_mouse_event(self):
|
||||
PRESS, RELEASE, DRAG, MOVE = range(4)
|
||||
NORMAL_PROTOCOL, UTF8_PROTOCOL, SGR_PROTOCOL, URXVT_PROTOCOL = range(4)
|
||||
L, M, R = defines.GLFW_MOUSE_BUTTON_LEFT, defines.GLFW_MOUSE_BUTTON_MIDDLE, defines.GLFW_MOUSE_BUTTON_RIGHT
|
||||
protocol = SGR_PROTOCOL
|
||||
|
||||
def enc(button=L, action=PRESS, mods=0, x=1, y=1):
|
||||
return defines.test_encode_mouse(x, y, protocol, button, action, mods)
|
||||
|
||||
self.ae(enc(), '<0;1;1M')
|
||||
self.ae(enc(action=RELEASE), '<0;1;1m')
|
||||
self.ae(enc(action=MOVE), '<35;1;1M')
|
||||
self.ae(enc(action=DRAG), '<32;1;1M')
|
||||
|
||||
self.ae(enc(R), '<2;1;1M')
|
||||
self.ae(enc(R, action=RELEASE), '<2;1;1m')
|
||||
self.ae(enc(R, action=DRAG), '<34;1;1M')
|
||||
|
||||
self.ae(enc(M), '<1;1;1M')
|
||||
self.ae(enc(M, action=RELEASE), '<1;1;1m')
|
||||
self.ae(enc(M, action=DRAG), '<33;1;1M')
|
||||
|
||||
self.ae(enc(x=1234, y=5678), '<0;1234;5678M')
|
||||
self.ae(enc(mods=defines.GLFW_MOD_SHIFT), '<4;1;1M')
|
||||
self.ae(enc(mods=defines.GLFW_MOD_ALT), '<8;1;1M')
|
||||
self.ae(enc(mods=defines.GLFW_MOD_CONTROL), '<16;1;1M')
|
||||
|
||||
@@ -367,3 +367,16 @@ class TestScreen(BaseTest):
|
||||
s.cursor.y = 2
|
||||
s.erase_characters(2)
|
||||
self.ae(s.linebuf.dirty_lines(), [0, 1, 2])
|
||||
|
||||
def test_selection_as_text(self):
|
||||
s = self.create_screen()
|
||||
for i in range(2 * s.lines):
|
||||
if i != 0:
|
||||
s.carriage_return(), s.linefeed()
|
||||
s.draw(str(i) * s.columns)
|
||||
s.start_selection(0, 0, False)
|
||||
s.update_selection(4, 4, True)
|
||||
expected = ('55555', '\n66666', '\n77777', '\n88888', '\n99999')
|
||||
self.ae(s.text_for_selection(), expected)
|
||||
s.scroll(2, True)
|
||||
self.ae(s.text_for_selection(), expected)
|
||||
|
||||
13
launcher/kitty
Executable file
13
launcher/kitty
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, base)
|
||||
with open(os.path.join(base, '__main__.py')) as f:
|
||||
src = f.read()
|
||||
code = compile(src, f.name, 'exec')
|
||||
exec(code, {'__name__': '__main__'})
|
||||
@@ -47,15 +47,17 @@ static int run_embedded(const char* exe_dir_, int argc, wchar_t **argv) {
|
||||
if (num < 0 || num >= PATH_MAX) { fprintf(stderr, "Failed to create path to kitty lib\n"); return 1; }
|
||||
Py_Initialize();
|
||||
PySys_SetArgvEx(argc - 1, argv + 1, 0);
|
||||
PySys_SetObject("frozen", Py_True); // dont care if this fails
|
||||
PySys_SetObject("frozen", Py_True); // dont care if this fails
|
||||
if (ed) { PySys_SetObject("bundle_exe_dir", ed); Py_CLEAR(ed); }
|
||||
PyObject *kitty = PyUnicode_FromWideChar(stdlib, -1);
|
||||
if (kitty == NULL) { fprintf(stderr, "Failed to allocate python kitty lib object\n"); goto end; }
|
||||
PyObject *runpy = PyImport_ImportModule("runpy");
|
||||
if (runpy == NULL) { PyErr_Print(); fprintf(stderr, "Unable to import runpy\n"); Py_CLEAR(kitty); goto end; }
|
||||
PyObject *res = PyObject_CallMethod(runpy, "run_path", "O", kitty);
|
||||
Py_CLEAR(runpy); Py_CLEAR(kitty);
|
||||
if (res == NULL) PyErr_Print();
|
||||
PyObject *run_name = PyUnicode_FromString("__main__");
|
||||
if (run_name == NULL) { fprintf(stderr, "Failed to allocate run_name\n"); goto end; }
|
||||
PyObject *res = PyObject_CallMethod(runpy, "run_path", "OOO", kitty, Py_None, run_name);
|
||||
Py_CLEAR(runpy); Py_CLEAR(kitty); Py_CLEAR(run_name);
|
||||
if (res == NULL) PyErr_Print();
|
||||
else { ret = 0; Py_CLEAR(res); }
|
||||
end:
|
||||
if (Py_FinalizeEx() < 0) ret = 120;
|
||||
@@ -88,8 +90,12 @@ int main(int argc, char *argv[]) {
|
||||
wchar_t *argvw[MAX_ARGC + 1] = {0};
|
||||
#ifdef WITH_PROFILER
|
||||
num = snprintf(lib, PATH_MAX, "%s%s", exe_dir, "/");
|
||||
#else
|
||||
#ifdef FOR_LAUNCHER
|
||||
num = snprintf(lib, PATH_MAX, "%s%s", exe_dir, "/../Frameworks/kitty");
|
||||
#else
|
||||
num = snprintf(lib, PATH_MAX, "%s%s", exe_dir, "/../lib/kitty");
|
||||
#endif
|
||||
#endif
|
||||
|
||||
if (num < 0 || num >= PATH_MAX) { fprintf(stderr, "Failed to create path to kitty lib\n"); return 1; }
|
||||
|
||||
@@ -23,7 +23,7 @@ if os.path.exists(iconset):
|
||||
shutil.rmtree(iconset)
|
||||
os.mkdir(iconset)
|
||||
os.chdir(iconset)
|
||||
for sz in (16, 32, 128, 256, 512, 1024):
|
||||
for sz in (16, 32, 64, 128, 256, 512, 1024):
|
||||
iname = 'icon_{0}x{0}.png'.format(sz)
|
||||
iname2x = 'icon_{0}x{0}@2x.png'.format(sz // 2)
|
||||
render(iname, sz)
|
||||
|
||||
77
remote-control.asciidoc
Normal file
77
remote-control.asciidoc
Normal file
@@ -0,0 +1,77 @@
|
||||
= Controlling kitty from scripts or the shell
|
||||
|
||||
kitty can be controlled from scripts or the shell prompt. You can open new
|
||||
windows, send arbitrary text input to any window, name windows and tabs, etc.
|
||||
Let's walk through a few examples of controlling kitty.
|
||||
|
||||
Start by running kitty as:
|
||||
|
||||
kitty -o allow_remote_control=yes --window-layout tall
|
||||
|
||||
In order for control to work, `allow_remote_control` must be enabled in
|
||||
kitty.conf. Here we turn it on explicitly at the command line.
|
||||
|
||||
Now, in the new kitty window, enter the command:
|
||||
|
||||
kitty @ new-window --title Output --keep-focus cat
|
||||
|
||||
This will open a new window, running the ``cat`` program that will appear next
|
||||
to the current window.
|
||||
|
||||
Let's send some text to this new window:
|
||||
|
||||
kitty @ send-text --match cmdline:cat Hello, World
|
||||
|
||||
This will make `Hello, World` show up in the window running the `cat` program.
|
||||
The `--match` option is very powerful, it allows selecting windows by their
|
||||
titles, the command line of the program running in the window, the working
|
||||
directory of the program running in the window, etc. See `kitty @ send-text
|
||||
--help` for details.
|
||||
|
||||
More usefully, you can pipe the output of a command running in one window to
|
||||
another window, for example:
|
||||
|
||||
ls | kitty @ send-text --match title:Output --stdin
|
||||
|
||||
This will show the output of ls in the output window instead of the current
|
||||
window. You can use this technique to, for example, show the output of running
|
||||
`make` in your editor in a different window. The possibilities are endless.
|
||||
|
||||
You can even have things you type show up in a different window. Run:
|
||||
|
||||
kitty @ send-text --match title:Output --stdin
|
||||
|
||||
And type some text, it will show up in the output window, instead of the current
|
||||
window. Type `Ctrl+D` when you are ready to stop.
|
||||
|
||||
Now, let's open a new tab.
|
||||
|
||||
kitty @ new-window --new-tab --tab-title "My Tab" --keep-focus bash
|
||||
|
||||
This will open a new tab running the bash shell with the title "My Tab".
|
||||
We can change the title of the tab with:
|
||||
|
||||
kitty @ set-tab-title --match title:My New Title
|
||||
|
||||
Let's change the title of the current tab:
|
||||
|
||||
kitty @ set-tab-title Master Tab
|
||||
|
||||
Now lets switch to the newly opened tab.
|
||||
|
||||
kitty @ focus-window --match title:New
|
||||
|
||||
You can get a listing of available tabs and windows, by running:
|
||||
|
||||
kitty @ ls
|
||||
|
||||
This outputs a tree of data in JSON format. The top level of the tree is all
|
||||
operating system kitty windows. Each OS window has an id and a list of tabs.
|
||||
Each tab has its own id, a title and a list of windows. Each window has an id,
|
||||
title, current working directory, process id (PID) and command-line of the
|
||||
process running in the window. You can use this information with `--match`
|
||||
to control individual windows.
|
||||
|
||||
As you can see, it is very easy to control kitty using the
|
||||
`kitty @` messaging system. This tutorial touches only the
|
||||
surface of what is possible. See `kitty @ --help` for more details.
|
||||
58
setup.py
58
setup.py
@@ -12,6 +12,7 @@ import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import sysconfig
|
||||
import time
|
||||
|
||||
base = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, os.path.join(base, 'glfw'))
|
||||
@@ -219,9 +220,7 @@ def kitty_env():
|
||||
pylib = get_python_flags(cflags)
|
||||
gl_libs = ['-framework', 'OpenGL'] if is_macos else pkg_config('gl', '--libs')
|
||||
libpng = pkg_config('libpng', '--libs')
|
||||
ans.ldpaths += pylib + font_libs + gl_libs + libpng + [
|
||||
'-lunistring'
|
||||
]
|
||||
ans.ldpaths += pylib + font_libs + gl_libs + libpng
|
||||
if is_macos:
|
||||
ans.ldpaths.extend('-framework Cocoa'.split())
|
||||
if is_travis and 'SW' in os.environ:
|
||||
@@ -455,7 +454,7 @@ def build_asan_launcher(args):
|
||||
run_tool(cmd, desc='Creating {} ...'.format(emphasis('asan-launcher')))
|
||||
|
||||
|
||||
def build_linux_launcher(args, launcher_dir='.', for_bundle=False):
|
||||
def build_linux_launcher(args, launcher_dir='.', for_bundle=False, sh_launcher=False):
|
||||
cflags = '-Wall -Werror -fpie'.split()
|
||||
libs = []
|
||||
if args.profile:
|
||||
@@ -466,6 +465,8 @@ def build_linux_launcher(args, launcher_dir='.', for_bundle=False):
|
||||
if for_bundle:
|
||||
cflags.append('-DFOR_BUNDLE')
|
||||
cflags.append('-DPYVER="{}"'.format(sysconfig.get_python_version()))
|
||||
elif sh_launcher:
|
||||
cflags.append('-DFOR_LAUNCHER')
|
||||
pylib = get_python_flags(cflags)
|
||||
exe = 'kitty-profile' if args.profile else 'kitty'
|
||||
cmd = [env.cc] + cflags + [
|
||||
@@ -475,7 +476,7 @@ def build_linux_launcher(args, launcher_dir='.', for_bundle=False):
|
||||
run_tool(cmd)
|
||||
|
||||
|
||||
def package(args, for_bundle=False): # {{{
|
||||
def package(args, for_bundle=False, sh_launcher=False): # {{{
|
||||
ddir = args.prefix
|
||||
libdir = os.path.join(ddir, 'lib', 'kitty')
|
||||
if os.path.exists(libdir):
|
||||
@@ -504,7 +505,7 @@ def package(args, for_bundle=False): # {{{
|
||||
os.chmod(path, 0o755 if f.endswith('.so') else 0o644)
|
||||
launcher_dir = os.path.join(ddir, 'bin')
|
||||
safe_makedirs(launcher_dir)
|
||||
build_linux_launcher(args, launcher_dir, for_bundle)
|
||||
build_linux_launcher(args, launcher_dir, for_bundle, sh_launcher)
|
||||
if not is_macos: # {{{ linux desktop gunk
|
||||
icdir = os.path.join(ddir, 'share', 'icons', 'hicolor', '256x256', 'apps')
|
||||
safe_makedirs(icdir)
|
||||
@@ -528,15 +529,47 @@ Categories=System;
|
||||
)
|
||||
# }}}
|
||||
|
||||
if for_bundle: # OS X bundle gunk {{{
|
||||
if for_bundle or sh_launcher: # OS X bundle gunk {{{
|
||||
import plistlib
|
||||
logo_dir = os.path.abspath(os.path.join('logo', appname + '.iconset'))
|
||||
os.chdir(ddir)
|
||||
os.mkdir('Contents')
|
||||
os.chdir('Contents')
|
||||
VERSION = '.'.join(map(str, version))
|
||||
pl = dict(
|
||||
CFBundleDevelopmentRegion='English',
|
||||
CFBundleDisplayName=appname,
|
||||
CFBundleName=appname,
|
||||
CFBundleIdentifier='net.kovidgoyal.' + appname,
|
||||
CFBundleVersion=VERSION,
|
||||
CFBundleShortVersionString=VERSION,
|
||||
CFBundlePackageType='APPL',
|
||||
CFBundleSignature='????',
|
||||
CFBundleExecutable=appname,
|
||||
LSMinimumSystemVersion='10.12.0',
|
||||
LSRequiresNativeExecution=True,
|
||||
NSAppleScriptEnabled=False,
|
||||
NSHumanReadableCopyright=time.strftime(
|
||||
'Copyright %Y, Kovid Goyal'),
|
||||
CFBundleGetInfoString='kitty, an OpenGL based terminal emulator https://github.com/kovidgoyal/kitty',
|
||||
CFBundleIconFile=appname + '.icns',
|
||||
NSHighResolutionCapable=True,
|
||||
NSSupportsAutomaticGraphicsSwitching=True,
|
||||
LSApplicationCategoryType='public.app-category.utilities',
|
||||
LSEnvironment={'KITTY_LAUNCHED_BY_LAUNCH_SERVICES': '1'},
|
||||
)
|
||||
plistlib.writePlist(pl, 'Info.plist')
|
||||
os.rename('../share', 'Resources')
|
||||
os.rename('../bin', 'MacOS')
|
||||
os.rename('../lib', 'Frameworks')
|
||||
if not os.path.exists(logo_dir):
|
||||
raise SystemExit('The kitty logo has not been generated, you need to run logo/make.py')
|
||||
subprocess.check_call([
|
||||
'iconutil', '-c', 'icns', logo_dir, '-o',
|
||||
os.path.join('Resources', os.path.basename(logo_dir).partition('.')[0] + '.icns')
|
||||
])
|
||||
# }}}
|
||||
# }}}
|
||||
# }}}
|
||||
|
||||
|
||||
def clean():
|
||||
@@ -556,7 +589,7 @@ def option_parser():
|
||||
'action',
|
||||
nargs='?',
|
||||
default='build',
|
||||
choices='build test linux-package osx-bundle clean'.split(),
|
||||
choices='build test linux-package kitty.app osx-bundle clean'.split(),
|
||||
help='Action to perform (default is build)'
|
||||
)
|
||||
p.add_argument(
|
||||
@@ -625,6 +658,13 @@ def main():
|
||||
elif args.action == 'osx-bundle':
|
||||
build(args, native_optimizations=False)
|
||||
package(args, for_bundle=True)
|
||||
elif args.action == 'kitty.app':
|
||||
args.prefix = 'kitty.app'
|
||||
if os.path.exists(args.prefix):
|
||||
shutil.rmtree(args.prefix)
|
||||
build(args)
|
||||
package(args, for_bundle=False, sh_launcher=True)
|
||||
print('kitty.app successfully built!')
|
||||
elif args.action == 'clean':
|
||||
clean()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user