Compare commits

..

67 Commits

Author SHA1 Message Date
Kovid Goyal
0923d6ecee version 0.2.1 2017-03-15 16:31:28 +05:30
Kovid Goyal
defda41861 Use rmkx codes as basis for control+special key codes 2017-03-15 16:27:17 +05:30
Kovid Goyal
66c51798b8 Fix a regression that broke Ctrl+Left and Ctrl+Down
Fixes #56
2017-03-15 16:16:27 +05:30
Kovid Goyal
ff966f667c Fix newlines being inserted at wrapping boundaries when copying. Fixes #55 2017-03-10 10:02:22 +05:30
Kovid Goyal
96be8dcb2c Remove spurious sentence 2017-03-09 09:14:17 +05:30
Kovid Goyal
892d3df6eb DRYer 2017-02-24 15:56:04 +05:30
Kovid Goyal
54e79a6901 Add a test for ARB_texture_buffer_object_rgb32
There are apparently some drivers on linux that are missing it,
see #54
2017-02-24 15:27:41 +05:30
Kovid Goyal
7d0c05e20d Add a command line option to debug OpenGL calls 2017-02-24 14:38:30 +05:30
Kovid Goyal
421ae6d289 Ensure texture buffer is bound before uploading to it 2017-02-24 14:35:02 +05:30
Kovid Goyal
3af501b715 Build with -march=native by default 2017-02-21 16:35:25 +05:30
Kovid Goyal
c73d6913da Some meta goals for the protocol extensions 2017-02-17 22:06:11 +05:30
Kovid Goyal
f7cb3e3f9e Option to disable audio bell 2017-02-14 08:10:00 +05:30
Kovid Goyal
d96c7d71a7 version 0.2.0 2017-02-13 17:31:17 +05:30
Kovid Goyal
7d8e172ce2 Keyboard maps should be respected in extended keyboard mode as well 2017-02-12 14:27:03 +05:30
Kovid Goyal
a671c7a184 When detecting URLs remove trailing "] and similar 2017-02-11 12:12:02 +05:30
Kovid Goyal
72125701f5 No need ot build the key map on every startup 2017-02-11 10:48:36 +05:30
Kovid Goyal
a66d2b0890 Ensure the extended keyboard protocol key encoding is stable
Also use base85 instead of base64 for keyname encoding to reduce average
length
2017-02-11 10:41:04 +05:30
Kovid Goyal
211b771316 pep8 2017-02-11 10:02:48 +05:30
Kovid Goyal
41ffad8e5c Ignore failures to beep 2017-02-11 09:50:19 +05:30
Kovid Goyal
91144a8b63 typo in comment 2017-02-11 08:58:51 +05:30
Kovid Goyal
0c408fa4af Add an optional "visual" bell
Fix #51
2017-02-11 08:56:40 +05:30
Kovid Goyal
8b8186660b Move bell handling into python 2017-02-11 08:47:42 +05:30
Kovid Goyal
7d2332da21 API to easily invert colors in a window 2017-02-11 08:28:10 +05:30
Kovid Goyal
2b4d0a4ac9 Fix regression that caused scrolling to return to origin when releasing keys 2017-02-10 16:03:20 +05:30
Kovid Goyal
edab1aebaa Allow clients to query if the terminal emulator support styled underlines 2017-02-10 16:00:51 +05:30
Kovid Goyal
924172f1ac Note on representation 2017-02-10 15:37:39 +05:30
Kovid Goyal
d9563e52c8 Reduce size of encoded key event by using base64 encoding 2017-02-10 15:34:06 +05:30
Kovid Goyal
ded9cf227a ... 2017-02-10 15:14:48 +05:30
Kovid Goyal
063d6652e0 ... 2017-02-10 15:13:22 +05:30
Kovid Goyal
0f8b83755a Implement the extended keyboard protocol 2017-02-10 15:11:07 +05:30
Kovid Goyal
45334d6b35 Recognize extended keyboard mode 2017-02-10 12:13:31 +05:30
Kovid Goyal
96921e3a39 Forgot one limitation 2017-02-10 11:48:39 +05:30
Kovid Goyal
7080168bdd Improved formatting 2017-02-10 11:42:20 +05:30
Kovid Goyal
48a2a395c4 Spec for extended keyboard protocol 2017-02-10 11:34:38 +05:30
Kovid Goyal
b6c0eb0909 ... 2017-02-10 09:47:46 +05:30
Kovid Goyal
b9a2524992 Add a link to the issue for graphics rendering 2017-02-10 09:45:41 +05:30
Kovid Goyal
28f0dc1e51 oops 2017-02-10 01:12:07 +05:30
Kovid Goyal
cbe599735c Allow overriding individual configuration options on the command line 2017-02-10 00:48:17 +05:30
Kovid Goyal
e012e9459d pep8 2017-02-10 00:34:53 +05:30
Kovid Goyal
eb71799b42 Allow specifying multiple config files
Can be used to create a "base" profile and then modify only a few
settings in different config files, that are merged when read.
2017-02-10 00:31:00 +05:30
Kovid Goyal
fe3f0932ea pep8 2017-02-10 00:23:35 +05:30
Kovid Goyal
783717b8a4 pep8 2017-02-10 00:04:05 +05:30
Kovid Goyal
bb37516d6f Add a note to the README about kitty's font control features 2017-02-09 21:38:05 +05:30
Kovid Goyal
6c6f000229 Implement symbol maps
A config option to use special fonts for specified unicode characters.
Useful for things like Powerline without needing patched fonts.
2017-02-09 21:15:53 +05:30
Kovid Goyal
a4715de5dc Fix tab bar borders not being blanked 2017-02-09 17:34:56 +05:30
Kovid Goyal
cbf0959fbf Fix tab bar not being rendered after first new tab is created 2017-02-09 17:02:02 +05:30
Kovid Goyal
49c81da763 Adjust rescale threshold 2017-02-09 13:38:19 +05:30
Kovid Goyal
7e34807859 Linux: use only advances for calculating cell sizes
There are apparently monospace fonts whose characters are not actually
monospaced when rendered, for example, Liberation Mono. It has
characters that when rendered result in bitmaps wider than the advance.

So we use only the advance as that is what most linux software seems to
do. Fixes #47
2017-02-09 13:36:44 +05:30
Kovid Goyal
e2ac9ec118 Fix building on OS X 2017-02-09 09:18:41 +05:30
Kovid Goyal
c58be6ddf5 A spot of refactoring 2017-02-08 22:24:58 +05:30
Kovid Goyal
b9b15d41f4 Explicitly depend only on fontconfig since it in turn depends on freetype 2017-02-08 21:59:05 +05:30
Kovid Goyal
dea60cdaf0 Forgot to also catch KeyError 2017-02-08 21:57:39 +05:30
Kovid Goyal
02ef3c6dc8 Linux: Use libfontconfig directly instead of calling fc-match
There are apparently some linux systems out there with versions of
fontconfig that do not understanf the :charset query when passed to
fc-match. See #46
2017-02-08 21:52:10 +05:30
Kovid Goyal
882a4f2ab3 Linux: Add support for .ttc files 2017-02-08 21:07:10 +05:30
Kovid Goyal
85e05a447d Linux: Fallback to using bitmapped fonts for characters that are not present in any scalable fonts on the system
Fixes #46
2017-02-08 11:29:24 +05:30
Kovid Goyal
1ff4e9703a Refactor the fontconfig API
Makes it more flexible and DRYer
2017-02-08 10:10:07 +05:30
Kovid Goyal
0b2af7c33a Configure yapf/isort 2017-02-08 09:59:36 +05:30
Kovid Goyal
68115b50a5 Simplification 2017-02-08 09:45:27 +05:30
Kovid Goyal
9aa1d74f83 Linux: Don't crash when fontconfig is unable to find a font for a character.
Instead render it as a missing glyph
2017-02-08 09:32:15 +05:30
Kovid Goyal
4532194b01 ... 2017-02-06 22:18:59 +05:30
Kovid Goyal
ebacb16f67 Compile the launcher with -fpie 2017-02-06 16:19:33 +05:30
Kovid Goyal
585a01fff6 Start out in normal key mode
Matches behavior of xterm
2017-02-05 16:39:40 +05:30
Kovid Goyal
bb7edb5f8f Implement DECRQM
Also add tests for DECRQM and DECCKM
2017-02-05 16:21:13 +05:30
Kovid Goyal
142c883b0c Implement DECCKM 2017-02-05 15:30:16 +05:30
Kovid Goyal
bf3f6f6014 Add a note about the availability of the dmg 2017-02-02 16:35:02 +05:30
Kovid Goyal
688d0f74c4 Use print() instead of stdout.buffer.write() 2017-02-02 16:30:53 +05:30
Kovid Goyal
71b4ea5a60 oops 2017-02-02 16:24:38 +05:30
40 changed files with 1752 additions and 350 deletions

View File

@@ -12,7 +12,7 @@ matrix:
addons:
apt:
packages:
- libfreetype6-dev
- libfontconfig1-dev
- libglew-dev
- libxi-dev
- libxrandr-dev
@@ -30,7 +30,7 @@ matrix:
addons:
apt:
packages:
- libfreetype6-dev
- libfontconfig1-dev
- libglew-dev
- libxi-dev
- libxrandr-dev

View File

@@ -21,7 +21,7 @@
:sc_next_window: pass:quotes[`ctrl+shift+]`]
:sc_ninth_window: pass:quotes[`ctrl+shift+9`]
:sc_paste_from_clipboard: pass:quotes[`ctrl+shift+v`]
:sc_paste_from_selection: pass:quotes[`ctrl+shift+s`]
:sc_paste_from_selection: pass:quotes[`ctrl+shift+s` or `shift+insert`]
:sc_previous_tab: pass:quotes[`ctrl+shift+left`]
:sc_previous_window: pass:quotes[`ctrl+shift+[`]
:sc_scroll_end: pass:quotes[`ctrl+shift+end`]
@@ -64,7 +64,7 @@ speed). Less than ten thousand lines of code.
* 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. See link:../../issues/5[5] for the status of the macOS port.
other platforms.
image::screenshot.png?raw=true[Screenshot, showing three programs in the "Tall" layout]
@@ -81,7 +81,7 @@ the following dependencies are installed first.
* python >= 3.5
* glfw >= 3.2
* glew >= 2.0 (not needed on macOS)
* freetype, fontconfig (not needed on macOS)
* fontconfig (not needed on macOS)
* xdpyinfo and xsel (only on X11 based systems)
* gcc or clang (required only for building)
* pkg-config (required only for building)
@@ -111,10 +111,11 @@ python3 /path/to/kitty/folder
=== macOS packages
Currently, no one has made macOS packages for kitty, but you can use the above
install from source instructions to run kitty on macOS, after installing its
two dependencies (python >= 3.5 and glfw >= 3.2 using http://brew.sh/[brew] or
a similar package manager)
kitty is available as a macOS `dmg` file for easy installation from the
link:../../releases[releases page]. You can also run kitty directly from
source using the above install from source instructions, after installing its
two dependencies (`python >= 3.5` and `glfw >= 3.2` using http://brew.sh/[brew]
or a similar package manager)
== Design philosophy
@@ -127,7 +128,7 @@ easy reproducability (I like to store config files in source control).
The code in kitty is designed to be simple, modular and hackable. It is
written in a mix of C (for performance sensitive parts) and Python (for
easy hackability of the UI). It does not depend on any large and complex
UI toolkit, using only OpenGL+FreeType for rendering everything.
UI toolkit, using only OpenGL for rendering everything.
Finally, kitty is designed from the ground up to support all modern
terminal features, such as unicode, true color, bold/italic fonts, text
@@ -280,6 +281,17 @@ launch emacs
kitty has a few extensions to the xterm protocol, to enable advanced features,
see link:protocol-extensions.asciidoc[Protocol Extensions].
== Font control
kitty has extremely flexible and powerful font selection features. You can
specify individual families for the regular, bold, italic and bold+italic
fonts. You can even specify specific font families for specific ranges of
unicode characters. This allows precise control over text rendering. It can
come in handy for applications like powerline, without the need to use patched
fonts. See the various font related configuration directives in the
link:kitty/kitty.conf[config file].
== Note for Linux/macOS packagers
While kitty does use python, it is not a traditional python package, so please do not install it in site-packages.

View File

@@ -1,2 +1,2 @@
#!/bin/bash
cloc --exclude-list-file <(echo -e 'kitty/wcwidth9.h\nkitty/unicode-data.h\nkitty/gl.h\nkitty/glfw.c\nkitty/glfw.h\nkitty/charsets.c') kitty
cloc --exclude-list-file <(echo -e 'kitty/wcwidth9.h\nkitty/unicode-data.h\nkitty/gl.h\nkitty/glfw.c\nkitty/glfw.h\nkitty/charsets.c\nkitty/key_encoding.py') kitty

128
key_encoding.asciidoc Normal file
View File

@@ -0,0 +1,128 @@
= Key encoding for extended keyboard protocol
See link:protocol-extensions.asciidoc#keyboard-handling[Keyboard Handling protocol extension]
|===
| Name | Encoded representation (base64)
| 0 | `G`
| 1 | `H`
| 2 | `I`
| 3 | `J`
| 4 | `K`
| 5 | `L`
| 6 | `M`
| 7 | `N`
| 8 | `O`
| 9 | `P`
| A | `S`
| APOSTROPHE | `B`
| B | `T`
| BACKSLASH | `t`
| BACKSPACE | `1`
| C | `U`
| CAPS LOCK | `:`
| COMMA | `C`
| D | `V`
| DELETE | `3`
| DOWN | `6`
| E | `W`
| END | `-`
| ENTER | `z`
| EQUAL | `R`
| ESCAPE | `y`
| F | `X`
| F1 | `/`
| F10 | `]`
| F11 | `{`
| F12 | `}`
| F13 | `@`
| F14 | `%`
| F15 | `$`
| F16 | `#`
| F17 | `BA`
| F18 | `BB`
| F19 | `BC`
| F2 | `*`
| F20 | `BD`
| F21 | `BE`
| F22 | `BF`
| F23 | `BG`
| F24 | `BH`
| F25 | `BI`
| F3 | `?`
| F4 | `&`
| F5 | `<`
| F6 | `>`
| F7 | `(`
| F8 | `)`
| F9 | `[`
| G | `Y`
| GRAVE ACCENT | `v`
| H | `Z`
| HOME | `.`
| I | `a`
| INSERT | `2`
| J | `b`
| K | `c`
| KP 0 | `BJ`
| KP 1 | `BK`
| KP 2 | `BL`
| KP 3 | `BM`
| KP 4 | `BN`
| KP 5 | `BO`
| KP 6 | `BP`
| KP 7 | `BQ`
| KP 8 | `BR`
| KP 9 | `BS`
| KP ADD | `BX`
| KP DECIMAL | `BT`
| KP DIVIDE | `BU`
| KP ENTER | `BY`
| KP EQUAL | `BZ`
| KP MULTIPLY | `BV`
| KP SUBTRACT | `BW`
| L | `d`
| LEFT | `5`
| LEFT ALT | `Bc`
| LEFT BRACKET | `s`
| LEFT CONTROL | `Bb`
| LEFT SHIFT | `Ba`
| LEFT SUPER | `Bd`
| M | `e`
| MINUS | `D`
| N | `f`
| NUM LOCK | `=`
| O | `g`
| P | `h`
| PAGE DOWN | `9`
| PAGE UP | `8`
| PAUSE | `!`
| PERIOD | `E`
| PRINT SCREEN | `^`
| Q | `i`
| R | `j`
| RIGHT | `4`
| RIGHT ALT | `Bg`
| RIGHT BRACKET | `u`
| RIGHT CONTROL | `Bf`
| RIGHT SHIFT | `Be`
| RIGHT SUPER | `Bh`
| S | `k`
| SCROLL LOCK | `+`
| SEMICOLON | `Q`
| SLASH | `F`
| SPACE | `A`
| T | `l`
| TAB | `0`
| U | `m`
| UP | `7`
| V | `n`
| W | `o`
| WORLD 1 | `w`
| WORLD 2 | `x`
| X | `p`
| Y | `q`
| Z | `r`
|===

View File

@@ -3,12 +3,13 @@
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from ctypes import addressof
from itertools import chain
from threading import Lock
from .constants import viewport_size, GLfloat, GLint, GLuint
from .fast_data_types import glUniform3fv, GL_TRIANGLE_FAN, glMultiDrawArrays
from .utils import get_dpi
from .constants import GLfloat, GLint, GLuint, viewport_size
from .fast_data_types import GL_TRIANGLE_FAN, glMultiDrawArrays, glUniform3fv
from .shaders import ShaderProgram
from .utils import get_dpi
def as_color(c):
@@ -71,9 +72,9 @@ class Borders:
*as_color(opts.inactive_border_color)
)
def __call__(self, windows, active_window, current_layout, draw_window_borders=True):
def __call__(self, windows, active_window, current_layout, extra_blank_rects, draw_window_borders=True):
rects = []
for br in current_layout.blank_rects:
for br in chain(current_layout.blank_rects, extra_blank_rects):
rects.extend(as_rect(*br))
if draw_window_borders and self.border_width > 0:
bw = self.border_width

View File

@@ -259,6 +259,7 @@ class Boss(Thread):
is_key_pressed[key] = action == GLFW_PRESS
self.start_cursor_blink()
self.cursor_blink_zero_time = monotonic()
func = None
if action == GLFW_PRESS or action == GLFW_REPEAT:
func = get_shortcut(self.opts.keymap, mods, key, scancode)
if func is not None:
@@ -267,24 +268,24 @@ class Boss(Thread):
passthrough = f()
if not passthrough:
return
tab = self.active_tab
if tab is None:
return
window = self.active_window
if window is not None:
yield window
if func is not None:
f = getattr(tab, func, getattr(window, func, None))
if f is not None:
passthrough = f()
if not passthrough:
return
if window.screen.auto_repeat_enabled or action == GLFW_PRESS:
if window.char_grid.scrolled_by and key not in MODIFIER_KEYS:
window.scroll_end()
data = interpret_key_event(key, scancode, mods)
if data:
window.write_to_child(data)
tab = self.active_tab
if tab is None:
return
window = self.active_window
if window is None:
return
yield window
if func is not None:
f = getattr(tab, func, getattr(window, func, None))
if f is not None:
passthrough = f()
if not passthrough:
return
if window.char_grid.scrolled_by and key not in MODIFIER_KEYS and action == GLFW_PRESS:
window.scroll_end()
data = interpret_key_event(key, scancode, mods, window, action)
if data:
window.write_to_child(data)
@callback
def on_focus(self, window, focused):
@@ -396,7 +397,7 @@ class Boss(Thread):
self.tab_manager.render(self.cell_program, self.sprites)
for window, rd in render_data.items():
if rd is not None:
window.char_grid.render_cells(rd, self.cell_program, self.sprites)
window.render_cells(rd, self.cell_program, self.sprites)
active = self.active_window
rd = render_data.get(active)
if rd is not None:

View File

@@ -31,6 +31,7 @@ uniform uvec2 dimensions; // xnum, ynum
uniform vec4 steps; // xstart, ystart, dx, dy
uniform vec2 sprite_layout; // dx, dy
uniform usamplerBuffer sprite_map; // gl_InstanceID -> x, y, z
uniform uvec2 color_indices; // which color to use as fg and which as bg
out vec3 sprite_pos;
out vec3 underline_pos;
out vec3 strike_pos;
@@ -78,8 +79,8 @@ void main() {
uvec4 spos = texelFetch(sprite_map, sprite_id);
uvec4 colors = texelFetch(sprite_map, sprite_id + 1);
sprite_pos = to_sprite_pos(pos, spos[0], spos[1], spos[2]);
foreground = to_color(colors[0]);
background = to_color(colors[1]);
foreground = to_color(colors[color_indices[0]]);
background = to_color(colors[color_indices[1]]);
uint decoration = colors[2];
decoration_fg = to_color(decoration);
underline_pos = to_sprite_pos(pos, (decoration >> 24) & SMASK, ZERO, ZERO);
@@ -195,12 +196,15 @@ class Selection: # {{{
if y == b[0]:
endx = max(0, min(b[1], endx))
l = line(y)
is_continued = l.is_continued()
if endx - startx >= linebuf.xnum - 1:
l = str(l).rstrip(' ')
else:
l = ''.join(l[x] for x in range(startx, endx + 1))
if not is_continued and startx == 0 and len(lines) > 0:
l = '\n' + l
lines.append(l)
return '\n'.join(lines)
return ''.join(lines)
# }}}
@@ -213,10 +217,11 @@ def calculate_gl_geometry(window_geometry):
return ScreenGeometry(xstart, ystart, window_geometry.xnum, window_geometry.ynum, dx, dy)
def render_cells(buffer_id, sg, cell_program, sprites):
def render_cells(buffer_id, sg, cell_program, sprites, invert_colors=False):
sprites.bind_sprite_map(buffer_id)
ul = cell_program.uniform_location
glUniform2ui(ul('dimensions'), sg.xnum, sg.ynum)
glUniform2ui(ul('color_indices'), 1 if invert_colors else 0, 0 if invert_colors else 1)
glUniform4f(ul('steps'), sg.xstart, sg.ystart, sg.dx, sg.dy)
glUniform1i(ul('sprites'), sprites.sampler_num)
glUniform1i(ul('sprite_map'), sprites.buffer_sampler_num)
@@ -372,6 +377,10 @@ class CharGrid:
for m in self.url_pat.finditer(text):
if m.start() <= x < m.end():
url = ''.join(l[i] for i in range(*m.span())).rstrip('.')
# Remove trailing "] and similar
url = re.sub(r'''["'][)}\]]$''', '', url)
# Remove closing trailing character if it is matched by it's
# corresponding opening character before the url
if m.start() > 0:
before = l[m.start() - 1]
closing = {'(': ')', '[': ']', '{': '}', '<': '>', '"': '"', "'": "'", '`': '`', '|': '|', ':': ':'}.get(before)
@@ -456,8 +465,8 @@ class CharGrid:
self.last_rendered_selection = sel
return sg
def render_cells(self, sg, cell_program, sprites):
render_cells(self.buffer_id, sg, cell_program, sprites)
def render_cells(self, sg, cell_program, sprites, invert_colors=False):
render_cells(self.buffer_id, sg, cell_program, sprites, invert_colors=invert_colors)
def render_cursor(self, sg, cursor_program, is_focused):
cursor = self.current_cursor

View File

@@ -2,21 +2,19 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import re
import sys
import os
import shlex
import json
import os
import re
import shlex
import sys
import tempfile
from collections import namedtuple
from .fast_data_types import (
CURSOR_BLOCK, CURSOR_BEAM, CURSOR_UNDERLINE
)
import kitty.fast_data_types as defines
from .utils import to_color, safe_print
from .layout import all_layouts
from . import fast_data_types as defines
from .constants import config_dir
from .fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE
from .layout import all_layouts
from .utils import safe_print, to_color
key_pat = re.compile(r'([a-zA-Z][a-zA-Z0-9_-]*)\s+(.+)$')
@@ -25,14 +23,21 @@ def to_font_size(x):
return max(6, float(x))
cshapes = {'block': CURSOR_BLOCK, 'beam': CURSOR_BEAM, 'underline': CURSOR_UNDERLINE}
cshapes = {
'block': CURSOR_BLOCK,
'beam': CURSOR_BEAM,
'underline': CURSOR_UNDERLINE
}
def to_cursor_shape(x):
try:
return cshapes[x.lower()]
except KeyError:
raise ValueError('Invalid cursor shape: {} allowed values are {}'.format(x, ', '.join(cshapes)))
raise ValueError(
'Invalid cursor shape: {} allowed values are {}'.
format(x, ', '.join(cshapes))
)
def to_bool(x):
@@ -53,15 +58,28 @@ def parse_mods(parts):
try:
mods |= getattr(defines, 'GLFW_MOD_' + map_mod(m.upper()))
except AttributeError:
safe_print('Shortcut: {} has an unknown modifier, ignoring'.format(parts.join('+')), file=sys.stderr)
safe_print(
'Shortcut: {} has an unknown modifier, ignoring'.
format(parts.join('+')),
file=sys.stderr
)
return
return mods
named_keys = {"'": 'APOSTROPHE', ',': 'COMMA', '-': 'MINUS', '.': 'PERIOD',
'/': 'SLASH', ';': 'SEMICOLON', '=': 'EQUAL', '[': 'LEFT_BRACKET',
']': 'RIGHT_BRACKET', '`': 'GRAVE_ACCENT'}
named_keys = {
"'": 'APOSTROPHE',
',': 'COMMA',
'-': 'MINUS',
'.': 'PERIOD',
'/': 'SLASH',
';': 'SEMICOLON',
'=': 'EQUAL',
'[': 'LEFT_BRACKET',
']': 'RIGHT_BRACKET',
'`': 'GRAVE_ACCENT'
}
def parse_key(val, keymap):
@@ -75,11 +93,48 @@ def parse_key(val, keymap):
key = parts[-1].upper()
key = getattr(defines, 'GLFW_KEY_' + named_keys.get(key, key), None)
if key is None:
safe_print('Shortcut: {} has an unknown key, ignoring'.format(val), file=sys.stderr)
safe_print(
'Shortcut: {} has an unknown key, ignoring'.format(val),
file=sys.stderr
)
return
keymap[(mods, key)] = action
def parse_symbol_map(val):
parts = val.split(' ')
symbol_map = {}
def abort():
safe_print(
'Symbol map: {} is invalid, ignoring'.format(val), file=sys.stderr
)
return {}
if len(parts) < 2:
return abort()
family = ' '.join(parts[1:])
def to_chr(x):
if not x.startswith('U+'):
raise ValueError()
x = int(x[2:], 16)
return x
for x in parts[0].split(','):
a, b = x.partition('-')[::2]
b = b or a
try:
a, b = map(to_chr, (a, b))
except Exception:
return abort()
if b < a or max(a, b) > sys.maxunicode or min(a, b) < 1:
return abort()
for y in range(a, b + 1):
symbol_map[chr(y)] = family
return symbol_map
def to_open_url_modifiers(val):
return parse_mods(val.split('+'))
@@ -104,6 +159,8 @@ type_map = {
'repaint_delay': int,
'window_border_width': float,
'wheel_scroll_multiplier': float,
'visual_bell_duration': float,
'enable_audio_bell': to_bool,
'click_interval': float,
'mouse_hide_wait': float,
'cursor_blink_interval': float,
@@ -115,7 +172,10 @@ type_map = {
'use_system_wcwidth': to_bool,
}
for name in 'foreground background cursor active_border_color inactive_border_color selection_foreground selection_background'.split():
for name in (
'foreground background cursor active_border_color inactive_border_color'
' selection_foreground selection_background'
).split():
type_map[name] = lambda x: to_color(x, validate=True)
for i in range(16):
type_map['color%d' % i] = lambda x: to_color(x, validate=True)
@@ -125,7 +185,7 @@ for a in ('active', 'inactive'):
def parse_config(lines):
ans = {'keymap': {}}
ans = {'keymap': {}, 'symbol_map': {}}
for line in lines:
line = line.strip()
if not line or line.startswith('#'):
@@ -136,6 +196,9 @@ def parse_config(lines):
if key == 'map':
parse_key(val, ans['keymap'])
continue
if key == 'symbol_map':
ans['symbol_map'].update(parse_symbol_map(val))
continue
tm = type_map.get(key)
if tm is not None:
val = tm(val)
@@ -143,10 +206,13 @@ def parse_config(lines):
return ans
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'kitty.conf')) as f:
with open(
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'kitty.conf')
) as f:
defaults = parse_config(f.readlines())
Options = namedtuple('Defaults', ','.join(defaults.keys()))
defaults = Options(**defaults)
actions = frozenset(defaults.keymap.values())
def update_dict(a, b):
@@ -155,34 +221,55 @@ def update_dict(a, b):
def merge_dicts(vals, defaults):
return {k: update_dict(v, vals.get(k, {})) if isinstance(v, dict) else vals.get(k, v) for k, v in defaults.items()}
return {
k: update_dict(v, vals.get(k, {}))
if isinstance(v, dict) else vals.get(k, v)
for k, v in defaults.items()
}
def load_config(path: str) -> Options:
if not path:
return defaults
try:
f = open(path)
except FileNotFoundError:
return defaults
ans = defaults._asdict()
actions = frozenset(defaults.keymap.values())
with f:
vals = parse_config(f)
remove_keys = {k for k, v in vals.get('keymap', {}).items() if v in ('noop', 'no-op', 'no_op')}
vals['keymap'] = {k: v for k, v in vals.get('keymap', {}).items() if v in actions}
def merge_configs(ans, vals):
vals['keymap'] = {
k: v
for k, v in vals.get('keymap', {}).items() if v in actions
}
remove_keys = {
k
for k, v in vals.get('keymap', {}).items()
if v in ('noop', 'no-op', 'no_op')
}
ans = merge_dicts(vals, ans)
for k in remove_keys:
ans['keymap'].pop(k, None)
return ans
def load_config(*paths, overrides=None) -> Options:
ans = defaults._asdict()
for path in paths:
if not path:
continue
try:
f = open(path)
except FileNotFoundError:
continue
with f:
vals = parse_config(f)
ans = merge_configs(ans, vals)
if overrides is not None:
vals = parse_config(overrides)
ans = merge_configs(ans, vals)
return Options(**ans)
def build_ansi_color_table(opts: Options=defaults):
def as_int(x):
return (x[0] << 16) | (x[1] << 8) | x[2]
def col(i):
return as_int(getattr(opts, 'color{}'.format(i)))
return list(map(col, range(16)))
@@ -198,21 +285,33 @@ def load_cached_values():
except FileNotFoundError:
pass
except Exception as err:
safe_print('Failed to load cached values with error: {}'.format(err), file=sys.stderr)
safe_print(
'Failed to load cached values with error: {}'.format(err),
file=sys.stderr
)
def save_cached_values():
fd, p = tempfile.mkstemp(dir=os.path.dirname(cached_path), suffix='cached.json.tmp')
fd, p = tempfile.mkstemp(
dir=os.path.dirname(cached_path), suffix='cached.json.tmp'
)
try:
with os.fdopen(fd, 'wb') as f:
f.write(json.dumps(cached_values).encode('utf-8'))
os.rename(p, cached_path)
except Exception as err:
safe_print('Failed to save cached values with error: {}'.format(err), file=sys.stderr)
safe_print(
'Failed to save cached values with error: {}'.format(err),
file=sys.stderr
)
finally:
try:
os.remove(p)
except FileNotFoundError:
pass
except Exception as err:
safe_print('Failed to delete temp file for saved cached values with error: {}'.format(err), file=sys.stderr)
safe_print(
'Failed to delete temp file for saved cached values with error: {}'.
format(err),
file=sys.stderr
)

View File

@@ -15,7 +15,7 @@ from .fast_data_types import (
GLFW_KEY_LEFT_SUPER, GLFW_KEY_RIGHT_SUPER)
appname = 'kitty'
version = (0, 1, 0)
version = (0, 2, 1)
str_version = '.'.join(map(str, version))
_plat = sys.platform.lower()
isosx = 'darwin' in _plat

View File

@@ -40,6 +40,9 @@ static PyMethodDef module_methods[] = {
{"read_bytes_dump", (PyCFunction)read_bytes_dump, METH_VARARGS, ""},
{"wcwidth", (PyCFunction)wcwidth_wrap, METH_O, ""},
{"change_wcwidth", (PyCFunction)change_wcwidth_wrap, METH_O, ""},
#ifndef __APPLE__
{"get_fontconfig_font", (PyCFunction)get_fontconfig_font, METH_VARARGS, ""},
#endif
GLFW_FUNC_WRAPPERS
{NULL, NULL, 0, NULL} /* Sentinel */
};
@@ -77,6 +80,7 @@ PyInit_fast_data_types(void) {
#else
if (!init_Face(m)) return NULL;
if (!init_freetype_library(m)) return NULL;
if (!init_fontconfig_library(m)) return NULL;
#endif
PyModule_AddIntConstant(m, "BOLD", BOLD_SHIFT);
PyModule_AddIntConstant(m, "ITALIC", ITALIC_SHIFT);

View File

@@ -244,8 +244,8 @@ PyTypeObject ChangeTracker_Type;
typedef struct {
bool mLNM, mIRM, mDECTCEM, mDECSCNM, mDECOM, mDECAWM, mDECCOLM, mDECARM,
mBRACKETED_PASTE, mFOCUS_TRACKING;
bool mLNM, mIRM, mDECTCEM, mDECSCNM, mDECOM, mDECAWM, mDECCOLM, mDECARM, mDECCKM,
mBRACKETED_PASTE, mFOCUS_TRACKING, mEXTENDED_KEYBOARD;
unsigned long mouse_tracking_mode, mouse_tracking_protocol;
} ScreenModes;
PyTypeObject ScreenModes_Type;
@@ -406,6 +406,7 @@ void screen_request_capabilities(Screen *, PyObject *);
void report_device_attributes(Screen *self, unsigned int UNUSED mode, bool UNUSED secondary);
void select_graphic_rendition(Screen *self, unsigned int *params, unsigned int count);
void report_device_status(Screen *self, unsigned int which, bool UNUSED);
void report_mode_status(Screen *self, unsigned int which, bool);
#define DECLARE_CH_SCREEN_HANDLER(name) void screen_##name(Screen *screen);
DECLARE_CH_SCREEN_HANDLER(bell)
DECLARE_CH_SCREEN_HANDLER(backspace)
@@ -414,3 +415,5 @@ DECLARE_CH_SCREEN_HANDLER(linefeed)
DECLARE_CH_SCREEN_HANDLER(carriage_return)
bool init_freetype_library(PyObject*);
bool init_fontconfig_library(PyObject*);
PyObject *get_fontconfig_font(PyObject *self, PyObject *args);

84
kitty/fontconfig.c Normal file
View File

@@ -0,0 +1,84 @@
/*
* fontconfig.c
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "data-types.h"
#include <fontconfig/fontconfig.h>
PyObject*
get_fontconfig_font(PyObject UNUSED *self, PyObject *args) {
char *family;
int bold, italic, allow_bitmapped_fonts, index = 0, hint_style=0, weight=0, slant=0;
double size_in_pts, dpi;
unsigned int character;
FcBool hinting, scalable, outline;
FcChar8 *path = NULL;
FcPattern *pat = NULL, *match = NULL;
FcResult result;
FcCharSet *charset = NULL;
PyObject *ans = NULL;
if (!PyArg_ParseTuple(args, "spppdId", &family, &bold, &italic, &allow_bitmapped_fonts, &size_in_pts, &character, &dpi)) return NULL;
pat = FcPatternCreate();
if (pat == NULL) return PyErr_NoMemory();
#define AP(func, which, in, desc) if (!func(pat, which, in)) { PyErr_Format(PyExc_RuntimeError, "Failed to add %s to fontconfig patter", desc, NULL); goto end; }
AP(FcPatternAddString, FC_FAMILY, (const FcChar8*)family, "family");
if (!allow_bitmapped_fonts) {
AP(FcPatternAddBool, FC_OUTLINE, true, "outline");
AP(FcPatternAddBool, FC_SCALABLE, true, "scalable");
}
if (size_in_pts > 0) { AP(FcPatternAddDouble, FC_SIZE, size_in_pts, "size"); }
if (dpi > 0) { AP(FcPatternAddDouble, FC_DPI, dpi, "dpi"); }
if (bold) { AP(FcPatternAddInteger, FC_WEIGHT, FC_WEIGHT_BOLD, "weight"); }
if (italic) { AP(FcPatternAddInteger, FC_SLANT, FC_SLANT_ITALIC, "slant"); }
if (character > 0) {
charset = FcCharSetCreate();
if (charset == NULL) { PyErr_NoMemory(); goto end; }
if (!FcCharSetAddChar(charset, character)) { PyErr_SetString(PyExc_RuntimeError, "Failed to add character to fontconfig charset"); goto end; }
AP(FcPatternAddCharSet, FC_CHARSET, charset, "charset");
}
#undef AP
FcConfigSubstitute(NULL, pat, FcMatchPattern);
FcDefaultSubstitute(pat);
match = FcFontMatch(NULL, pat, &result);
if (match == NULL) { PyErr_SetString(PyExc_KeyError, "FcFontMatch() failed"); goto end; }
#define GI(func, which, out, desc) \
if (func(match, which, 0, & out) != FcResultMatch) { \
PyErr_Format(PyExc_RuntimeError, "Failed to get %s from match object", desc, NULL); goto end; \
}
GI(FcPatternGetString, FC_FILE, path, "file path");
GI(FcPatternGetInteger, FC_INDEX, index, "face index");
GI(FcPatternGetInteger, FC_WEIGHT, weight, "weight");
GI(FcPatternGetInteger, FC_SLANT, slant, "slant");
GI(FcPatternGetInteger, FC_HINT_STYLE, hint_style, "hint style");
GI(FcPatternGetBool, FC_HINTING, hinting, "hinting");
GI(FcPatternGetBool, FC_SCALABLE, scalable, "scalable");
GI(FcPatternGetBool, FC_OUTLINE, outline, "outline");
#undef GI
#define BP(x) (x ? Py_True : Py_False)
ans = Py_BuildValue("siiOOOii", path, index, hint_style, BP(hinting), BP(scalable), BP(outline), weight, slant);
#undef BP
end:
if (pat != NULL) FcPatternDestroy(pat);
if (match != NULL) FcPatternDestroy(match);
if (charset != NULL) FcCharSetDestroy(charset);
if (PyErr_Occurred()) return NULL;
return ans;
}
bool
init_fontconfig_library(PyObject UNUSED *m) {
if (!FcInit()) {
PyErr_SetString(PyExc_RuntimeError, "Failed to initialize the fontconfig library");
return false;
}
return true;
}

View File

@@ -26,7 +26,7 @@ def draw_hline(buf, width, x1, x2, y, level):
def draw_vline(buf, width, y1, y2, x, level):
' Draw a horizontal line between [y1, y2) centered at x with the thickness given by level '
' Draw a vertical line between [y1, y2) centered at x with the thickness given by level '
sz = thickness(level=level, horizontal=True)
start = x - sz // 2
for x in range(start, start + sz):
@@ -345,6 +345,15 @@ def render_box_char(ch, buf, width, height):
return buf
def render_missing_glyph(buf, width, height):
hgap = thickness(level=0, horizontal=True) + 1
vgap = thickness(level=0, horizontal=False) + 1
draw_hline(buf, width, hgap, width - hgap + 1, vgap, 0)
draw_hline(buf, width, hgap, width - hgap + 1, height - vgap, 0)
draw_vline(buf, width, vgap, height - vgap + 1, hgap, 0)
draw_vline(buf, width, vgap, height - vgap + 1, width - hgap, 0)
def join_rows(width, height, rows):
import ctypes
ans = (ctypes.c_ubyte * (width * height * len(rows)))()

View File

@@ -7,9 +7,18 @@ from kitty.fast_data_types import CTFace as Face
from kitty.utils import get_logical_dpi, wcwidth, ceil_int
main_font = {}
symbol_map = {}
cell_width = cell_height = baseline = CellTexture = WideCellTexture = underline_thickness = underline_position = None
def install_symbol_map(val, font_size, dpi):
global symbol_map
symbol_map = {}
family_map = {f: Face(f, False, False, False, font_size, dpi) for f in set(val.values())}
for ch, family in val.items():
symbol_map[ch] = family_map[family]
def set_font_family(opts, ignore_dpi_failure=False):
global cell_width, cell_height, baseline, CellTexture, WideCellTexture, underline_thickness, underline_position
try:
@@ -32,6 +41,7 @@ def set_font_family(opts, ignore_dpi_failure=False):
for bold in (False, True):
for italic in (False, True):
main_font[(bold, italic)] = Face(get_family(bold, italic), bold, italic, True, opts.font_size, dpi)
install_symbol_map(opts.symbol_map, opts.font_size, dpi)
mf = main_font[(False, False)]
cell_width, cell_height = mf.cell_size()
CellTexture = ctypes.c_ubyte * (cell_width * cell_height)
@@ -57,8 +67,9 @@ def split(buf, cell_width, cell_height):
def render_cell(text=' ', bold=False, italic=False):
width = wcwidth(text[0])
face = main_font[(bold, italic)]
ch = text[0]
width = wcwidth(ch)
face = symbol_map.get(ch) or main_font[(bold, italic)]
if width == 2:
buf, width = WideCellTexture(), cell_width * 2
else:

View File

@@ -2,57 +2,145 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import os
import re
import subprocess
from collections import namedtuple
from functools import lru_cache
from kitty.fast_data_types import Face
from kitty.fast_data_types import Face, get_fontconfig_font
def escape_family_name(name):
return re.sub(r'([-:,\\])', lambda m: '\\' + m.group(1), name)
Font = namedtuple('Font', 'face hinting hintstyle bold italic')
Font = namedtuple(
'Font',
'face hinting hintstyle bold italic scalable outline weight slant index'
)
def get_font(query, bold, italic):
query += ':scalable=true:outline=true'
raw = subprocess.check_output(['fc-match', query, '-f', '%{file}\x1e%{hinting}\x1e%{hintstyle}']).decode('utf-8')
class FontNotFound(ValueError):
pass
def to_bool(x):
return x.lower() == 'true'
def font_not_found(err, char):
msg = 'Failed to find font'
if char is not None:
msg = 'Failed to find font for character U+{:X}, error from fontconfig: {}'. format(ord(char[0]), err)
return FontNotFound(msg)
def get_font_subprocess(
family='monospace',
bold=False,
italic=False,
allow_bitmaped_fonts=False,
size_in_pts=None,
character=None,
dpi=None
):
query = escape_family_name(family)
if character is not None:
query += ':charset={:x}'.format(ord(character[0]))
if not allow_bitmaped_fonts:
query += ':scalable=true:outline=true'
if size_in_pts is not None:
query += ':size={:.1f}'.format(size_in_pts)
if dpi is not None:
query += ':dpi={:.1f}'.format(dpi)
if bold:
query += ':weight=200'
if italic:
query += ':slant=100'
try:
raw = subprocess.check_output([
'fc-match', query, '-f',
'%{file}\x1e%{hinting}\x1e%{hintstyle}\x1e%{scalable}\x1e%{outline}\x1e%{weight}\x1e%{slant}\x1e%{index}'
]).decode('utf-8')
except subprocess.CalledProcessError as err:
raise font_not_found(err, character)
parts = raw.split('\x1e')
hintstyle, hinting = 1, 'True'
if len(parts) == 3:
path, hinting, hintstyle = parts
else:
try:
path, hinting, hintstyle, scalable, outline, weight, slant, index = parts
except ValueError:
path = parts[0]
hinting = hinting.lower() == 'true'
hintstyle = int(hintstyle)
return Font(path, hinting, hintstyle, bold, italic)
hintstyle, hinting, scalable, outline, weight, slant, index = 1, 'True', 'True', 'True', 100, 0, 0
return Font(
path,
to_bool(hinting),
int(hintstyle), bold, italic,
to_bool(scalable),
to_bool(outline), int(weight), int(slant), int(index)
)
@lru_cache(maxsize=4096)
def find_font_for_character(family, char, bold=False, italic=False):
q = escape_family_name(family) + ':charset={}'.format(hex(ord(char[0]))[2:])
if bold:
q += ':weight=200'
if italic:
q += ':slant=100'
return get_font(q, bold, italic)
def get_font_lib(
family='monospace',
bold=False,
italic=False,
allow_bitmaped_fonts=False,
size_in_pts=None,
character=None,
dpi=None
):
try:
path, index, hintstyle, hinting, scalable, outline, weight, slant = get_fontconfig_font(
family, bold, italic, allow_bitmaped_fonts, size_in_pts or 0,
0 if character is None else ord(character[0]), dpi or 0
)
except KeyError as err:
raise font_not_found(err, character)
return Font(
path, hinting, hintstyle, bold, italic, scalable, outline, weight,
slant, index
)
@lru_cache(maxsize=64)
def get_font_information(q, bold=False, italic=False):
q = escape_family_name(q)
if bold:
q += ':weight=200'
if italic:
q += ':slant=100'
return get_font(q, bold, italic)
get_font = get_font_lib
def find_font_for_character(
family,
char,
bold=False,
italic=False,
allow_bitmaped_fonts=False,
size_in_pts=None,
dpi=None
):
ans = get_font(
family,
bold,
italic,
character=char,
allow_bitmaped_fonts=allow_bitmaped_fonts,
size_in_pts=size_in_pts,
dpi=dpi
)
if not ans.face or not os.path.exists(ans.face):
raise FontNotFound(
'Failed to find font for character U+{:X}'.format(ord(char[0]))
)
return ans
def get_font_information(family, bold=False, italic=False):
return get_font(family, bold, italic)
def get_font_files(opts):
ans = {}
attr_map = {'bold': 'bold_font', 'italic': 'italic_font', 'bi': 'bold_italic_font'}
attr_map = {
'bold': 'bold_font',
'italic': 'italic_font',
'bi': 'bold_italic_font'
}
def get_family(key=None):
ans = getattr(opts, attr_map.get(key, 'font_family'))
@@ -61,11 +149,21 @@ def get_font_files(opts):
return ans
n = get_font_information(get_family())
ans['regular'] = Font(Face(n.face), n.hinting, n.hintstyle, n.bold, n.italic)
ans['regular'] = n._replace(face=Face(n.face, n.index))
def do(key):
b = get_font_information(get_family(key), bold=key in ('bold', 'bi'), italic=key in ('italic', 'bi'))
b = get_font_information(
get_family(key),
bold=key in ('bold', 'bi'),
italic=key in ('italic', 'bi')
)
if b.face != n.face:
ans[key] = Font(Face(b.face), b.hinting, b.hintstyle, b.bold, b.italic)
ans[key] = b._replace(face=Face(b.face, b.index))
do('bold'), do('italic'), do('bi')
return ans
def font_for_family(family):
ans = get_font_information(family)
return ans._replace(face=Face(ans.face, ans.index))

View File

@@ -2,22 +2,26 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import unicodedata
import ctypes
import sys
import unicodedata
from collections import namedtuple
from functools import lru_cache
from itertools import chain
from threading import Lock
from kitty.fast_data_types import Face, FT_PIXEL_MODE_GRAY
from kitty.utils import ceil_int
from .fontconfig import find_font_for_character, get_font_files
from kitty.utils import get_logical_dpi, wcwidth
from kitty.fast_data_types import FT_PIXEL_MODE_GRAY, Face
from kitty.fonts.box_drawing import render_missing_glyph
from kitty.utils import ceil_int, get_logical_dpi, safe_print, wcwidth
from .fontconfig import (
FontNotFound, find_font_for_character, font_for_family, get_font_files
)
current_font_family = current_font_family_name = cff_size = cell_width = cell_height = baseline = None
CharTexture = underline_position = underline_thickness = None
alt_face_cache = {}
symbol_map = {}
def set_char_size(face, width=0, height=0, hres=0, vres=0):
@@ -34,13 +38,33 @@ def calc_cell_width(font, face):
ch = chr(i)
load_char(font, face, ch)
m = face.glyph_metrics()
ans = max(ans, max(ceil_int(m.horiAdvance / 64), face.bitmap().width))
ans = max(ans, ceil_int(m.horiAdvance / 64))
return ans
@lru_cache(maxsize=2**10)
def font_for_char(char, bold=False, italic=False):
return find_font_for_character(current_font_family_name, char, bold, italic)
def font_for_char(char, bold=False, italic=False, allow_bitmaped_fonts=False):
if allow_bitmaped_fonts:
return find_font_for_character(
current_font_family_name,
char,
bold,
italic,
allow_bitmaped_fonts=True,
size_in_pts=cff_size['width'] / 64,
dpi=(cff_size['hres'] + cff_size['vres']) / 2
)
return find_font_for_character(
current_font_family_name, char, bold, italic
)
def install_symbol_map(val):
global symbol_map
symbol_map = {}
family_map = {f: font_for_family(f) for f in set(val.values())}
for ch, family in val.items():
symbol_map[ch] = family_map[family]
def font_units_to_pixels(x, units_per_em, size_in_pts, dpi):
@@ -51,27 +75,44 @@ def set_font_family(opts):
global current_font_family, current_font_family_name, cff_size, cell_width, cell_height, CharTexture, baseline
global underline_position, underline_thickness
size_in_pts = opts.font_size
find_font_for_character.cache_clear()
current_font_family = get_font_files(opts)
current_font_family_name = opts.font_family
dpi = get_logical_dpi()
cff_size = ceil_int(64 * size_in_pts)
cff_size = {'width': cff_size, 'height': cff_size, 'hres': int(dpi[0]), 'vres': int(dpi[1])}
for fobj in current_font_family.values():
cff_size = {
'width': cff_size,
'height': cff_size,
'hres': int(dpi[0]),
'vres': int(dpi[1])
}
install_symbol_map(opts.symbol_map)
for fobj in chain(current_font_family.values(), symbol_map.values()):
set_char_size(fobj.face, **cff_size)
face = current_font_family['regular'].face
cell_width = calc_cell_width(current_font_family['regular'], face)
cell_height = font_units_to_pixels(face.height, face.units_per_EM, size_in_pts, dpi[1])
baseline = font_units_to_pixels(face.ascender, face.units_per_EM, size_in_pts, dpi[1])
underline_position = min(baseline - font_units_to_pixels(face.underline_position, face.units_per_EM, size_in_pts, dpi[1]), cell_height - 1)
underline_thickness = font_units_to_pixels(face.underline_thickness, face.units_per_EM, size_in_pts, dpi[1])
cell_height = font_units_to_pixels(
face.height, face.units_per_EM, size_in_pts, dpi[1]
)
baseline = font_units_to_pixels(
face.ascender, face.units_per_EM, size_in_pts, dpi[1]
)
underline_position = min(
baseline - font_units_to_pixels(
face.underline_position, face.units_per_EM, size_in_pts, dpi[1]
), cell_height - 1
)
underline_thickness = font_units_to_pixels(
face.underline_thickness, face.units_per_EM, size_in_pts, dpi[1]
)
CharTexture = ctypes.c_ubyte * (cell_width * cell_height)
font_for_char.cache_clear()
alt_face_cache.clear()
return cell_width, cell_height
CharBitmap = namedtuple('CharBitmap', 'data bearingX bearingY advance rows columns')
CharBitmap = namedtuple(
'CharBitmap', 'data bearingX bearingY advance rows columns'
)
freetype_lock = Lock()
@@ -80,45 +121,66 @@ def render_to_bitmap(font, face, text):
bitmap = face.bitmap()
if bitmap.pixel_mode != FT_PIXEL_MODE_GRAY:
raise ValueError(
'FreeType rendered the glyph for {!r} with an unsupported pixel mode: {}'.format(text, bitmap.pixel_mode))
'FreeType rendered the glyph for {!r} with an unsupported pixel mode: {}'.
format(text, bitmap.pixel_mode)
)
return bitmap
def render_using_face(font, face, text, width, italic, bold):
bitmap = render_to_bitmap(font, face, text)
if width == 1 and bitmap.width > cell_width:
extra = bitmap.width - cell_width
if italic and extra < cell_width // 2:
bitmap = face.trim_to_width(bitmap, cell_width)
elif extra > max(2, 0.3 * cell_width) and face.is_scalable:
# rescale the font size so that the glyph is visible in a single
# cell and hope somebody updates libc's wcwidth
sz = cff_size.copy()
sz['width'] = int(sz['width'] * cell_width / bitmap.width)
# Preserve aspect ratio
sz['height'] = int(sz['height'] * cell_width / bitmap.width)
try:
set_char_size(face, **sz)
bitmap = render_to_bitmap(font, face, text)
finally:
set_char_size(face, **cff_size)
m = face.glyph_metrics()
return CharBitmap(
bitmap.buffer,
ceil_int(abs(m.horiBearingX) / 64),
ceil_int(abs(m.horiBearingY) / 64),
ceil_int(m.horiAdvance / 64), bitmap.rows, bitmap.width
)
def render_char(text, bold=False, italic=False, width=1):
key = 'regular'
if bold:
key = 'bi' if italic else 'bold'
elif italic:
key = 'italic'
ch = text[0]
with freetype_lock:
font = current_font_family.get(key) or current_font_family['regular']
face = font.face
if not face.get_char_index(text[0]):
font = font_for_char(text[0], bold, italic)
face = alt_face_cache.get(font)
if face is None:
face = alt_face_cache[font] = Face(font.face)
set_char_size(face, **cff_size)
bitmap = render_to_bitmap(font, face, text)
if width == 1 and bitmap.width > cell_width:
extra = bitmap.width - cell_width
if italic and extra < cell_width // 2:
bitmap = face.trim_to_width(bitmap, cell_width)
elif extra > max(2, 0.1 * cell_width):
# rescale the font size so that the glyph is visible in a single
# cell and hope somebody updates libc's wcwidth
sz = cff_size.copy()
sz['width'] = int(sz['width'] * cell_width / bitmap.width)
# Preserve aspect ratio
sz['height'] = int(sz['height'] * cell_width / bitmap.width)
font = symbol_map.get(ch)
if font is None or not font.face.get_char_index(ch):
font = current_font_family.get(key) or current_font_family['regular']
face = font.face
if not face.get_char_index(ch):
try:
set_char_size(face, **sz)
bitmap = render_to_bitmap(font, face, text)
finally:
set_char_size(face, **cff_size)
m = face.glyph_metrics()
return CharBitmap(bitmap.buffer, ceil_int(abs(m.horiBearingX) / 64),
ceil_int(abs(m.horiBearingY) / 64), ceil_int(m.horiAdvance / 64), bitmap.rows, bitmap.width)
font = font_for_char(ch, bold, italic)
except FontNotFound:
font = font_for_char(
ch, bold, italic, allow_bitmaped_fonts=True
)
face = alt_face_cache.get(font)
if face is None:
face = alt_face_cache[font] = Face(font.face, font.index)
if face.is_scalable:
set_char_size(face, **cff_size)
else:
face = font.face
return render_using_face(font, face, text, width, italic, bold)
def place_char_in_cell(bitmap_char):
@@ -133,7 +195,9 @@ def place_char_in_cell(bitmap_char):
extra = dest_start_column + bitmap_char.columns - cell_width
if extra > 0:
dest_start_column -= extra
column_count = min(bitmap_char.columns - src_start_column, cell_width - dest_start_column)
column_count = min(
bitmap_char.columns - src_start_column, cell_width - dest_start_column
)
# Calculate row bounds, making sure the baseline is aligned with the cell
# baseline
@@ -141,9 +205,13 @@ def place_char_in_cell(bitmap_char):
src_start_row, dest_start_row = bitmap_char.bearingY - baseline, 0
else:
src_start_row, dest_start_row = 0, baseline - bitmap_char.bearingY
row_count = min(bitmap_char.rows - src_start_row, cell_height - dest_start_row)
return create_cell_buffer(bitmap_char, src_start_row, dest_start_row, row_count,
src_start_column, dest_start_column, column_count)
row_count = min(
bitmap_char.rows - src_start_row, cell_height - dest_start_row
)
return create_cell_buffer(
bitmap_char, src_start_row, dest_start_row, row_count,
src_start_column, dest_start_column, column_count
)
def split_char_bitmap(bitmap_char):
@@ -168,12 +236,31 @@ def current_cell():
return CharTexture, cell_width, cell_height, baseline, underline_thickness, underline_position
@lru_cache(maxsize=8)
def missing_glyph(width):
w = cell_width * width
ans = bytearray(w * cell_height)
render_missing_glyph(ans, w, cell_height)
first, second = CharBitmap(ans, 0, 0, 0, cell_height, w), None
if width == 2:
first, second = split_char_bitmap(first)
second = create_cell_buffer(
second, 0, 0, second.rows, 0, 0, second.columns
)
first = create_cell_buffer(first, 0, 0, first.rows, 0, 0, first.columns)
return first, second
def render_cell(text=' ', bold=False, italic=False):
# TODO: Handle non-normalizable combining chars. Probably need to use
# harfbuzz for that
text = unicodedata.normalize('NFC', text)[0]
width = wcwidth(text)
bitmap_char = render_char(text, bold, italic, width)
try:
bitmap_char = render_char(text, bold, italic, width)
except FontNotFound as err:
safe_print('ERROR:', err, file=sys.stderr)
return missing_glyph(width)
second = None
if width == 2:
if bitmap_char.columns > cell_width:
@@ -187,11 +274,16 @@ def render_cell(text=' ', bold=False, italic=False):
return first, second
def create_cell_buffer(bitmap_char, src_start_row, dest_start_row, row_count, src_start_column, dest_start_column, column_count):
def create_cell_buffer(
bitmap_char, src_start_row, dest_start_row, row_count, src_start_column,
dest_start_column, column_count
):
src = bitmap_char.data
src_stride = bitmap_char.columns
dest = CharTexture()
for r in range(row_count):
sr, dr = src_start_column + (src_start_row + r) * src_stride, dest_start_column + (dest_start_row + r) * cell_width
sr, dr = src_start_column + (
src_start_row + r
) * src_stride, dest_start_column + (dest_start_row + r) * cell_width
dest[dr:dr + column_count] = src[sr:sr + column_count]
return dest

View File

@@ -15,6 +15,7 @@ typedef struct {
FT_Face face;
unsigned int units_per_EM;
int ascender, descender, height, max_advance_width, max_advance_height, underline_position, underline_thickness;
bool is_scalable;
} Face;
@@ -56,18 +57,20 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
Face *self;
char *path;
int error;
long index;
/* unsigned int columns=80, lines=24, scrollback=0; */
if (!PyArg_ParseTuple(args, "s", &path)) return NULL;
if (!PyArg_ParseTuple(args, "sl", &path, &index)) return NULL;
self = (Face *)type->tp_alloc(type, 0);
if (self != NULL) {
Py_BEGIN_ALLOW_THREADS;
error = FT_New_Face(library, path, 0, &(self->face));
error = FT_New_Face(library, path, index, &(self->face));
Py_END_ALLOW_THREADS;
if(error) { set_freetype_error("Failed to load face, with error:", error); Py_CLEAR(self); return NULL; }
#define CPY(n) self->n = self->face->n;
CPY(units_per_EM); CPY(ascender); CPY(descender); CPY(height); CPY(max_advance_width); CPY(max_advance_height); CPY(underline_position); CPY(underline_thickness);
#undef CPY
self->is_scalable = FT_IS_SCALABLE(self->face);
}
return (PyObject*)self;
}
@@ -236,6 +239,7 @@ static PyMemberDef members[] = {
MEM(max_advance_height, T_INT),
MEM(underline_position, T_INT),
MEM(underline_thickness, T_INT),
MEM(is_scalable, T_BOOL),
{NULL} /* Sentinel */
};

View File

@@ -141,10 +141,14 @@ _glewInit(PyObject UNUSED *self) {
PyErr_Format(PyExc_RuntimeError, "GLEW init failed: %s", glewGetErrorString(err));
return NULL;
}
if(!GLEW_ARB_texture_storage) {
PyErr_SetString(PyExc_RuntimeError, "OpenGL is missing the required ARB_texture_storage extension");
return NULL;
#define ARB_TEST(name) \
if (!GLEW_ARB_##name) { \
PyErr_Format(PyExc_RuntimeError, "The OpenGL driver on this system is missing the required extension: ARB_%s", #name); \
return NULL; \
}
ARB_TEST(texture_storage);
ARB_TEST(texture_buffer_object_rgb32);
#undef ARB_TEST
#endif
Py_RETURN_NONE;
}

304
kitty/key_encoding.py Normal file
View File

@@ -0,0 +1,304 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
import string
from . import fast_data_types as defines
# ENCODING {{{
ENCODING = {
'0': 'G',
'1': 'H',
'2': 'I',
'3': 'J',
'4': 'K',
'5': 'L',
'6': 'M',
'7': 'N',
'8': 'O',
'9': 'P',
'A': 'S',
'APOSTROPHE': 'B',
'B': 'T',
'BACKSLASH': 't',
'BACKSPACE': '1',
'C': 'U',
'CAPS LOCK': ':',
'COMMA': 'C',
'D': 'V',
'DELETE': '3',
'DOWN': '6',
'E': 'W',
'END': '-',
'ENTER': 'z',
'EQUAL': 'R',
'ESCAPE': 'y',
'F': 'X',
'F1': '/',
'F10': ']',
'F11': '{',
'F12': '}',
'F13': '@',
'F14': '%',
'F15': '$',
'F16': '#',
'F17': 'BA',
'F18': 'BB',
'F19': 'BC',
'F2': '*',
'F20': 'BD',
'F21': 'BE',
'F22': 'BF',
'F23': 'BG',
'F24': 'BH',
'F25': 'BI',
'F3': '?',
'F4': '&',
'F5': '<',
'F6': '>',
'F7': '(',
'F8': ')',
'F9': '[',
'G': 'Y',
'GRAVE ACCENT': 'v',
'H': 'Z',
'HOME': '.',
'I': 'a',
'INSERT': '2',
'J': 'b',
'K': 'c',
'KP 0': 'BJ',
'KP 1': 'BK',
'KP 2': 'BL',
'KP 3': 'BM',
'KP 4': 'BN',
'KP 5': 'BO',
'KP 6': 'BP',
'KP 7': 'BQ',
'KP 8': 'BR',
'KP 9': 'BS',
'KP ADD': 'BX',
'KP DECIMAL': 'BT',
'KP DIVIDE': 'BU',
'KP ENTER': 'BY',
'KP EQUAL': 'BZ',
'KP MULTIPLY': 'BV',
'KP SUBTRACT': 'BW',
'L': 'd',
'LEFT': '5',
'LEFT ALT': 'Bc',
'LEFT BRACKET': 's',
'LEFT CONTROL': 'Bb',
'LEFT SHIFT': 'Ba',
'LEFT SUPER': 'Bd',
'M': 'e',
'MINUS': 'D',
'N': 'f',
'NUM LOCK': '=',
'O': 'g',
'P': 'h',
'PAGE DOWN': '9',
'PAGE UP': '8',
'PAUSE': '!',
'PERIOD': 'E',
'PRINT SCREEN': '^',
'Q': 'i',
'R': 'j',
'RIGHT': '4',
'RIGHT ALT': 'Bg',
'RIGHT BRACKET': 'u',
'RIGHT CONTROL': 'Bf',
'RIGHT SHIFT': 'Be',
'RIGHT SUPER': 'Bh',
'S': 'k',
'SCROLL LOCK': '+',
'SEMICOLON': 'Q',
'SLASH': 'F',
'SPACE': 'A',
'T': 'l',
'TAB': '0',
'U': 'm',
'UP': '7',
'V': 'n',
'W': 'o',
'WORLD 1': 'w',
'WORLD 2': 'x',
'X': 'p',
'Y': 'q',
'Z': 'r'
}
KEY_MAP = {
32: 'A',
39: 'B',
44: 'C',
45: 'D',
46: 'E',
47: 'F',
48: 'G',
49: 'H',
50: 'I',
51: 'J',
52: 'K',
53: 'L',
54: 'M',
55: 'N',
56: 'O',
57: 'P',
59: 'Q',
61: 'R',
65: 'S',
66: 'T',
67: 'U',
68: 'V',
69: 'W',
70: 'X',
71: 'Y',
72: 'Z',
73: 'a',
74: 'b',
75: 'c',
76: 'd',
77: 'e',
78: 'f',
79: 'g',
80: 'h',
81: 'i',
82: 'j',
83: 'k',
84: 'l',
85: 'm',
86: 'n',
87: 'o',
88: 'p',
89: 'q',
90: 'r',
91: 's',
92: 't',
93: 'u',
96: 'v',
161: 'w',
162: 'x',
256: 'y',
257: 'z',
258: '0',
259: '1',
260: '2',
261: '3',
262: '4',
263: '5',
264: '6',
265: '7',
266: '8',
267: '9',
268: '.',
269: '-',
280: ':',
281: '+',
282: '=',
283: '^',
284: '!',
290: '/',
291: '*',
292: '?',
293: '&',
294: '<',
295: '>',
296: '(',
297: ')',
298: '[',
299: ']',
300: '{',
301: '}',
302: '@',
303: '%',
304: '$',
305: '#',
306: 'BA',
307: 'BB',
308: 'BC',
309: 'BD',
310: 'BE',
311: 'BF',
312: 'BG',
313: 'BH',
314: 'BI',
320: 'BJ',
321: 'BK',
322: 'BL',
323: 'BM',
324: 'BN',
325: 'BO',
326: 'BP',
327: 'BQ',
328: 'BR',
329: 'BS',
330: 'BT',
331: 'BU',
332: 'BV',
333: 'BW',
334: 'BX',
335: 'BY',
336: 'BZ',
340: 'Ba',
341: 'Bb',
342: 'Bc',
343: 'Bd',
344: 'Be',
345: 'Bf',
346: 'Bg',
347: 'Bh'
}
# END_ENCODING }}}
def encode(
integer,
chars=string.ascii_uppercase + string.ascii_lowercase + string.digits +
'.-:+=^!/*?&<>()[]{}@%$#'
):
ans = ''
d = len(chars)
while True:
integer, remainder = divmod(integer, d)
ans = chars[remainder] + ans
if integer == 0:
break
return ans
def symbolic_name(glfw_name):
return glfw_name[9:].replace('_', ' ')
def update_encoding():
import re
import subprocess
keys = {a for a in dir(defines) if a.startswith('GLFW_KEY_')}
ans = ENCODING
key_map = {}
i = len(ans)
for k in sorted(keys, key=lambda k: getattr(defines, k)):
val = getattr(defines, k)
name = symbolic_name(k)
if val < defines.GLFW_KEY_LAST and val != defines.GLFW_KEY_UNKNOWN:
if name not in ans:
ans[name] = encode(i)
i += 1
key_map[val] = ans[name]
with open(__file__, 'r+') as f:
raw = f.read()
nraw = re.sub(
r'^ENCODING = {.+^# END_ENCODING',
'ENCODING = {!r}\nKEY_MAP={!r}\n# END_ENCODING'.format(
ans, key_map
),
raw,
flags=re.MULTILINE | re.DOTALL
)
if raw == nraw:
raise SystemExit('Failed to replace ENCODING dict')
f.seek(0), f.truncate()
f.write(nraw)
subprocess.check_call(['yapf', '-i', __file__])

View File

@@ -2,10 +2,12 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import kitty.fast_data_types as defines
from . import fast_data_types as defines
from .terminfo import key_as_bytes
from .utils import base64_encode
from .key_encoding import KEY_MAP
key_map = {
smkx_key_map = {
defines.GLFW_KEY_UP: 'kcuu1',
defines.GLFW_KEY_DOWN: 'kcud1',
defines.GLFW_KEY_LEFT: 'kcub1',
@@ -17,16 +19,17 @@ key_map = {
defines.GLFW_KEY_PAGE_UP: 'kpp',
defines.GLFW_KEY_PAGE_DOWN: 'knp',
}
key_map = {k: key_as_bytes(v) for k, v in key_map.items()}
smkx_key_map = {k: key_as_bytes(v) for k, v in smkx_key_map.items()}
for f in range(1, 13):
key_map[getattr(defines, 'GLFW_KEY_F{}'.format(f))] = key_as_bytes('kf{}'.format(f))
del f
kf = getattr(defines, 'GLFW_KEY_F{}'.format(f))
smkx_key_map[kf] = key_as_bytes('kf{}'.format(f))
del f, kf
key_map[defines.GLFW_KEY_ESCAPE] = b'\033'
key_map[defines.GLFW_KEY_ENTER] = b'\r'
key_map[defines.GLFW_KEY_KP_ENTER] = b'\r'
key_map[defines.GLFW_KEY_BACKSPACE] = key_as_bytes('kbs')
key_map[defines.GLFW_KEY_TAB] = b'\t'
smkx_key_map[defines.GLFW_KEY_ESCAPE] = b'\033'
smkx_key_map[defines.GLFW_KEY_ENTER] = b'\r'
smkx_key_map[defines.GLFW_KEY_KP_ENTER] = b'\r'
smkx_key_map[defines.GLFW_KEY_BACKSPACE] = key_as_bytes('kbs')
smkx_key_map[defines.GLFW_KEY_TAB] = b'\t'
SHIFTED_KEYS = {
defines.GLFW_KEY_TAB: key_as_bytes('kcbt'),
@@ -36,26 +39,65 @@ SHIFTED_KEYS = {
defines.GLFW_KEY_RIGHT: key_as_bytes('kRIT'),
}
control_codes = {k: (1 + i,) for i, k in enumerate(range(defines.GLFW_KEY_A, defines.GLFW_KEY_RIGHT_BRACKET + 1))}
control_codes[defines.GLFW_KEY_UP] = bytearray(key_as_bytes('cuu1').replace(b'[', b'[1;5'))
control_codes[defines.GLFW_KEY_DOWN] = bytearray(key_as_bytes('cud1').replace(b'[', b'[1;5'))
control_codes[defines.GLFW_KEY_LEFT] = bytearray(key_as_bytes('cub1').replace(b'[', b'[1;5'))
control_codes[defines.GLFW_KEY_RIGHT] = bytearray(key_as_bytes('cuf1').replace(b'[', b'[1;5'))
control_codes[defines.GLFW_KEY_HOME] = bytearray(key_as_bytes('khome').replace(b'O', b'[1;5'))
control_codes[defines.GLFW_KEY_END] = bytearray(key_as_bytes('kend').replace(b'O', b'[1;5'))
control_codes[defines.GLFW_KEY_PAGE_UP] = bytearray(key_as_bytes('kpp').replace(b'~', b';5~'))
control_codes[defines.GLFW_KEY_PAGE_DOWN] = bytearray(key_as_bytes('knp').replace(b'~', b';5~'))
control_codes[defines.GLFW_KEY_DELETE] = bytearray(key_as_bytes('kdch1').replace(b'~', b';5~'))
alt_codes = {k: (0x1b, k) for i, k in enumerate(range(defines.GLFW_KEY_SPACE, defines.GLFW_KEY_RIGHT_BRACKET + 1))}
control_codes = {
k: (1 + i, )
for i, k in
enumerate(range(defines.GLFW_KEY_A, defines.GLFW_KEY_RIGHT_BRACKET + 1))
}
def rkey(name, a, b):
return bytearray(key_as_bytes(name).replace(a, b))
control_codes[defines.GLFW_KEY_PAGE_UP] = rkey('kpp', b'~', b';5~')
control_codes[defines.GLFW_KEY_PAGE_DOWN] = rkey('knp', b'~', b';5~')
control_codes[defines.GLFW_KEY_DELETE] = rkey('kdch1', b'~', b';5~')
alt_codes = {
k: (0x1b, k)
for i, k in enumerate(
range(defines.GLFW_KEY_SPACE, defines.GLFW_KEY_RIGHT_BRACKET + 1)
)
}
rmkx_key_map = smkx_key_map.copy()
rmkx_key_map.update({
defines.GLFW_KEY_UP: b'\033[A',
defines.GLFW_KEY_DOWN: b'\033[B',
defines.GLFW_KEY_LEFT: b'\033[D',
defines.GLFW_KEY_RIGHT: b'\033[C',
defines.GLFW_KEY_HOME: b'\033[H',
defines.GLFW_KEY_END: b'\033[F',
})
for sk in 'UP DOWN LEFT RIGHT HOME END'.split():
sk = getattr(defines, 'GLFW_KEY_' + sk)
control_codes[sk] = rmkx_key_map[sk].replace(b'[', b'[1;5')
cursor_key_mode_map = {True: smkx_key_map, False: rmkx_key_map}
def get_key_map(screen):
return cursor_key_mode_map[screen.cursor_key_mode]
valid_localized_key_names = {
k: getattr(defines, 'GLFW_KEY_' + k) for k in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
k: getattr(defines, 'GLFW_KEY_' + k)
for k in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
}
for name, ch in {
'APOSTROPHE': "'", 'COMMA': ',', 'PERIOD': '.', 'SLASH': '/', 'MINUS': '-', 'SEMICOLON': ';', 'EQUAL': '=',
'LEFT_BRACKET': '[', 'RIGHT_BRACKET': ']', 'GRAVE_ACCENT': '`', 'BACKSLASH': '\\'}.items():
'APOSTROPHE': "'",
'COMMA': ',',
'PERIOD': '.',
'SLASH': '/',
'MINUS': '-',
'SEMICOLON': ';',
'EQUAL': '=',
'LEFT_BRACKET': '[',
'RIGHT_BRACKET': ']',
'GRAVE_ACCENT': '`',
'BACKSLASH': '\\'
}.items():
valid_localized_key_names[ch] = getattr(defines, 'GLFW_KEY_' + name)
@@ -64,21 +106,53 @@ def get_localized_key(key, scancode):
return valid_localized_key_names.get((name or '').upper(), key)
def interpret_key_event(key, scancode, mods):
data = bytearray()
action_map = {
defines.GLFW_PRESS: b'p',
defines.GLFW_RELEASE: b'r',
defines.GLFW_REPEAT: b't'
}
def extended_key_event(key, scancode, mods, action):
if key >= defines.GLFW_KEY_LAST or key == defines.GLFW_KEY_UNKNOWN or (
# Shifted printable key should be handled by interpret_text_event()
mods == defines.GLFW_MOD_SHIFT and 32 <= key <= 126
):
return b''
if mods == 0 and key in (
defines.GLFW_KEY_BACKSPACE, defines.GLFW_KEY_ENTER
):
return smkx_key_map[key]
name = KEY_MAP.get(key)
if name is None:
return b''
return '\033_K{}{}{}\033\\'.format(
action_map[action], base64_encode(mods), name
).encode('ascii')
def interpret_key_event(key, scancode, mods, window, action):
screen = window.screen
key = get_localized_key(key, scancode)
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 == defines.GLFW_MOD_ALT and key in alt_codes:
# Map Alt+key to Esc-key
data.extend(alt_codes[key])
else:
x = key_map.get(key)
if x is not None:
if mods == defines.GLFW_MOD_SHIFT:
x = SHIFTED_KEYS.get(key, x)
data.extend(x)
if screen.extended_keyboard:
return extended_key_event(key, scancode, mods, action)
data = bytearray()
if action == defines.GLFW_PRESS or (
action == defines.GLFW_REPEAT and screen.auto_repeat_enabled
):
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 == defines.GLFW_MOD_ALT and key in alt_codes:
# Map Alt+key to Esc-key
data.extend(alt_codes[key])
else:
key_map = get_key_map(screen)
x = key_map.get(key)
if x is not None:
if mods == defines.GLFW_MOD_SHIFT:
x = SHIFTED_KEYS.get(key, x)
data.extend(x)
return bytes(data)

View File

@@ -6,7 +6,7 @@
# that have many weight variants like Book, Medium, Thick, etc. For example:
# font_family Operator Mono Book
# bold_font Operator Mono Thick
# bold_talic_font Operator Mono Medium
# bold_italic_font Operator Mono Medium
font_family monospace
italic_font auto
bold_font auto
@@ -87,6 +87,13 @@ initial_window_height 400
# that sufficient for most uses.
repaint_delay 10
# Visual bell duration. Flash the screen when a bell occurs for the specified number of
# seconds. Set to zero to disable.
visual_bell_duration 0.0
# Enable/disable the audio bell. Useful in environments that require silence.
enable_audio_bell yes
# The modifier keys to press when clicking with the mouse on URLs to open the URL
open_url_modifiers ctrl+shift
@@ -207,3 +214,18 @@ map ctrl+shift+q close_tab
map ctrl+shift+l next_layout
map ctrl+shift+. move_tab_forward
map ctrl+shift+, move_tab_backward
# Symbol mapping (special font for specified unicode code points). Map the
# specified unicode codepoints to a particular font. Useful if you need special
# rendering for some symbols, such as for Powerline. Avoids the need for
# patched fonts. Each unicode code point is specified in the form U+<code point
# in hexadecimal>. You can specify multiple code points, separated by commas
# and ranges separated by hyphens. symbol_map itself can be specified multiple times.
# Syntax is:
#
# symbol_map codepoints Font Family Name
#
# For example:
#
# symbol_map U+E0A0-U+E0A2,U+E0B0-U+E0B3 PowerlineSymbols

View File

@@ -200,6 +200,14 @@ as_ansi(Line* self) {
return ans;
}
static PyObject*
is_continued(Line* self) {
#define is_continued_doc "Return the line's continued flag"
PyObject *ans = self->continued ? Py_True : Py_False;
Py_INCREF(ans);
return ans;
}
static PyObject*
__repr__(Line* self) {
PyObject *s = as_unicode(self);
@@ -305,7 +313,8 @@ cursor_from(Line* self, PyObject *args) {
return (PyObject*)ans;
}
void line_clear_text(Line *self, unsigned int at, unsigned int num, int ch) {
void
line_clear_text(Line *self, unsigned int at, unsigned int num, int ch) {
const char_type repl = ((char_type)ch & CHAR_MASK) | (1 << ATTRS_SHIFT);
for (index_type i = at; i < MIN(self->xnum, at + num); i++) {
self->chars[i] = (self->chars[i] & ATTRS_MASK_WITHOUT_WIDTH) | repl;
@@ -323,7 +332,8 @@ clear_text(Line* self, PyObject *args) {
Py_RETURN_NONE;
}
void line_apply_cursor(Line *self, Cursor *cursor, unsigned int at, unsigned int num, bool clear_char) {
void
line_apply_cursor(Line *self, Cursor *cursor, unsigned int at, unsigned int num, bool clear_char) {
char_type attrs = CURSOR_TO_ATTRS(cursor, 1);
color_type col = (cursor->fg & COL_MASK) | ((color_type)(cursor->bg & COL_MASK) << COL_SHIFT);
decoration_type dfg = cursor->decoration_fg & COL_MASK;
@@ -390,7 +400,8 @@ left_shift(Line *self, PyObject *args) {
Py_RETURN_NONE;
}
void line_set_char(Line *self, unsigned int at, uint32_t ch, unsigned int width, Cursor *cursor) {
void
line_set_char(Line *self, unsigned int at, uint32_t ch, unsigned int width, Cursor *cursor) {
char_type attrs;
if (cursor == NULL) {
attrs = (((self->chars[at] >> ATTRS_SHIFT) & ~3) | (width & 3)) << ATTRS_SHIFT;
@@ -470,6 +481,7 @@ static PyMethodDef methods[] = {
METHOD(set_attribute, METH_VARARGS)
METHOD(as_base_text, METH_NOARGS)
METHOD(as_ansi, METH_NOARGS)
METHOD(is_continued, METH_NOARGS)
METHOD(width, METH_O)
METHOD(basic_cell_data, METH_O)

View File

@@ -3,49 +3,130 @@
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import argparse
import tempfile
import os
import sys
from queue import Empty
import tempfile
from gettext import gettext as _
from queue import Empty
from .config import load_config, load_cached_values, cached_values, save_cached_values
from .constants import appname, str_version, config_dir, viewport_size, isosx, logo_data_file
from .layout import all_layouts
from .boss import Boss
from .shaders import GL_VERSION
from .fast_data_types import (
glewInit, enable_automatic_opengl_error_checking, glClear, glClearColor,
GL_COLOR_BUFFER_BIT, GLFW_CONTEXT_VERSION_MAJOR, GLFW_STENCIL_BITS,
GLFW_CONTEXT_VERSION_MINOR, GLFW_OPENGL_PROFILE,
GLFW_OPENGL_FORWARD_COMPAT, GLFW_OPENGL_CORE_PROFILE, GLFW_SAMPLES,
glfw_set_error_callback, glfw_init, glfw_terminate, glfw_window_hint,
glfw_swap_interval, glfw_wait_events, Window, change_wcwidth
from .config import (
cached_values, load_cached_values, load_config, save_cached_values
)
from .constants import (
appname, config_dir, isosx, logo_data_file, str_version, viewport_size
)
from .fast_data_types import (
GL_COLOR_BUFFER_BIT, GLFW_CONTEXT_VERSION_MAJOR,
GLFW_CONTEXT_VERSION_MINOR, GLFW_OPENGL_CORE_PROFILE,
GLFW_OPENGL_FORWARD_COMPAT, GLFW_OPENGL_PROFILE, GLFW_SAMPLES,
GLFW_STENCIL_BITS, Window, change_wcwidth,
enable_automatic_opengl_error_checking, glClear, glClearColor, glewInit,
glfw_init, glfw_set_error_callback, glfw_swap_interval, glfw_terminate,
glfw_wait_events, glfw_window_hint
)
from .layout import all_layouts
from .shaders import GL_VERSION
from .utils import safe_print
defconf = os.path.join(config_dir, 'kitty.conf')
def option_parser():
parser = argparse.ArgumentParser(prog=appname, description=_('The {} terminal emulator').format(appname))
defconf = os.path.join(config_dir, 'kitty.conf')
parser = argparse.ArgumentParser(
prog=appname,
description=_('The {} terminal emulator').format(appname)
)
a = parser.add_argument
a('--class', default=appname, dest='cls', help=_('Set the WM_CLASS property'))
a('--config', default=defconf, help=_('Specify a path to the config file to use. Default: {}').format(defconf))
a('--cmd', '-c', default=None, help=_('Run python code in the kitty context'))
a('-d', '--directory', default='.', help=_('Change to the specified directory when launching'))
a('--version', '-v', action='version', version='{} {} by Kovid Goyal'.format(appname, str_version))
a('--profile', action='store_true', default=False, help=_('Show profiling data after exit'))
a('--dump-commands', action='store_true', default=False, help=_('Output commands received from child process to stdout'))
a('--replay-commands', default=None, help=_('Replay previously dumped commands'))
a('--window-layout', default=None, choices=frozenset(all_layouts.keys()), help=_(
'The window layout to use on startup'))
a('--session', default=None, help=_(
'Path to a file containing the startup session (tabs, windows, layout, programs)'))
a('args', nargs=argparse.REMAINDER, help=_(
'The remaining arguments are used to launch a program other than the default shell. Any further options are passed'
' directly to the program being invoked.'
))
a(
'--class',
default=appname,
dest='cls',
help=_('Set the WM_CLASS property')
)
a(
'--config',
action='append',
help=_(
'Specify a path to the config file(s) to use.'
' Can be specified multiple times to read multiple'
' config files in sequence, which are merged. Default: {}'
).format(defconf)
)
a(
'--override',
'-o',
action='append',
help=_(
'Override individual configuration options, can be specified'
' multiple times. Syntax: name=value. For example: {}'
).format('-o font_size=20')
)
a(
'--cmd',
'-c',
default=None,
help=_('Run python code in the kitty context')
)
a(
'-d',
'--directory',
default='.',
help=_('Change to the specified directory when launching')
)
a(
'--version',
'-v',
action='version',
version='{} {} by Kovid Goyal'.format(appname, str_version)
)
a(
'--profile',
action='store_true',
default=False,
help=_('Show profiling data after exit')
)
a(
'--dump-commands',
action='store_true',
default=False,
help=_('Output commands received from child process to stdout')
)
a(
'--replay-commands',
default=None,
help=_('Replay previously dumped commands')
)
a(
'--debug-gl',
action='store_true',
default=False,
help=_('Debug OpenGL commands. This will cause all OpenGL calls'
' to check for errors instead of ignoring them. Useful'
' when debugging rendering problems.')
)
a(
'--window-layout',
default=None,
choices=frozenset(all_layouts.keys()),
help=_('The window layout to use on startup')
)
a(
'--session',
default=None,
help=_(
'Path to a file containing the startup session (tabs, windows, layout, programs)'
)
)
a(
'args',
nargs=argparse.REMAINDER,
help=_(
'The remaining arguments are used to launch a program other than the default shell. Any further options are passed'
' directly to the program being invoked.'
)
)
return parser
@@ -100,8 +181,7 @@ def run_app(opts, args):
else:
viewport_size.width = opts.initial_window_width
viewport_size.height = opts.initial_window_height
window = Window(
viewport_size.width, viewport_size.height, args.cls)
window = Window(viewport_size.width, viewport_size.height, args.cls)
window.set_title(appname)
window.make_context_current()
if not isosx:
@@ -138,7 +218,8 @@ def on_glfw_error(code, msg):
def main():
if os.environ.pop('KITTY_LAUNCHED_BY_LAUNCH_SERVICES', None) == '1' and getattr(sys, 'frozen', True):
if os.environ.pop('KITTY_LAUNCHED_BY_LAUNCH_SERVICES',
None) == '1' and getattr(sys, 'frozen', True):
os.chdir(os.path.expanduser('~'))
args = option_parser().parse_args()
if args.cmd:
@@ -148,10 +229,12 @@ def main():
from kitty.client import main
main(args.replay_commands)
return
opts = load_config(args.config)
config = args.config or (defconf, )
overrides = (a.replace('=', ' ', 1) for a in args.override or ())
opts = load_config(*config, overrides=overrides)
change_wcwidth(not opts.use_system_wcwidth)
glfw_set_error_callback(on_glfw_error)
enable_automatic_opengl_error_checking(False)
enable_automatic_opengl_error_checking(args.debug_gl)
if not glfw_init():
raise SystemExit('GLFW initialization failed')
try:

View File

@@ -75,3 +75,9 @@
#define BRACKETED_PASTE (2004 << 5)
#define BRACKETED_PASTE_START "\033[200~"
#define BRACKETED_PASTE_END "\033[201~"
// Styled underlines
#define STYLED_UNDERLINES (2016 << 5)
// Extended keyboard protocol
#define EXTENDED_KEYBOARD (2017 << 5)

View File

@@ -314,7 +314,8 @@ dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
case '"': \
case '*': \
case '\'': \
case ' ':
case ' ': \
case '$':
static inline void
@@ -472,6 +473,14 @@ dispatch_csi(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
CALL_CSI_HANDLER1(screen_indexn, 1);
case SD:
CALL_CSI_HANDLER1(screen_reverse_indexn, 1);
case DECSTR:
if (end_modifier == '$') {
// DECRQM
CALL_CSI_HANDLER1P(report_mode_status, 0, '?');
} else {
REPORT_ERROR("Unknown DECSTR CSI sequence with start and end modifiers: 0x%x 0x%x", start_modifier, end_modifier);
}
break;
default:
REPORT_ERROR("Unknown CSI code: 0x%x", code);
}

View File

@@ -396,6 +396,7 @@ set_mode_from_const(Screen *self, unsigned int mode, bool val) {
SIMPLE_MODE(IRM)
SIMPLE_MODE(DECARM)
SIMPLE_MODE(BRACKETED_PASTE)
SIMPLE_MODE(EXTENDED_KEYBOARD)
SIMPLE_MODE(FOCUS_TRACKING)
MOUSE_MODE(MOUSE_BUTTON_TRACKING, mouse_tracking_mode, BUTTON_MODE)
MOUSE_MODE(MOUSE_MOTION_TRACKING, mouse_tracking_mode, MOTION_MODE)
@@ -404,10 +405,12 @@ set_mode_from_const(Screen *self, unsigned int mode, bool val) {
MOUSE_MODE(MOUSE_SGR_MODE, mouse_tracking_protocol, SGR_PROTOCOL)
MOUSE_MODE(MOUSE_URXVT_MODE, mouse_tracking_protocol, URXVT_PROTOCOL)
case DECCKM:
case DECSCLM:
case DECNRCM:
break; // we ignore these modes
case DECCKM:
self->modes.mDECCKM = val;
break;
case DECTCEM:
self->modes.mDECTCEM = val;
tracker_cursor_changed(self->change_tracker);
@@ -854,12 +857,9 @@ void screen_erase_characters(Screen *self, unsigned int count) {
void
screen_bell(Screen UNUSED *self) {
FILE *f = fopen("/dev/tty", "w");
static const char *bell = "\007";
if (f != NULL) {
fwrite(bell, 1, 1, f);
fclose(f);
}
PyObject_CallMethod(self->callbacks, "bell", NULL);
if (PyErr_Occurred()) PyErr_Print();
PyErr_Clear();
}
static inline void
@@ -898,6 +898,34 @@ report_device_status(Screen *self, unsigned int which, bool private) {
}
}
void
report_mode_status(Screen *self, unsigned int which, bool private) {
unsigned int q = private ? which << 5 : which;
unsigned int ans = 0;
char buf[50] = {0};
switch(q) {
#define KNOWN_MODE(x) \
case x: \
ans = self->modes.m##x ? 1 : 2; break;
KNOWN_MODE(LNM);
KNOWN_MODE(IRM);
KNOWN_MODE(DECTCEM);
KNOWN_MODE(DECSCNM);
KNOWN_MODE(DECOM);
KNOWN_MODE(DECAWM);
KNOWN_MODE(DECCOLM);
KNOWN_MODE(DECARM);
KNOWN_MODE(DECCKM);
KNOWN_MODE(BRACKETED_PASTE);
KNOWN_MODE(EXTENDED_KEYBOARD);
KNOWN_MODE(FOCUS_TRACKING);
#undef KNOWN_MODE
case STYLED_UNDERLINES:
ans = 3; break;
}
if (snprintf(buf, sizeof(buf) - 1, "\x1b[%s%u;%u$y", (private ? "?" : ""), which, ans)) callback("write_to_child", self, buf, 0);
}
void
screen_set_margins(Screen *self, unsigned int top, unsigned int bottom) {
if (!top) top = 1;
@@ -1044,9 +1072,11 @@ WRAP1B(erase_in_display, 0)
static int name##_set(Screen *self, PyObject *val, void UNUSED *closure) { if (val == NULL) { PyErr_SetString(PyExc_TypeError, "Cannot delete attribute"); return -1; } set_mode_from_const(self, uname, PyObject_IsTrue(val) ? true : false); return 0; }
MODE_GETSET(in_bracketed_paste_mode, BRACKETED_PASTE)
MODE_GETSET(extended_keyboard, EXTENDED_KEYBOARD)
MODE_GETSET(focus_tracking_enabled, FOCUS_TRACKING)
MODE_GETSET(auto_repeat_enabled, DECARM)
MODE_GETSET(cursor_visible, DECTCEM)
MODE_GETSET(cursor_key_mode, DECCKM)
static PyObject*
mouse_tracking_mode(Screen *self) {
@@ -1247,9 +1277,11 @@ static PyMethodDef methods[] = {
static PyGetSetDef getsetters[] = {
GETSET(in_bracketed_paste_mode)
GETSET(extended_keyboard)
GETSET(auto_repeat_enabled)
GETSET(focus_tracking_enabled)
GETSET(cursor_visible)
GETSET(cursor_key_mode)
{NULL} /* Sentinel */
};

View File

@@ -157,6 +157,7 @@ class Sprites:
return glGenBuffers(1)
def set_sprite_map(self, buf_id, data, usage=GL_STREAM_DRAW):
self.bind_sprite_map(buf_id)
glNamedBufferData(buf_id, sizeof(data), addressof(data), usage)
def bind_sprite_map(self, buf_id):

View File

@@ -12,7 +12,7 @@ from .config import build_ansi_color_table
from .constants import get_boss, appname, shell_path, cell_size, queue_action, viewport_size, WindowGeometry, GLuint
from .fast_data_types import glfw_post_empty_event, Screen, DECAWM, DATA_CELL_SIZE, ColorProfile
from .char_grid import calculate_gl_geometry, render_cells
from .layout import all_layouts
from .layout import all_layouts, Rect
from .utils import color_as_int
from .borders import Borders
from .window import Window
@@ -79,7 +79,9 @@ class Tab:
self.relayout_borders()
def relayout_borders(self):
self.borders(self.windows, self.active_window, self.current_layout, self.current_layout.needs_window_borders and len(self.windows) > 1)
tm = get_boss().tab_manager
self.borders(self.windows, self.active_window, self.current_layout, tm.blank_rects,
self.current_layout.needs_window_borders and len(self.windows) > 1)
def next_layout(self):
if len(self.opts.enabled_layouts) > 1:
@@ -200,15 +202,16 @@ class TabManager:
def __init__(self, opts, args, startup_session):
self.opts, self.args = opts, args
self.buffer_id = None
self.tabbar_lock = Lock()
self.tabs = [Tab(opts, args, self.title_changed, t) for t in startup_session.tabs]
self.cell_ranges = []
self.active_tab_idx = startup_session.active_tab_idx
self.tabbar_lock = Lock()
self.tabbar_dirty = True
self.color_profile = ColorProfile()
self.color_profile.update_ansi_color_table(build_ansi_color_table(opts))
self.default_fg = color_as_int(opts.inactive_tab_foreground)
self.default_bg = color_as_int(opts.inactive_tab_background)
self.tab_bar_blank_rects = ()
def as_rgb(x):
return (x << 8) | 2
@@ -216,12 +219,16 @@ class TabManager:
self.active_bg = as_rgb(color_as_int(opts.active_tab_background))
self.active_fg = as_rgb(color_as_int(opts.active_tab_foreground))
self.can_render = False
if len(self.tabs) > 1:
self.layout_tab_bar()
def resize(self, only_tabs=False):
if not only_tabs:
self.layout_tab_bar()
for tab in self.tabs:
tab.relayout()
if only_tabs:
return
def layout_tab_bar(self):
self.can_render = False
ncells = viewport_size.width // cell_size.width
s = Screen(None, 1, ncells)
@@ -232,9 +239,13 @@ class TabManager:
self.tab_bar_screen = s
self.tabbar_dirty = True
margin = (viewport_size.width - ncells * cell_size.width) // 2
self.window_geometry = WindowGeometry(
self.window_geometry = g = WindowGeometry(
margin, viewport_size.height - cell_size.height, viewport_size.width - margin, viewport_size.height, s.columns, s.lines)
self.screen_geometry = calculate_gl_geometry(self.window_geometry)
if margin > 0:
self.tab_bar_blank_rects = (Rect(0, g.top, g.left, g.bottom), Rect(g.right - 1, g.top, viewport_size.width, g.bottom))
else:
self.tab_bar_blank_rects = ()
self.screen_geometry = calculate_gl_geometry(g)
self.screen = s
self.can_render = True
@@ -254,10 +265,6 @@ class TabManager:
def __len__(self):
return len(self.tabs)
def new_tab(self, special_window=None):
self.active_tab_idx = len(self.tabs)
self.tabs.append(Tab(self.opts, self.args, self.title_changed, special_window=special_window))
@property
def active_tab(self):
return self.tabs[self.active_tab_idx] if self.tabs else None
@@ -278,6 +285,16 @@ class TabManager:
with self.tabbar_lock:
self.tabbar_dirty = True
def new_tab(self, special_window=None):
' Must be called in the GUI thread '
needs_resize = len(self.tabs) == 1
self.active_tab_idx = len(self.tabs)
self.tabs.append(Tab(self.opts, self.args, self.title_changed, special_window=special_window))
if needs_resize:
if not self.can_render:
queue_action(self.layout_tab_bar)
queue_action(get_boss().tabbar_visibility_changed)
def remove(self, tab):
' Must be called in the GUI thread '
needs_resize = len(self.tabs) == 2
@@ -328,6 +345,12 @@ class TabManager:
queue_action(self.set_active_tab, i)
return
@property
def blank_rects(self):
if len(self.tabs) < 2:
return ()
return self.tab_bar_blank_rects
def render(self, cell_program, sprites):
if not self.can_render or len(self.tabs) < 2:
return

View File

@@ -92,15 +92,15 @@ string_capabilities = {
# Make cursor appear normal
'cnorm': r'\E[?12l\E[?25h',
# Carriage return
'cr': r'^M',
'cr': r'^M', # CR (carriage return \r)
# Change scroll region
'csr': r'\E[%i%p1%d;%p2%dr',
# Move cursor to the left by the specified amount
'cub': r'\E[%p1%dD',
'cub1': r'^H',
'cub1': r'^H', # BS (backspace)
# Move cursor down specified number of lines
'cud': r'\E[%p1%dB',
'cud1': r'^J',
'cud1': r'^J', # LF (line-feed \n)
# Move cursor to the right by the specified amount
'cuf': r'\E[%p1%dC',
'cuf1': r'\E[C',
@@ -203,6 +203,8 @@ string_capabilities = {
'rmcup': r'\E[?1049l',
# Exit insert mode
'rmir': r'\E[4l',
# Exit application keypad mode
'rmkx': r'\E[?1l',
# Exit standout mode
'rmso': r'\E[27m',
# Exit underline mode
@@ -227,6 +229,8 @@ string_capabilities = {
'smcup': r'\E[?1049h',
# Enster insert mode
'smir': r'\E[4h',
# Enter application keymap mode
'smkx': r'\E[?1h',
# Enter standout mode
'smso': r'\E[7m',
# Enter underline mode
@@ -373,6 +377,8 @@ termcap_aliases.update({
'ZR': 'ritm',
'as': 'smacs',
'ae': 'rmacs',
'ks': 'smkx',
'ke': 'rmkx',
'#2': 'kHOM',
'#4': 'kLFT',
'*7': 'kEND',

View File

@@ -2,12 +2,13 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import re
import os
import signal
import shlex
import subprocess
import math
import os
import re
import shlex
import signal
import string
import subprocess
from collections import namedtuple
from contextlib import contextmanager
from functools import lru_cache
@@ -56,7 +57,9 @@ def get_logical_dpi():
get_logical_dpi.ans = glfw_get_physical_dpi()
else:
raw = subprocess.check_output(['xdpyinfo']).decode('utf-8')
m = re.search(r'^\s*resolution:\s*(\d+)+x(\d+)', raw, flags=re.MULTILINE)
m = re.search(
r'^\s*resolution:\s*(\d+)+x(\d+)', raw, flags=re.MULTILINE
)
get_logical_dpi.ans = int(m.group(1)), int(m.group(2))
return get_logical_dpi.ans
@@ -67,11 +70,13 @@ def get_dpi():
get_dpi.ans = {'physical': pdpi, 'logical': get_logical_dpi()}
return get_dpi.ans
# Color names {{{
color_pat = re.compile(r'^#([a-fA-F0-9]{3}|[a-fA-F0-9]{6})$')
color_pat2 = re.compile(r'rgb:([a-f0-9]{2})/([a-f0-9]{2})/([a-f0-9]{2})$', re.IGNORECASE)
color_pat2 = re.compile(
r'rgb:([a-f0-9]{2})/([a-f0-9]{2})/([a-f0-9]{2})$', re.IGNORECASE
)
color_names = {
'aliceblue': 'f0f8ff',
@@ -223,6 +228,7 @@ color_names = {
'yellowgreen': '9acd32',
}
Color = namedtuple('Color', 'red green blue')
# }}}
@@ -295,6 +301,20 @@ def get_primary_selection():
return subprocess.check_output(['xsel', '-p']).decode('utf-8')
def base64_encode(
integer,
chars=string.ascii_uppercase + string.ascii_lowercase + string.digits +
'+/'
):
ans = ''
while True:
integer, remainder = divmod(integer, 64)
ans = chars[remainder] + ans
if integer == 0:
break
return ans
def set_primary_selection(text):
if isosx:
return # There is no primary selection on OS X

View File

@@ -17,7 +17,7 @@ from .fast_data_types import (
GLFW_MOUSE_BUTTON_5, ANY_MODE, MOTION_MODE, GLFW_KEY_LEFT_SHIFT,
GLFW_KEY_RIGHT_SHIFT, GLFW_KEY_UP, GLFW_KEY_DOWN, GLFW_MOUSE_BUTTON_4
)
from .keys import key_map
from .keys import get_key_map
from .mouse import encode_mouse_event, PRESS, RELEASE, MOVE, DRAG
from .terminfo import get_capabilities
from .utils import sanitize_title, get_primary_selection, parse_color_set, safe_print
@@ -37,6 +37,7 @@ class Window:
self._is_visible_in_layout = True
self.child, self.opts = child, opts
self.child_fd = child.child_fd
self.start_visual_bell_at = None
self.screen = Screen(self, 24, 80, opts.scrollback_lines)
self.read_bytes = partial(read_bytes_dump, self.dump_commands) if args.dump_commands else read_bytes
self.draw_dump_buf = []
@@ -106,6 +107,17 @@ class Window:
self.write_buf = memoryview(self.write_buf.tobytes() + data)
wakeup()
def bell(self):
if self.opts.enable_audio_bell:
try:
with open('/dev/tty', 'wb') as f:
f.write(b'\007')
except EnvironmentError:
pass # failure to beep is not critical
if self.opts.visual_bell_duration > 0:
self.start_visual_bell_at = monotonic()
glfw_post_empty_event()
def update_screen(self):
self.char_grid.update_cell_data()
glfw_post_empty_event()
@@ -254,12 +266,20 @@ class Window:
if ev:
self.write_to_child(ev)
else:
k = key_map[GLFW_KEY_UP if upwards else GLFW_KEY_DOWN]
k = get_key_map(self.screen)[GLFW_KEY_UP if upwards else GLFW_KEY_DOWN]
self.write_to_child(k * abs(s))
def buf_toggled(self, is_main_linebuf):
self.char_grid.scroll('full', False)
def render_cells(self, render_data, program, sprites):
invert_colors = False
if self.start_visual_bell_at is not None:
invert_colors = monotonic() - self.start_visual_bell_at <= self.opts.visual_bell_duration
if not invert_colors:
self.start_visual_bell_at = None
self.char_grid.render_cells(render_data, program, sprites, invert_colors)
# actions {{{
def show_scrollback(self):

View File

@@ -132,6 +132,18 @@ class TestParser(BaseTest):
c.clear()
pb('\033[6n', ('report_device_status', 6, 0))
self.ae(c.wtcbuf, b'\033[2;1R')
c.clear()
s.cursor_key_mode = True
pb('\033[?1$p', ('report_mode_status', 1, 1))
self.ae(c.wtcbuf, b'\033[?1;1$y')
pb('\033[?1l', ('screen_reset_mode', 1, 1))
self.assertFalse(s.cursor_key_mode)
c.clear()
pb('\033[?2016$p', ('report_mode_status', 2016, 1))
self.ae(c.wtcbuf, b'\033[?2016;3$y')
c.clear()
pb('\033[?1$p', ('report_mode_status', 1, 1))
self.ae(c.wtcbuf, b'\033[?1;2$y')
pb('\033[2;4r', ('screen_set_margins', 2, 4))
self.ae(s.margin_top, 1), self.ae(s.margin_bottom, 3)
pb('\033[r', ('screen_set_margins', 0, 0))

View File

@@ -2,14 +2,15 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import json
import os
import re
import subprocess
from collections import defaultdict
base = os.path.dirname(os.path.abspath(__file__))
os.chdir(base)
defns = defaultdict(list)
for line in open('kitty/kitty.conf'):
@@ -17,13 +18,35 @@ for line in open('kitty/kitty.conf'):
_, sc, name = line.split(maxsplit=3)
defns[name].append('`' + sc + '`')
defns = [':sc_{}: pass:quotes[{}]'.format(name, ' or '.join(defns[name])) for name in sorted(defns)]
defns = [
':sc_{}: pass:quotes[{}]'.format(name, ' or '.join(defns[name]))
for name in sorted(defns)
]
defns = '\n'.join(defns)
raw = open('README.asciidoc').read()
pat = re.compile(r'^// START_SHORTCUT_BLOCK$.+?^// END_SHORTCUT_BLOCK$', re.M | re.DOTALL)
nraw = pat.sub('// START_SHORTCUT_BLOCK\n' +
defns + '\n// END_SHORTCUT_BLOCK', raw)
pat = re.compile(
r'^// START_SHORTCUT_BLOCK$.+?^// END_SHORTCUT_BLOCK$', re.M | re.DOTALL
)
nraw = pat.sub(
'// START_SHORTCUT_BLOCK\n' + defns + '\n// END_SHORTCUT_BLOCK', raw
)
if raw != nraw:
print('Updating shortcuts block')
open('README.asciidoc', 'w').write(nraw)
raw = subprocess.check_output([
'kitty', '-c',
'from kitty.key_encoding import *; import json; print(json.dumps(ENCODING))'
]).decode('utf-8')
key_map = json.loads(raw)
lines = [
'See link:protocol-extensions.asciidoc#keyboard-handling[Keyboard Handling protocol extension]',
'', '|===', '| Name | Encoded representation (base64)', ''
]
for k in sorted(key_map):
lines.append('| {:15s} | `{}`'.format(k.replace('_', ' '), key_map[k].replace('`', '\\`')))
lines += ['', '|===']
with open('key_encoding.asciidoc', 'w') as f:
print('= Key encoding for extended keyboard protocol\n', file=f)
print('\n'.join(lines), file=f)

View File

@@ -5,7 +5,19 @@
kitty has a few extensions to the xterm protocol, to enable advanced features.
These are typically in the form of new or re-purposed escape codes. While these
extensions are currently kitty specific, it would be nice to get some of them
adopted more broadly.
adopted more broadly, to push the state of terminal emulators forward.
The goal of these extensions is to be as small an unobtrusive as possible,
while filling in some gaps in the existing xterm protocol. In particular, one
of the goals of this specification is explicitly not to "re-imagine" the tty.
The tty should remain what it is -- a device for efficiently processing text
received as a simple byte stream. Another objective is to only move the minimum
possible amount of extra functionality into the terminal program itself. This
is to make it as easy to implement these protocol extensions as possible,
thereby hopefully encouraging their widespread adoption.
If you wish to discuss these extensions, propose additions/changes to them
please do so by opening issues in the github bug tracker.
toc::[]
@@ -16,9 +28,6 @@ in terminal editors such as vim and emacs to display red, wavy underlines under
mis-spelled words and/or syntax errors. This is done by re-purposing some SGR escape codes
that are not used in modern terminals (https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_codes)
Setting the width and/or height to zero means that no drawing is done and the
cursor position remains unchanged.
To change the underline style from straight line to curl (this used to be the
code for rapid blinking text, only previous use I know of was in MS-DOS ANSI.sys):
@@ -41,6 +50,23 @@ To reset the underline color (also previously reserved and unused):
<ESC>[59m
```
A client can query if the terminal emulator supports this underline extension using the
standard link:http://vt100.net/docs/vt510-rm/DECRQM[DECRQM] escape sequence, with the
mode value `2016`, like this:
Client sends:
```
<ESC>[?2016$p
```
If the terminal supports this underline extension, it must respond with
link:http://vt100.net/docs/vt510-rm/DECRPM[DECRPM]
```
<ESC>[?2016;3$p
```
== Graphics rendering
@@ -54,6 +80,8 @@ emulator. The major design goals are
the existing cursor based protocols.
* Should use optimizations when the client is running on the same computer as the terminal emulator.
For some discussion regarding the design choices, see link:../../issues/33[#33].
=== Getting the window size
In order to know what size of images to display and how to position them, the client must be able to get the
@@ -296,3 +324,82 @@ the file repeatedly with no overhead.
| y | _y-offset_ -- the row in the pixel data to start from (0-based)
|===
== Keyboard handling
There are various problems with the current state of keyboard handling. They
include:
* No way to use modifiers other than `Ctrl` and `Alt`
* No way to handle different types of keyboard events, such as press, release or repeat
* No reliable way to distinguish single `Esc` keypresses from the
start of a escape sequence. Currently, client programs use
fragile timing related hacks for this, leading to bugs, for example:
link:https://github.com/neovim/neovim/issues/2035[neovim #2035]
There are already two distinct keyboard handling modes, _normal mode_ and
_application mode_. These modes generate different escape sequences for the
various special keys (arrow keys, function keys, home/end etc.) Most terminals
start out in normal mode, however, most shell programs like `bash` switch them to
application mode. We propose adding a third mode, named _full mode_ that addresses
the shortcomings listed above.
Switching to the new _full mode_ is accomplished using the standard private
mode DECSET escape sequence
```
<ESC>[?2017h
```
and to leave _full mode_, use DECRST
```
<ESC>[?2017l
```
The number `2017` above is not used for any existing modes, as far as I know.
Client programs can query if the terminal emulator is in _full mode_ by using
the standard link:http://vt100.net/docs/vt510-rm/DECRQM[DECRQM] escape sequence.
The new mode works as follows:
* All printable key presses without modifier keys are sent just as in the
_normal mode_. This means all printable ASCII characters and in addition,
`Enter`, `Space` and `Backspace`. Also any unicode characters generated by
platform specific extended input modes, such as using the `AltGr` key. This
is done so that client programs that are not aware of this mode can still
handle basic text entry, so if a _full mode_ using program crashes and does
not reset, the user can still issue a `reset` command in the shell to restore
normal key handling. Note that this includes pressing the `Shift` modifier
and printable keys.
* For non printable keys and key combinations including one or more modifiers,
an escape sequence encoding the key event is sent. For details on the
escape sequence, see below.
The escape sequence encodes the following properties:
* Type of event: `press,repeat,release`
* Modifiers pressed at the time of the event
* The actual key being pressed
```
<ESC>_K<type><modifiers><key><ESC>\
```
Where `<type>` is one of `p` -- press, `r` -- release and `t` -- repeat.
Modifiers is a bitmask represented as a single base64 digit. Shift -- `0x1`,
Control -- `0x2`, Alt -- `0x4` and Super -- `0x8`. `<key>` is a number
(encoded in base85) corresponding to the key pressed. The key name to number
mapping is defined in link:key_encoding.asciidoc[this table].
For example:
```
<ESC>_KpGp<ESC>\ is <Ctrl>+<Alt>+x (press)
<ESC>_KrP8<ESC>\ is <Ctrl>+<Alt>+<Shift>+<Super>+PageUp (release)
```
This encoding means each key event is represented by 8 or 9 printable ascii
only bytes, for maximum robustness.

View File

@@ -65,8 +65,10 @@ class ReadFileWithProgressReporting(io.BufferedReader): # {{{
return data
def report_progress(self, size):
sys.stdout.buffer.write(b'\x1b[s')
sys.stdout.buffer.write(b'\x1b[K')
def write(*args):
print(*args, end='')
write('\x1b[s\x1b[K')
frac = float(self.tell()) / self._total
mb_pos = self.tell() / float(1024**2)
mb_tot = self._total / float(1024**2)
@@ -75,14 +77,13 @@ class ReadFileWithProgressReporting(io.BufferedReader): # {{{
bit_rate = kb_rate * 1024
eta = int((self._total - self.tell()) / bit_rate) + 1
eta_m, eta_s = eta / 60, eta % 60
sys.stdout.write(
write(
' %.1f%% %.1f/%.1fMB %.1f KB/sec %d minutes, %d seconds left'
% (frac * 100, mb_pos, mb_tot, kb_rate, eta_m, eta_s))
sys.stdout.buffer.write(b'\x1b[u')
write('\x1b[u')
if self.tell() >= self._total:
sys.stdout.write('\n')
t = int(time.time() - self.start_time) + 1
print('Upload took %d minutes and %d seconds at %.1f KB/sec' %
print('\nUpload took %d minutes and %d seconds at %.1f KB/sec' %
(t / 60, t % 60, kb_rate))
sys.stdout.flush()
@@ -202,7 +203,7 @@ class GitHub(Base): # {{{
def fail(self, r, msg):
print(msg, ' Status Code: %s' % r.status_code, file=sys.stderr)
print("JSON from response:", file=sys.stderr)
pprint(dict(r.json()), stream=sys.stderr)
pprint.pprint(dict(r.json()), stream=sys.stderr)
raise SystemExit(1)
def already_exists(self, r):
@@ -253,7 +254,7 @@ class GitHub(Base): # {{{
def get_github_data():
with open(os.path.expanduser('~/work/env/private/github')) as f:
with open(os.environ['PENV'] + '/github') as f:
un, pw = f.read().strip().split(':')
return {'username': un, 'password': pw}
@@ -263,14 +264,13 @@ def run_upload(args):
os.path.join(build_path, 'build', f.format(version)): desc
for f, desc in {
'osx/dist/kitty-{}.dmg': 'macOS dmg',
}
}.items()
}
for f in files:
if not os.path.exists(f):
raise SystemExit('The installer {} does not exist'.format(f))
gd = get_github_data()
gh = GitHub(files, appname, version, 'kovidgoyal', gd['username'],
gd['password'])
gh = GitHub(files, appname, version, gd['username'], gd['password'])
gh()

View File

@@ -1,3 +1,14 @@
[flake8]
max-line-length = 160
exclude==template.py,linux-package
[yapf]
based_on_style = pep8
split_penalty_import_names = 100
dedent_closing_brackets = True
coalesce_brackets = True
blank_line_before_nested_class_or_def = True
[isort]
combine_as_imports = True
multi_line_output = 5

166
setup.py
View File

@@ -17,18 +17,32 @@ constants = os.path.join(base, 'kitty', 'constants.py')
with open(constants, 'rb') as f:
constants = f.read().decode('utf-8')
appname = re.search(r"^appname = '([^']+)'", constants, re.MULTILINE).group(1)
version = tuple(map(int, re.search(r"^version = \((\d+), (\d+), (\d+)\)", constants, re.MULTILINE).group(1, 2, 3)))
version = tuple(
map(
int,
re.search(
r"^version = \((\d+), (\d+), (\d+)\)", constants, re.MULTILINE
).group(1, 2, 3)
)
)
_plat = sys.platform.lower()
isosx = 'darwin' in _plat
is_travis = os.environ.get('TRAVIS') == 'true'
cflags = ldflags = cc = ldpaths = None
PKGCONFIG = os.environ.get('PKGCONFIG_EXE', 'pkg-config')
def pkg_config(pkg, *args):
return list(filter(None, shlex.split(subprocess.check_output([PKGCONFIG, pkg] + list(args)).decode('utf-8'))))
return list(
filter(
None,
shlex.split(
subprocess.check_output([PKGCONFIG, pkg] + list(args))
.decode('utf-8')
)
)
)
def cc_version():
@@ -43,7 +57,9 @@ def cc_version():
def get_python_flags(cflags):
cflags.extend('-I' + sysconfig.get_path(x) for x in 'include platinclude'.split())
cflags.extend(
'-I' + sysconfig.get_path(x) for x in 'include platinclude'.split()
)
libs = []
libs += sysconfig.get_config_var('LIBS').split()
libs += sysconfig.get_config_var('SYSLIBS').split()
@@ -53,21 +69,28 @@ def get_python_flags(cflags):
val = sysconfig.get_path(var)
if val and '/{}.framework'.format(fw) in val:
fdir = val[:val.index('/{}.framework'.format(fw))]
if os.path.isdir(os.path.join(fdir, '{}.framework'.format(fw))):
if os.path.isdir(
os.path.join(fdir, '{}.framework'.format(fw))
):
framework_dir = fdir
break
else:
raise SystemExit('Failed to find Python framework')
libs.append(os.path.join(framework_dir, sysconfig.get_config_var('LDLIBRARY')))
libs.append(
os.path.join(framework_dir, sysconfig.get_config_var('LDLIBRARY'))
)
else:
libs += ['-L' + sysconfig.get_config_var('LIBDIR')]
libs += ['-lpython' + sysconfig.get_config_var('VERSION') + sys.abiflags]
libs += [
'-lpython' + sysconfig.get_config_var('VERSION') + sys.abiflags
]
libs += sysconfig.get_config_var('LINKFORSHARED').split()
return libs
def init_env(debug=False, asan=False):
def init_env(debug=False, asan=False, native_optimizations=True):
global cflags, ldflags, cc, ldpaths
native_optimizations = native_optimizations and not asan and not debug
ccver = cc_version()
stack_protector = '-fstack-protector'
if ccver >= (4, 9):
@@ -81,44 +104,58 @@ def init_env(debug=False, asan=False):
optimize = '-ggdb'
if asan:
optimize += ' -fsanitize=address -fno-omit-frame-pointer'
cflags = os.environ.get('OVERRIDE_CFLAGS', (
'-Wextra -Wno-missing-field-initializers -Wall -std=c99 -D_XOPEN_SOURCE=700'
' -pedantic-errors -Werror {} -DNDEBUG -fwrapv {} {} -pipe').format(optimize, stack_protector, missing_braces))
cflags = shlex.split(cflags) + shlex.split(sysconfig.get_config_var('CCSHARED'))
ldflags = os.environ.get('OVERRIDE_LDFLAGS', '-Wall ' + (
'-fsanitize=address' if asan else ('' if debug else '-O3')))
cflags = os.environ.get(
'OVERRIDE_CFLAGS', (
'-Wextra -Wno-missing-field-initializers -Wall -std=c99 -D_XOPEN_SOURCE=700'
' -pedantic-errors -Werror {} -DNDEBUG -fwrapv {} {} -pipe {}'
).format(
optimize, stack_protector, missing_braces, '-march=native'
if native_optimizations else ''
)
)
cflags = shlex.split(cflags
) + shlex.split(sysconfig.get_config_var('CCSHARED'))
ldflags = os.environ.get(
'OVERRIDE_LDFLAGS', '-Wall ' +
('-fsanitize=address' if asan else ('' if debug else '-O3'))
)
ldflags = shlex.split(ldflags)
cflags += shlex.split(os.environ.get('CFLAGS', ''))
ldflags += shlex.split(os.environ.get('LDFLAGS', ''))
cflags.append('-pthread')
if not is_travis and not isosx and subprocess.Popen([PKGCONFIG, 'glew', '--atleast-version=2']).wait() != 0:
if not is_travis and not isosx and subprocess.Popen(
[PKGCONFIG, 'glew', '--atleast-version=2']
).wait() != 0:
try:
ver = subprocess.check_output([PKGCONFIG, 'glew', '--modversion']).decode('utf-8').strip()
ver = subprocess.check_output([PKGCONFIG, 'glew', '--modversion']
).decode('utf-8').strip()
major = int(re.match(r'\d+', ver).group())
except Exception:
ver = 'not found'
major = 0
if major < 2:
raise SystemExit('glew >= 2.0.0 is required, found version: ' + ver)
raise SystemExit(
'glew >= 2.0.0 is required, found version: ' + ver
)
if not isosx:
cflags.extend(pkg_config('glew', '--cflags-only-I'))
if isosx:
font_libs = ['-framework', 'CoreText', '-framework', 'CoreGraphics']
else:
cflags.extend(pkg_config('freetype2', '--cflags-only-I'))
font_libs = pkg_config('freetype2', '--libs')
cflags.extend(pkg_config('fontconfig', '--cflags-only-I'))
font_libs = pkg_config('fontconfig', '--libs')
cflags.extend(pkg_config('glfw3', '--cflags-only-I'))
ldflags.append('-shared')
pylib = get_python_flags(cflags)
if isosx:
glfw_ldflags = pkg_config('--libs', '--static', 'glfw3') + ['-framework', 'OpenGL']
glfw_ldflags = pkg_config('--libs', '--static', 'glfw3'
) + ['-framework', 'OpenGL']
glew_libs = []
else:
glfw_ldflags = pkg_config('glfw3', '--libs')
glew_libs = pkg_config('glew', '--libs')
ldpaths = pylib + \
glew_libs + font_libs + glfw_ldflags
ldpaths = pylib + glew_libs + font_libs + glfw_ldflags
try:
os.mkdir(build_dir)
@@ -158,7 +195,10 @@ def newer(dest, *sources):
def compile_c_extension(module, incremental, sources, headers):
prefix = os.path.basename(module)
objects = [os.path.join(build_dir, prefix + '-' + os.path.basename(src) + '.o') for src in sources]
objects = [
os.path.join(build_dir, prefix + '-' + os.path.basename(src) + '.o')
for src in sources
]
for src, dest in zip(sources, objects):
cflgs = cflags[:]
@@ -176,35 +216,62 @@ def compile_c_extension(module, incremental, sources, headers):
def option_parser():
p = argparse.ArgumentParser()
p.add_argument('action', nargs='?', default='build', choices='build test linux-package osx-bundle'.split(), help='Action to perform (default is build)')
p.add_argument('--debug', default=False, action='store_true',
help='Build extension modules with debugging symbols')
p.add_argument('--asan', default=False, action='store_true',
help='Turn on address sanitization to detect memory access errors. Note that if you do turn it on,'
' you have to run kitty with the environment variable LD_PRELOAD=/usr/lib/libasan.so')
p.add_argument('--prefix', default='./linux-package', help='Where to create the linux package')
p.add_argument('--incremental', default=False, action='store_true', help='Only build changed files')
p.add_argument(
'action',
nargs='?',
default='build',
choices='build test linux-package osx-bundle'.split(),
help='Action to perform (default is build)'
)
p.add_argument(
'--debug',
default=False,
action='store_true',
help='Build extension modules with debugging symbols'
)
p.add_argument(
'--asan',
default=False,
action='store_true',
help='Turn on address sanitization to detect memory access errors. Note that if you do turn it on,'
' you have to run kitty with the environment variable LD_PRELOAD=/usr/lib/libasan.so'
)
p.add_argument(
'--prefix',
default='./linux-package',
help='Where to create the linux package'
)
p.add_argument(
'--incremental',
default=False,
action='store_true',
help='Only build changed files'
)
return p
def find_c_files():
ans, headers = [], []
d = os.path.join(base, 'kitty')
exclude = {'freetype.c'} if isosx else {'core_text.m'}
exclude = {'freetype.c', 'fontconfig.c'} if isosx else {'core_text.m'}
for x in os.listdir(d):
ext = os.path.splitext(x)[1]
if ext in ('.c', '.m') and os.path.basename(x) not in exclude:
ans.append(os.path.join('kitty', x))
elif ext == '.h':
headers.append(os.path.join('kitty', x))
ans.sort(key=lambda x: os.path.getmtime(os.path.join(base, x)), reverse=True)
ans.sort(
key=lambda x: os.path.getmtime(os.path.join(base, x)), reverse=True
)
ans.append('kitty/parser_dump.c')
return tuple(ans), tuple(headers)
def build(args):
init_env(args.debug, args.asan)
compile_c_extension('kitty/fast_data_types', args.incremental, *find_c_files())
def build(args, native_optimizations=True):
init_env(args.debug, args.asan, native_optimizations)
compile_c_extension(
'kitty/fast_data_types', args.incremental, *find_c_files()
)
def safe_makedirs(path):
@@ -228,7 +295,10 @@ def package(args, for_bundle=False): # {{{
shutil.copy2('logo/kitty.rgba', os.path.join(libdir, 'logo'))
def src_ignore(parent, entries):
return [x for x in entries if '.' in x and x.rpartition('.')[2] not in ('py', 'so', 'conf')]
return [
x for x in entries
if '.' in x and x.rpartition('.')[2] not in ('py', 'so', 'conf')
]
shutil.copytree('kitty', os.path.join(libdir, 'kitty'), ignore=src_ignore)
import compileall
@@ -239,12 +309,14 @@ 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)
cflags = '-O3 -Wall -Werror'.split()
cflags = '-O3 -Wall -Werror -fpie'.split()
if for_bundle:
cflags.append('-DFOR_BUNDLE')
cflags.append('-DPYVER="{}"'.format(sysconfig.get_python_version()))
pylib = get_python_flags(cflags)
cmd = [cc] + cflags + ['linux-launcher.c', '-o', os.path.join(launcher_dir, 'kitty')] + pylib
cmd = [cc] + cflags + [
'linux-launcher.c', '-o', os.path.join(launcher_dir, 'kitty')
] + pylib
run_tool(cmd)
if not isosx: # {{{ linux desktop gunk
icdir = os.path.join(ddir, 'share', 'icons', 'hicolor', '256x256')
@@ -253,7 +325,8 @@ def package(args, for_bundle=False): # {{{
deskdir = os.path.join(ddir, 'share', 'applications')
safe_makedirs(deskdir)
with open(os.path.join(deskdir, 'kitty.desktop'), 'w') as f:
f.write('''\
f.write(
'''\
[Desktop Entry]
Version=1.0
Type=Application
@@ -264,7 +337,8 @@ TryExec=kitty
Exec=kitty
Icon=kitty
Categories=System;
''')
'''
)
# }}}
if for_bundle: # OS X bundle gunk {{{
@@ -275,7 +349,7 @@ Categories=System;
os.rename('../bin', 'MacOS')
os.rename('../lib', 'Frameworks')
# }}}
# }}}
# }}}
def main():
@@ -287,12 +361,14 @@ def main():
if args.action == 'build':
build(args)
elif args.action == 'test':
os.execlp(sys.executable, sys.executable, os.path.join(base, 'test.py'))
os.execlp(
sys.executable, sys.executable, os.path.join(base, 'test.py')
)
elif args.action == 'linux-package':
build(args)
build(args, native_optimizations=False)
package(args)
elif args.action == 'osx-bundle':
build(args)
build(args, native_optimizations=False)
package(args, for_bundle=True)

Binary file not shown.

View File

@@ -101,6 +101,7 @@ xterm-kitty|KovIdTTY,
rmam=\E[?7l,
rmcup=\E[?1049l,
rmir=\E[4l,
rmkx=\E[?1l,
rmso=\E[27m,
rmul=\E[24m,
rs1=\Ec,
@@ -114,6 +115,7 @@ xterm-kitty|KovIdTTY,
smam=\E[?7h,
smcup=\E[?1049h,
smir=\E[4h,
smkx=\E[?1h,
smso=\E[7m,
smul=\E[4m,
tbc=\E[3g,

Binary file not shown.