Compare commits

...

58 Commits

Author SHA1 Message Date
Kovid Goyal
4bf0e0ea23 version 0.11.2 2018-07-01 07:58:46 +05:30
Kovid Goyal
c7879d06c9 Output native key names in --debug-config 2018-06-30 18:48:39 +05:30
Kovid Goyal
05227e7dfb Fix --debug-config failing when native keys present 2018-06-30 18:42:19 +05:30
Kovid Goyal
9805d164d0 Remove unknown flags from brew install for harfbuzz on travis 2018-06-30 18:34:27 +05:30
Kovid Goyal
05384c46e7 ... 2018-06-30 18:25:45 +05:30
Kovid Goyal
c2679eff9a Fix compiler warning
The compiler warning is harmless, since we exec immediately after
using the const char* but since I have a no warning policy...
2018-06-30 18:15:08 +05:30
Kovid Goyal
3ec66c0b9f Fix regression in previous release that caused --debug-config to fail when printing shortcuts
Forgot to adjust the print shortcuts code for native shortcuts.
Fixes #695
2018-06-30 17:48:25 +05:30
Kovid Goyal
9277b80fad Remove unused code 2018-06-30 12:01:56 +05:30
Kovid Goyal
568a4c9b5d Re-raise KeyError idiomatically 2018-06-29 13:49:08 +05:30
Kovid Goyal
4de18688ca Merge branch 'fix/conf-utils-keyparser' of https://github.com/arubertoson/kitty 2018-06-29 13:42:21 +05:30
Marcus Albertsson
12975347ee Fix: Exception in conf utils
The bug was related to trying to split a string with unstript spaces in
the action parameter. It resulted in a unbound error as there was no
previous raise when getting the key from args_funcs. The fix is twofold,
it makes sure that we raise the keyerror if we're trying to grab a non
existing key and we're stripping the spaces from the action before
splitting.

This should fix both the unbound error and the not being able to resolve
correct actions.
2018-06-29 08:56:25 +01:00
Kovid Goyal
e35c3cc913 Add an option single_window_margin_width to allow different margins when only a single window is visible in the layout
Fixes #688
2018-06-29 09:56:05 +05:30
Kovid Goyal
65fef81f29 hints kitten: Add an option to select multiple items
Fixes #687
2018-06-29 06:49:16 +05:30
Kovid Goyal
e3f60e6fa7 Update CHANGELOG 2018-06-28 20:34:56 +05:30
Kovid Goyal
0c88e11cd7 More useful exception when unexpected output encountered from a kitten 2018-06-27 14:13:40 +05:30
Kovid Goyal
d689c21283 Fix pasting large amounts of text very slow.
Fixes #682

Apparently when refactoring child.py I somehow lost the code to make the
child fd non-blocking.
2018-06-26 18:20:46 +05:30
Kovid Goyal
5362d1a7a6 Micro-optimization 2018-06-26 16:02:05 +05:30
Kovid Goyal
3b1490020d ... 2018-06-26 12:35:38 +05:30
Kovid Goyal
b8ba25b81f Add a link to the kitty source code for packagers 2018-06-26 12:03:07 +05:30
Kovid Goyal
8bfdc47998 macOS: Fix an error in remote control when using --listen-on. Fixes #679 2018-06-25 12:33:54 +05:30
Kovid Goyal
2491fafe98 Update glfw from upstream 2018-06-23 14:59:36 +05:30
Kovid Goyal
b5bc466aef Merge branch 'master' of https://github.com/Ma-Shell/kitty 2018-06-23 13:54:47 +05:30
Marcel Schumacher
cfd6ea4301 bugfix for get-text with extent=screen 2018-06-23 10:13:37 +02:00
Kovid Goyal
2cdf41f190 ... 2018-06-23 13:20:53 +05:30
Kovid Goyal
9169d0aa9b ... 2018-06-23 10:34:35 +05:30
Kovid Goyal
05a25e3531 redirect to kitty website 2018-06-23 10:30:34 +05:30
Kovid Goyal
2852ce9a4d Update glfw from upstream 2018-06-23 10:23:21 +05:30
Kovid Goyal
20f8f2c401 Removed unused code
The template is no longer needed since I switched my global
template to py3
2018-06-23 10:03:37 +05:30
Kovid Goyal
0995187080 Remove unused code
linux-installer.sh was accidentally committed to the kitty repo
2018-06-23 09:58:20 +05:30
Kovid Goyal
d10eac1b02 Merge branch 'fix-new_os_window' of https://github.com/blueyed/kitty 2018-06-23 08:28:11 +05:30
Daniel Hahler
2cc2f803f1 create_session: handle args=None
Fixes `new_os_window`:

    scancode: 0x39 release: 0 clean_sym: n composed_sym: N mods: ctrl+shift glfw_key: N
    Traceback (most recent call last):
      File "…/Vcs/kitty/kitty/boss.py", line 431, in dispatch_special_key
        return self.dispatch_action(key_action)
      File "…/Vcs/kitty/kitty/boss.py", line 487, in dispatch_action
        passthrough = f(*key_action.args)
      File "…/Vcs/kitty/kitty/boss.py", line 199, in new_os_window
        self._new_os_window(args)
      File "…/Vcs/kitty/kitty/boss.py", line 195, in _new_os_window
        startup_session = create_session(self.opts, special_window=sw, cwd_from=cwd_from)
      File "…/Vcs/kitty/kitty/session.py", line 127, in create_session
        if args.hold:
2018-06-23 02:57:47 +02:00
Kovid Goyal
66a52ac588 Also ignore failure to read readline history in the ask kitten 2018-06-22 17:23:55 +05:30
Kovid Goyal
32f22e0ada kitty shell: Ignore failure to read readline history file 2018-06-22 17:23:01 +05:30
Kovid Goyal
c037f53534 ... 2018-06-22 15:35:24 +05:30
Kovid Goyal
aba84581f5 When running kitty from within kitty, use a full path
Fixes inability to run kitty from the shell when the users shell scripts
reset PATH. See #668
2018-06-22 15:33:07 +05:30
Kovid Goyal
82f6fbff77 Add a --hold command line option to stay open after the child process exits
Fixes #667
2018-06-22 13:52:03 +05:30
Kovid Goyal
d16215ab6e Initialize glfw for --debug-config as well 2018-06-22 13:30:03 +05:30
Kovid Goyal
45b0233f60 Update GLFW from upstream
Use the new init hint for keyboard debugging
2018-06-22 13:26:41 +05:30
Kovid Goyal
1ce4366972 ... 2018-06-22 12:44:08 +05:30
Kovid Goyal
c8fc21d336 Linux: Allow using XKB key names to bind shortcuts to keys not supported by GLFW
Useful to bind keys such as the play/pause or volume buttons. Also can
be used to bind non-ascii keys on international keyboards. Fixes #665
2018-06-22 12:41:50 +05:30
Kovid Goyal
5dd3243674 Update glfw from upstream 2018-06-22 10:21:51 +05:30
Kovid Goyal
815dd99f7c Keep the Debian PC police happy
Fixes #661
2018-06-21 13:12:45 +05:30
Kovid Goyal
bf0dcb1d2e Update version of glad used to generate OpenGL wrapper
Also fix #654
2018-06-20 12:43:26 +05:30
Kovid Goyal
031fc10859 Merge branch 'title-command-line-parameter' of https://github.com/lamby/kitty 2018-06-19 13:48:55 +05:30
Chris Lamb
a644e4c31e Support "-T" as an alias for "--title".
Thank you for developing Kitty.

Currently kitty is not available on the list of "alternatives" for a
terminal emulator on Debian GNU/Linux and derivatives. This is because
Debian Policy states that packages that provide a terminal emulator must
provide, inter alia, support the command-line option -T title, which
creates a new terminal window with the specified window title [0].

This PR therefore adds -T as an alias for --title, thus bringing kitty into
compliance and allowing cleaner integration into the Debian desktop.

This issue was originally filed as #900704.

  [0] https://www.debian.org/doc/debian-policy/#packages-providing-a-terminal-emulator
  [1] https://bugs.debian.org/900704

Signed-off-by: Chris Lamb <lamby@debian.org>
2018-06-19 10:10:43 +02:00
Kovid Goyal
0398dbdcbe Fix extra blank lines when using log_error() 2018-06-19 12:51:54 +05:30
Kovid Goyal
84b2fbba28 --debug-font-fallback also prints out what basic fonts were matched
Fixes #656
2018-06-19 12:48:33 +05:30
Kovid Goyal
491f233d33 diff kitten: When triggering a search scroll to the first match automatically 2018-06-19 10:30:52 +05:30
Kovid Goyal
5e38af6d24 When closing a kitty window reset the mouse cursor to its default shape and ensure it is visible. Fixes #655 2018-06-19 10:19:17 +05:30
Kovid Goyal
4c0f68150a Merge branch 'master' of https://github.com/Luflosi/kitty 2018-06-18 18:41:12 +05:30
Luflosi
3f25f70783 Ignore .DS_Store 2018-06-18 14:39:12 +02:00
Kovid Goyal
d7007f77aa DRYer 2018-06-18 12:18:04 +05:30
Kovid Goyal
94273941ad ... 2018-06-18 12:10:42 +05:30
Kovid Goyal
c09542ea6a Speed up reading of command responses 2018-06-18 11:42:32 +05:30
Kovid Goyal
c5583d380d No need to use select() for TTYIO
Instead make read() block with timeout via termios
2018-06-18 11:10:16 +05:30
Kovid Goyal
f820f72768 Add a kitty @ get-colors command 2018-06-18 10:02:05 +05:30
Kovid Goyal
6db1f8e3e3 Dont read more data than needed in TTYIO 2018-06-18 08:36:47 +05:30
Kovid Goyal
2aba3e249c Linux installer: Workaround for bug in 3.0 <= python <= 3.5 2018-06-18 07:21:44 +05:30
63 changed files with 3447 additions and 3986 deletions

1
.gitignore vendored
View File

@@ -15,3 +15,4 @@ __pycache__
glfw/wayland-*-client-protocol.[ch]
docs/_build
docs/generated
.DS_Store

View File

@@ -103,7 +103,7 @@ install: |
if [[ "$USE_BREW" == "1" ]]; then
brew update;
brew upgrade python;
brew install harfbuzz --without-glib --without-gobject-introspection --without-graphite2 --without-icu4c --without-freetype;
brew install harfbuzz --without-graphite2 --without-icu4c --without-freetype;
brew install imagemagick
brew install optipng
brew install librsvg

View File

@@ -1,5 +0,0 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: %YEAR%, %USER% <%MAIL%>
%HERE%

View File

@@ -26,6 +26,13 @@ def runpy(args):
exec(args[1])
def hold(args):
import subprocess
ret = subprocess.Popen(args[1:]).wait()
sys.stdin.read()
raise SystemExit(ret)
def launch(args):
import runpy
sys.argv = args[1:]
@@ -61,6 +68,7 @@ entry_points = {
'+': namespaced,
}
namespaced_entry_points = {k: v for k, v in entry_points.items() if k[0] not in '+@'}
namespaced_entry_points['hold'] = hold
def setup_openssl_environment():

View File

@@ -60,24 +60,27 @@ just ``kitty``.
Note for Linux/macOS packagers
----------------------------------
While kitty does use python, it is not a traditional python package, so please
The released |kitty| source code is available as a `tarball`_ from
`the GitHub releases page <https://github.com/kovidgoyal/kitty/releases>`_.
While |kitty| does use python, it is not a traditional python package, so please
do not install it in site-packages.
Instead run::
python3 setup.py linux-package
This will install kitty into the directory :file:`linux-package`. You can run kitty
This will install |kitty| into the directory :file:`linux-package`. You can run |kitty|
with :file:`linux-package/bin/kitty`. All the files needed to run kitty will be in
:file:`linux-package/lib/kitty`. The terminfo file will be installed into
:file:`linux-package/share/terminfo`. Simply copy these files into :file:`/usr` to install
kitty. In other words, :file:`linux-package` is the staging area into which kitty is
|kitty|. In other words, :file:`linux-package` is the staging area into which |kitty| is
installed. You can choose a different staging area, by passing the ``--prefix``
argument to :file:`setup.py`.
You should probably split kitty into two packages, :file:`kitty-terminfo` that
You should probably split |kitty| into two packages, :file:`kitty-terminfo` that
installs the terminfo file and :file:`kitty` that installs the main program.
This allows users to install the terminfo file on servers into which they ssh,
without needing to install all of kitty.
without needing to install all of |kitty|.
.. note::
You need a couple of extra dependencies to build linux-package.
@@ -87,5 +90,5 @@ without needing to install all of kitty.
need :file:`sphinx-build` from the `Sphinx documentation generator
<http://www.sphinx-doc.org/>`_.
This applies to creating packages for kitty for macOS package managers such as
This applies to creating packages for |kitty| for macOS package managers such as
brew or MacPorts as well.

View File

@@ -3,6 +3,43 @@ Changelog
|kitty| is a feature full, cross-platform, *fast*, GPU based terminal emulator.
0.11.2 [2018-07-01]
------------------------------
- Linux: Allow using XKB key names to bind shortcuts to keys not supported by GLFW (:pull:`665`)
- kitty shell: Ignore failure to read readline history file. Happens if the
user migrates their kitty cache directory between systems with incompatible
readline implementations.
- macOS: Fix an error in remote control when using --listen-on (:iss:`679`)
- hints kitten: Add a :option:`kitty +kitten hints --multiple` option to select
multiple items (:iss:`687`)
- Fix pasting large amounts of text very slow (:iss:`682`)
- Add an option :opt:`single_window_margin_width` to allow different margins
when only a single window is visible in the layout (:iss:`688`)
- Add a :option:`kitty --hold` command line option to stay open after the child process exits (:iss:`667`)
- diff kitten: When triggering a search scroll to the first match automatically
- :option:`kitty --debug-font-fallback` also prints out what basic fonts were matched
- When closing a kitty window reset the mouse cursor to its default shape and ensure it is visible (:iss:`655`).
- Remote control: Speed-up reading of command responses
- Linux installer: Fix installer failing on systems with python < 3.5
- Support "-T" as an alias for "--title" (:pull:`659`)
- Fix a regression in the previous release that broke using :option:`kitty
--debug-config` with custom key mappings (:iss:`695`)
0.11.1 [2018-06-17]
------------------------------

View File

@@ -85,6 +85,8 @@ pygments_style = 'sphinx'
rst_prolog = '''
.. |kitty| replace:: *kitty*
.. |version| replace:: VERSION
.. _tarball: https://github.com/kovidgoyal/kitty/releases/download/vVERSION/kitty-VERSION.tar.xz
.. role:: green
.. role:: italic
.. role:: bold
@@ -92,7 +94,7 @@ rst_prolog = '''
.. role:: title
.. role:: env
'''
'''.replace('VERSION', str_version)
# -- Options for HTML output -------------------------------------------------

View File

@@ -30,7 +30,7 @@ kitty - the fast, featureful, GPU based terminal emulator
the window/tab layout, working directories and programs to run on startup.
* Cross-platform: |kitty| works on Linux and macOS, but because it uses only
OpenGL for rendering, it should be trivial to port to other platforms.
OpenGL for rendering, it should be trivial to port to other Unix-like platforms.
* Allows you to open :ref:`the scrollback buffer <scrollback>` in a
separate window using arbitrary programs of your choice. This is useful for

View File

@@ -72,8 +72,8 @@ class Reporter: # {{{
def get_latest_release_data():
print('Checking for latest release on GitHub...')
req = urllib.Request('https://api.github.com/repos/kovidgoyal/kitty/releases/latest', headers={'Accept': 'application/vnd.github.v3+json'})
res = urllib.urlopen(req)
data = json.load(res)
res = urllib.urlopen(req).read().decode('utf-8')
data = json.loads(res)
html_url = data['html_url'].replace('/tag/', '/download/').rstrip('/')
for asset in data.get('assets', ()):
name = asset['name']

View File

@@ -5,7 +5,7 @@ See :ref:`extended-key-protocol` for more information and `this table in JSON
format <https://github.com/kovidgoyal/kitty/blob/master/key_encoding.json>`_.
===================== ======================
Name Encoded representation (base64)
Name Encoded representation
0 ``G``
1 ``H``
2 ``I``
@@ -126,4 +126,3 @@ X ``p``
Y ``q``
Z ``r``
===================== ======================

View File

@@ -43,6 +43,8 @@ def export():
switch = ['glad_debug_{0} = glad_{0}'.format(f) for f in functions]
c = c.replace('<glad/glad.h>', '"gl-wrapper.h"', 1)
c = '#pragma GCC diagnostic ignored "-Wpedantic"\n' + c
# See https://github.com/kovidgoyal/kitty/issues/654
c = c.replace('strncpy(local_str, gl_str_tmp, len+1)', 'memcpy(local_str, gl_str_tmp, len+1)', 1)
c += '''
int
init_glad(GLADloadproc load, int debug) {

View File

@@ -69,7 +69,6 @@ static void changeToResourcesDirectory(void)
static void createKeyTables(void)
{
int scancode;
_glfw.ns.debug_keyboard = getenv("GLFW_DEBUG_KEYBOARD") != NULL;
memset(_glfw.ns.keycodes, -1, sizeof(_glfw.ns.keycodes));
memset(_glfw.ns.scancodes, -1, sizeof(_glfw.ns.scancodes));

View File

@@ -121,7 +121,6 @@ typedef struct _GLFWlibraryNS
char keyName[64];
char text[256];
GLFWbool debug_keyboard;
short int keycodes[256];
short int scancodes[GLFW_KEY_LAST + 1];
char* clipboardString;

View File

@@ -209,7 +209,7 @@ static int translateFlags(NSUInteger flags)
return mods;
}
#define debug_key(...) if (_glfw.ns.debug_keyboard) NSLog(__VA_ARGS__)
#define debug_key(...) if (_glfw.hints.init.debugKeyboard) NSLog(__VA_ARGS__)
static inline const char*
format_mods(int mods) {

View File

@@ -208,6 +208,7 @@ def generate_wrappers(glfw_header, glfw_native_header):
int32_t glfwGetX11Window(GLFWwindow* window)
void glfwSetX11SelectionString(const char* string)
const char* glfwGetX11SelectionString(void)
int glfwGetXKBScancode(const char* key_name, int case_sensitive)
'''.splitlines():
if line:
functions.append(Function(line.strip(), check_fail=False))

1
glfw/glfw3.h vendored
View File

@@ -1057,6 +1057,7 @@ extern "C" {
/*! @addtogroup init
* @{ */
#define GLFW_JOYSTICK_HAT_BUTTONS 0x00050001
#define GLFW_DEBUG_KEYBOARD 0x00050002
#define GLFW_COCOA_CHDIR_RESOURCES 0x00051001
#define GLFW_COCOA_MENUBAR 0x00051002

5
glfw/init.c vendored
View File

@@ -51,6 +51,7 @@ static GLFWerrorfun _glfwErrorCallback;
static _GLFWinitconfig _glfwInitHints =
{
GLFW_TRUE, // hat buttons
GLFW_FALSE, // debug keyboard
{
GLFW_TRUE, // macOS menu bar
GLFW_TRUE // macOS bundle chdir
@@ -255,6 +256,9 @@ GLFWAPI void glfwInitHint(int hint, int value)
case GLFW_JOYSTICK_HAT_BUTTONS:
_glfwInitHints.hatButtons = value;
return;
case GLFW_DEBUG_KEYBOARD:
_glfwInitHints.debugKeyboard = value;
return;
case GLFW_COCOA_CHDIR_RESOURCES:
_glfwInitHints.ns.chdir = value;
return;
@@ -311,4 +315,3 @@ GLFWAPI GLFWerrorfun glfwSetErrorCallback(GLFWerrorfun cbfun)
_GLFW_SWAP_POINTERS(_glfwErrorCallback, cbfun);
return cbfun;
}

1
glfw/internal.h vendored
View File

@@ -242,6 +242,7 @@ struct _GLFWerror
struct _GLFWinitconfig
{
GLFWbool hatButtons;
GLFWbool debugKeyboard;
struct {
GLFWbool menubar;
GLFWbool chdir;

2
glfw/wl_init.c vendored
View File

@@ -164,7 +164,7 @@ static void pointerHandleMotion(void* data,
wl_fixed_t sy)
{
_GLFWwindow* window = _glfw.wl.pointerFocus;
const char* cursorName;
const char* cursorName = NULL;
if (!window)
return;

4
glfw/wl_window.c vendored
View File

@@ -1563,3 +1563,7 @@ GLFWAPI struct wl_surface* glfwGetWaylandWindow(GLFWwindow* handle)
_GLFW_REQUIRE_INIT_OR_RETURN(NULL);
return window->wl.surface;
}
GLFWAPI int glfwGetXKBScancode(const char* keyName, GLFWbool caseSensitive) {
return glfw_xkb_keysym_from_name(keyName, caseSensitive);
}

4
glfw/x11_window.c vendored
View File

@@ -2937,3 +2937,7 @@ GLFWAPI const char* glfwGetX11SelectionString(void)
_GLFW_REQUIRE_INIT_OR_RETURN(NULL);
return getSelectionString(_glfw.x11.PRIMARY);
}
GLFWAPI int glfwGetXKBScancode(const char* keyName, GLFWbool caseSensitive) {
return glfw_xkb_keysym_from_name(keyName, caseSensitive);
}

19
glfw/xkb_glfw.c vendored
View File

@@ -30,8 +30,7 @@
#include "internal.h"
#include "xkb_glfw.h"
static GLFWbool debug_keyboard = GLFW_FALSE;
#define debug(...) if (debug_keyboard) printf(__VA_ARGS__);
#define debug(...) if (_glfw.hints.init.debugKeyboard) printf(__VA_ARGS__);
#define map_key(key) { \
switch(key) { \
@@ -206,7 +205,6 @@ glfw_xkb_release(_GLFWXKBData *xkb) {
GLFWbool
glfw_xkb_create_context(_GLFWXKBData *xkb) {
xkb->context = xkb_context_new(0);
debug_keyboard = getenv("GLFW_DEBUG_KEYBOARD") != NULL;
if (!xkb->context)
{
_glfwInputError(GLFW_PLATFORM_ERROR,
@@ -361,6 +359,10 @@ glfw_xkb_keysym_name(xkb_keysym_t sym) {
return name;
}
int
glfw_xkb_keysym_from_name(const char *name, GLFWbool case_sensitive) {
return (int)xkb_keysym_from_name(name, case_sensitive ? XKB_KEYSYM_NO_FLAGS : XKB_KEYSYM_CASE_INSENSITIVE);
}
static inline const char*
format_mods(unsigned int mods) {
@@ -445,15 +447,20 @@ glfw_xkb_handle_key_event(_GLFWwindow *window, _GLFWXKBData *xkb, xkb_keycode_t
if (text[0]) { debug("%s: %s ", text_type, text); }
}
int glfw_keycode = glfw_key_for_sym(glfw_sym);
GLFWbool is_fallback = GLFW_FALSE;
if (glfw_keycode == GLFW_KEY_UNKNOWN && !text[0]) {
int num_default_syms = xkb_state_key_get_syms(xkb->default_state, code_for_sym, &default_syms);
if (num_default_syms > 0) {
glfw_sym = default_syms[0];
glfw_keycode = glfw_key_for_sym(glfw_sym);
is_fallback = GLFW_TRUE;
}
debug("%sglfw_fallback_key: %s\n", format_mods(xkb->modifiers), _glfwGetKeyName(glfw_keycode));
} else {
debug("%sglfw_key: %s\n", format_mods(xkb->modifiers), _glfwGetKeyName(glfw_keycode));
}
debug(
"%s%s: %s xkb_key_name: %s\n",
format_mods(xkb->modifiers),
is_fallback ? "glfw_fallback_key" : "glfw_key", _glfwGetKeyName(glfw_keycode),
glfw_xkb_keysym_name(glfw_sym)
);
_glfwInputKeyboard(window, glfw_keycode, glfw_sym, action, xkb->modifiers, text, 0);
}

1
glfw/xkb_glfw.h vendored
View File

@@ -83,3 +83,4 @@ GLFWbool glfw_xkb_should_repeat(_GLFWXKBData *xkb, xkb_keycode_t scancode);
const char* glfw_xkb_keysym_name(xkb_keysym_t sym);
xkb_keysym_t glfw_xkb_sym_for_key(int key);
void glfw_xkb_handle_key_event(_GLFWwindow *window, _GLFWXKBData *xkb, xkb_keycode_t scancode, int action);
int glfw_xkb_keysym_from_name(const char *name, GLFWbool case_sensitive);

View File

@@ -1,129 +1 @@
= Key encoding for extended keyboard protocol
See link:protocol-extensions.asciidoc#keyboard-handling[Keyboard Handling protocol extension]
for more information and link:key_encoding.json[for this table in JSON format].
|===
| 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`
|===
See https://sw.kovidgoyal.net/kitty/key-encoding.html

View File

@@ -50,8 +50,10 @@ class HistoryCompleter:
def __enter__(self):
if self.history_path:
if os.path.exists(self.history_path):
try:
readline.read_history_file(self.history_path)
except Exception:
pass
readline.set_completer(self.complete)
return self

View File

@@ -168,12 +168,13 @@ class DiffHandler(Handler):
return
self.cmd.bell()
def scroll_to_next_match(self, backwards=False):
def scroll_to_next_match(self, backwards=False, include_current=False):
if self.current_search is not None:
offset = 0 if include_current else 1
if backwards:
r = range(self.scroll_pos - 1, -1, -1)
r = range(self.scroll_pos - offset, -1, -1)
else:
r = range(self.scroll_pos + 1, len(self.diff_lines))
r = range(self.scroll_pos + offset, len(self.diff_lines))
for i in r:
if i in self.current_search:
self.scroll_lines(i - self.scroll_pos)
@@ -351,7 +352,9 @@ class DiffHandler(Handler):
self.message = sanitize(_('Bad regex: {}').format(query[1:]))
self.cmd.bell()
else:
if not self.current_search(self.diff_lines, self.margin_size, self.screen_size.cols):
if self.current_search(self.diff_lines, self.margin_size, self.screen_size.cols):
self.scroll_to_next_match(include_current=True)
else:
self.state = MESSAGE
self.message = sanitize(_('No matches found'))
self.cmd.bell()

View File

@@ -24,7 +24,7 @@ HINT_ALPHABET = string.digits + string.ascii_lowercase
screen_size = screen_size_function()
class Mark(object):
class Mark:
__slots__ = ('index', 'start', 'end', 'text')
@@ -63,8 +63,10 @@ def highlight_mark(m, text, current_input):
)
def render(text, current_input, all_marks):
def render(text, current_input, all_marks, ignore_mark_indices):
for mark in reversed(all_marks):
if mark.index in ignore_mark_indices:
continue
mtext = highlight_mark(mark, text[mark.start:mark.end], current_input)
text = text[:mark.start] + mtext + text[mark.end:]
@@ -78,11 +80,16 @@ class Hints(Handler):
def __init__(self, text, all_marks, index_map, args):
self.text, self.index_map = text, index_map
self.all_marks = all_marks
self.current_input = ''
self.current_text = None
self.ignore_mark_indices = set()
self.args = args
self.window_title = _('Choose URL') if args.type == 'url' else _('Choose text')
self.chosen = None
self.multiple = args.multiple
self.chosen = []
self.reset()
def reset(self):
self.current_input = ''
self.current_text = None
def init_terminal_state(self):
self.cmd.set_cursor_visible(False)
@@ -101,13 +108,17 @@ class Hints(Handler):
changed = True
if changed:
matches = [
m.text for idx, m in self.index_map.items()
m for idx, m in self.index_map.items()
if encode_hint(idx).startswith(self.current_input)
]
if len(matches) == 1:
self.chosen = matches[0]
self.quit_loop(0)
return
self.chosen.append(matches[0].text)
if self.multiple:
self.ignore_mark_indices.add(matches[0].index)
self.reset()
else:
self.quit_loop(0)
return
self.current_text = None
self.draw_screen()
@@ -119,15 +130,20 @@ class Hints(Handler):
elif key_event is enter_key and self.current_input:
try:
idx = decode_hint(self.current_input)
self.chosen = self.index_map[idx].text
self.chosen.append(self.index_map[idx].text)
self.ignore_mark_indices.add(idx)
except Exception:
self.current_input = ''
self.current_text = None
self.draw_screen()
else:
self.quit_loop(0)
if self.multiple:
self.reset()
self.draw_screen()
else:
self.quit_loop(0)
elif key_event.key is ESCAPE:
self.quit_loop(1)
self.quit_loop(0 if self.multiple else 1)
def on_interrupt(self):
self.quit_loop(1)
@@ -141,7 +157,7 @@ class Hints(Handler):
def draw_screen(self):
if self.current_text is None:
self.current_text = render(self.text, self.current_input, self.all_marks)
self.current_text = render(self.text, self.current_input, self.all_marks, self.ignore_mark_indices)
self.cmd.clear_screen()
self.write(self.current_text)
@@ -342,6 +358,12 @@ Defaults to the select_by_word_characters setting from kitty.conf.
default=3
type=int
The minimum number of characters to consider a match.
--multiple
type=bool-set
Select multiple matches and perform the action on all of them together at the end.
In this mode, press :kbd:`Esc` to finish selecting.
'''.format(','.join(sorted(URL_PREFIXES))).format
help_text = 'Select text from the screen using the keyboard. Defaults to searching for URLs.'
usage = ''
@@ -377,18 +399,22 @@ def main(args):
def handle_result(args, data, target_window_id, boss):
program = data['program']
matches = tuple(filter(None, data['match']))
if program == '-':
w = boss.window_id_map.get(target_window_id)
if w is not None:
w.paste(data['match'])
for m in matches:
w.paste(m)
elif program == '@':
set_clipboard_string(data['match'])
set_clipboard_string(matches[-1])
else:
cwd = None
w = boss.window_id_map.get(target_window_id)
if w is not None:
cwd = w.cwd_of_child
boss.open_url(data['match'], None if program == 'default' else program, cwd=cwd)
program = None if program == 'default' else program
for m in matches:
boss.open_url(m, program, cwd=cwd)
handle_result.type_of_input = 'screen'

View File

@@ -76,8 +76,11 @@ def launch(args):
def deserialize(output):
import json
if output.startswith('OK: '):
prefix, sz, rest = output.split(' ', 2)
return json.loads(rest[:int(sz)])
try:
prefix, sz, rest = output.split(' ', 2)
return json.loads(rest[:int(sz)])
except Exception:
raise ValueError('Failed to parse kitten output: {!r}'.format(output))
def run_kitten(kitten, run_name='__main__'):

View File

@@ -15,7 +15,7 @@ from .conf.utils import to_cmdline
from .config import initial_window_size_func, prepare_config_file_for_editing
from .config_data import MINIMUM_FONT_SIZE
from .constants import (
appname, config_dir, set_boss, supports_primary_selection
appname, config_dir, kitty_exe, set_boss, supports_primary_selection
)
from .fast_data_types import (
ChildMonitor, background_opacity_of, change_background_opacity,
@@ -561,7 +561,7 @@ class Boss:
confpath = prepare_config_file_for_editing()
# On macOS vim fails to handle SIGWINCH if it occurs early, so add a
# small delay.
cmd = ['kitty', '+runpy', 'import os, sys, time; time.sleep(0.05); os.execvp(sys.argv[1], sys.argv[1:])'] + get_editor() + [confpath]
cmd = [kitty_exe(), '+runpy', 'import os, sys, time; time.sleep(0.05); os.execvp(sys.argv[1], sys.argv[1:])'] + get_editor() + [confpath]
self.new_os_window(*cmd)
def get_output(self, source_window, num_lines=1):
@@ -600,7 +600,7 @@ class Boss:
copts = {k: self.opts[k] for k in ('select_by_word_characters', 'open_url_with')}
overlay_window = tab.new_special_window(
SpecialWindow(
['kitty', '+runpy', 'from kittens.runner import main; main()'] + args,
[kitty_exe(), '+runpy', 'from kittens.runner import main; main()'] + args,
stdin=data,
env={
'KITTY_COMMON_OPTS': json.dumps(copts),
@@ -647,7 +647,7 @@ class Boss:
break
def kitty_shell(self, window_type):
cmd = ['kitty', '@']
cmd = [kitty_exe(), '@']
if window_type == 'tab':
window = self._new_tab(cmd).active_window
elif window_type == 'os_window':

View File

@@ -17,7 +17,13 @@ serialize_string_tuple(PyObject *src) {
Py_ssize_t sz = PyTuple_GET_SIZE(src);
char **ans = calloc(sz + 1, sizeof(char*));
if (!ans) fatal("Out of memory");
for (Py_ssize_t i = 0; i < sz; i++) ans[i] = PyUnicode_AsUTF8(PyTuple_GET_ITEM(src, i));
for (Py_ssize_t i = 0; i < sz; i++) {
const char *pysrc = PyUnicode_AsUTF8(PyTuple_GET_ITEM(src, i));
size_t len = strlen(pysrc);
ans[i] = calloc(len + 1, sizeof(char));
if (ans[i] == NULL) fatal("Out of memory");
memcpy(ans[i], pysrc, len);
}
return ans;
}

View File

@@ -83,6 +83,7 @@ class Child:
if stdin is not None:
os.close(stdin_read_fd)
fast_data_types.thread_write(stdin_write_fd, stdin)
fcntl.fcntl(self.child_fd, fcntl.F_SETFL, fcntl.fcntl(self.child_fd, fcntl.F_GETFL) | os.O_NONBLOCK)
return pid
@property

View File

@@ -7,9 +7,10 @@ import re
import sys
from collections import deque
from .config import defaults, load_config
from .conf.utils import resolve_config
from .config import defaults, load_config
from .constants import appname, defconf, is_macos, is_wayland, str_version
from .fast_data_types import GLFW_KEY_UNKNOWN, glfw_get_key_name
CONFIG_HELP = '''\
Specify a path to the configuration file(s) to use. All configuration files are
@@ -44,7 +45,7 @@ condition=not is_macos
Set the name part of the :italic:`WM_CLASS` property (defaults to using the value from :option:`{appname} --class`)
--title
--title -T
Set the window title. This will override any title set by the program running inside kitty. So
only use this if you are running a program that does not set titles.
@@ -76,6 +77,12 @@ Path to a file containing the startup :italic:`session` (tabs, windows, layout,
See the README file for details and an example.
--hold
type=bool-set
Remain open after child process exits. Note that this only affects the first
window. You can quit by either using the close window shortcut or :kbd:`Ctrl+d`.
--single-instance -1
type=bool-set
If specified only a single instance of :italic:`{appname}` will run. New invocations will
@@ -213,6 +220,10 @@ def code(x):
return x
def kbd(x):
return x
def env(x):
return italic(x)
@@ -614,12 +625,15 @@ def print_shortcut(key_sequence, action):
keys = []
for key in key_sequence:
names = []
mods, key = key
mods, is_native, key = key
for name, val in mmap.items():
if mods & val:
names.append(name)
if key:
names.append(krmap[key])
if is_native:
names.append(glfw_get_key_name(GLFW_KEY_UNKNOWN, key))
else:
names.append(krmap[key])
keys.append('+'.join(names))
print('\t', ' > '.join(keys), action)

View File

@@ -10,6 +10,7 @@ from .cli import parse_args
from .config import parse_config, parse_send_text_bytes
from .constants import appname
from .tabs import SpecialWindow
from .utils import natsort_ints
class MatchError(ValueError):
@@ -529,7 +530,7 @@ def get_text(boss, window, payload):
if payload['extent'] == 'selection':
ans = window.text_for_selection()
else:
ans = window.as_text(as_ansi=bool(payload['ansi']), add_history=True)
ans = window.as_text(as_ansi=bool(payload['ansi']), add_history=payload['extent'] == 'all')
return ans
# }}}
@@ -607,6 +608,38 @@ def set_colors(boss, window, payload):
# }}}
# get_colors {{{
@cmd(
'Get terminal colors',
'Get the terminal colors for the specified window (defaults to active window). Colors will be output to stdout in the same syntax as used for kitty.conf',
options_spec='''\
--configured -c
type=bool-set
Instead of outputting the colors for the specified window, output the currently
configured colors.
''' + '\n\n' + MATCH_WINDOW_OPTION
)
def cmd_get_colors(global_opts, opts, args):
return {'configured': opts.configured, 'match': opts.match}
def get_colors(boss, window, payload):
from .rgb import Color, color_as_sharp, color_from_int
ans = {k: getattr(boss.opts, k) for k in boss.opts if isinstance(getattr(boss.opts, k), Color)}
if not payload['configured']:
windows = (window or boss.active_window,)
if payload['match']:
windows = tuple(boss.match_windows(payload['match']))
if not windows:
raise MatchError(payload['match'])
ans.update({k: color_from_int(v) for k, v in windows[0].current_colors.items()})
all_keys = natsort_ints(ans)
maxlen = max(map(len, all_keys))
return '\n'.join(('{:%ds} {}' % maxlen).format(key, color_as_sharp(ans[key])) for key in all_keys)
# }}}
# set_background_opacity {{{
@cmd(
'Set the background_opacity',

View File

@@ -150,6 +150,37 @@ colorprofile_to_color(ColorProfile *self, color_type entry, color_type defval) {
}
static PyObject*
as_dict(ColorProfile *self, PyObject *args UNUSED) {
#define as_dict_doc "Return all colors as a dictionary of color_name to integer (names are the same as used in kitty.conf)"
PyObject *ans = PyDict_New();
if (ans == NULL) return PyErr_NoMemory();
for (unsigned i = 0; i < arraysz(self->color_table); i++) {
static char buf[32] = {0};
snprintf(buf, sizeof(buf) - 1, "color%u", i);
PyObject *val = PyLong_FromUnsignedLong(self->color_table[i]);
if (!val) { Py_CLEAR(ans); return PyErr_NoMemory(); }
int ret = PyDict_SetItemString(ans, buf, val);
Py_CLEAR(val);
if (ret != 0) { Py_CLEAR(ans); return NULL; }
}
#define D(attr, name) { \
color_type c = colorprofile_to_color(self, self->overridden.attr, 0xffffffff); \
if (c != 0xffffffff) { \
PyObject *val = PyLong_FromUnsignedLong(c); \
if (!val) { Py_CLEAR(ans); return PyErr_NoMemory(); } \
int ret = PyDict_SetItemString(ans, #name, val); \
Py_CLEAR(val); \
if (ret != 0) { Py_CLEAR(ans); return NULL; } \
}}
D(default_fg, foreground); D(default_bg, background);
D(cursor_color, cursor); D(highlight_fg, selection_foreground);
D(highlight_bg, selection_background);
#undef D
return ans;
}
static PyObject*
as_color(ColorProfile *self, PyObject *val) {
#define as_color_doc "Convert the specified terminal color into an (r, g, b) tuple based on the current profile values"
@@ -275,6 +306,7 @@ static PyMemberDef members[] = {
static PyMethodDef methods[] = {
METHOD(update_ansi_color_table, METH_O)
METHOD(reset_color_table, METH_NOARGS)
METHOD(as_dict, METH_NOARGS)
METHOD(color_table_address, METH_NOARGS)
METHOD(as_color, METH_O)
METHOD(reset_color, METH_O)

View File

@@ -228,19 +228,28 @@ def parse_kittens_shortcut(sc):
def parse_kittens_func_args(action, args_funcs):
parts = action.split(' ', 1)
parts = action.strip().split(' ', 1)
func = parts[0]
if len(parts) == 1:
return func, ()
rest = parts[1]
parser = args_funcs.get(func)
if parser is not None:
try:
func, args = parser(func, rest)
except Exception:
raise ValueError('Unknown key action: {}'.format(action))
try:
parser = args_funcs[func]
except KeyError as e:
raise KeyError(
'Unknown action: {}. Check if map action: '
'{} is valid'.format(func, action)
) from e
try:
func, args = parser(func, rest)
except Exception:
raise ValueError('Unknown key action: {}'.format(action))
if not isinstance(args, (list, tuple)):
args = (args,)
return func, tuple(args)

View File

@@ -37,12 +37,14 @@ def parse_shortcut(sc):
parts = sc.split('+')
mods = parse_mods(parts[:-1], sc)
if mods is None:
return None, None
return None, None, None
key = parts[-1].upper()
key = getattr(defines, 'GLFW_KEY_' + named_keys.get(key, key), None)
if key is not None:
return mods, key
return mods, None
is_native = False
if key is None:
key = defines.key_for_native_key_name(parts[-1])
is_native = key is not None
return mods, is_native, key
KeyAction = namedtuple('KeyAction', 'func args')
@@ -140,15 +142,15 @@ sequence_sep = '>'
class KeyDefinition:
def __init__(self, is_sequence, action, mods, key, rest=()):
def __init__(self, is_sequence, action, mods, is_native, key, rest=()):
self.is_sequence = is_sequence
self.action = action
self.trigger = mods, key
self.trigger = mods, is_native, key
self.rest = rest
def resolve(self, kitty_mod):
self.trigger = defines.resolve_key_mods(kitty_mod, self.trigger[0]), self.trigger[1]
self.rest = tuple((defines.resolve_key_mods(kitty_mod, mods), key) for mods, key in self.rest)
self.trigger = defines.resolve_key_mods(kitty_mod, self.trigger[0]), self.trigger[1], self.trigger[2]
self.rest = tuple((defines.resolve_key_mods(kitty_mod, mods), is_native, key) for mods, is_native, key in self.rest)
def parse_key(val, key_definitions):
@@ -161,18 +163,18 @@ def parse_key(val, key_definitions):
trigger = None
rest = []
for part in sc.split(sequence_sep):
mods, key = parse_shortcut(part)
mods, is_native, key = parse_shortcut(part)
if key is None:
if mods is not None:
log_error('Shortcut: {} has unknown key, ignoring'.format(sc))
return
if trigger is None:
trigger = mods, key
trigger = mods, is_native, key
else:
rest.append((mods, key))
rest.append((mods, is_native, key))
rest = tuple(rest)
else:
mods, key = parse_shortcut(sc)
mods, is_native, key = parse_shortcut(sc)
if key is None:
if mods is not None:
log_error('Shortcut: {} has unknown key, ignoring'.format(sc))
@@ -186,9 +188,9 @@ def parse_key(val, key_definitions):
if paction is not None:
all_key_actions.add(paction.func)
if is_sequence:
key_definitions.append(KeyDefinition(True, paction, trigger[0], trigger[1], rest))
key_definitions.append(KeyDefinition(True, paction, trigger[0], trigger[1], trigger[2], rest))
else:
key_definitions.append(KeyDefinition(False, paction, mods, key))
key_definitions.append(KeyDefinition(False, paction, mods, is_native, key))
def parse_symbol_map(val):

View File

@@ -83,8 +83,17 @@ as color16 to color255.''')
'shortcuts': [
_('Keyboard shortcuts'),
_('''\
For a list of key names, see: :link:`GLFW keys <http://www.glfw.org/docs/latest/group__keys.html>`
For a list of modifier names, see: :link:`GLFW mods <http://www.glfw.org/docs/latest/group__mods.html>`
For a list of key names, see: :link:`GLFW keys
<http://www.glfw.org/docs/latest/group__keys.html>`. The name to use is the part
after the :code:`GLFW_KEY_` prefix. For a list of modifier names, see:
:link:`GLFW mods <http://www.glfw.org/docs/latest/group__mods.html>`
On Linux you can also use XKB key names to bind keys that are not supported by
GLFW. See :link:`XKB keys
<https://github.com/xkbcommon/libxkbcommon/blob/master/xkbcommon/xkbcommon-keysyms.h>`
for a list of key names. The name to use is the part after the :code:`XKB_KEY_`
prefix. Note that you should only use an XKB key name for keys that are not present
in the list of GLFW keys.
You can use the special action :code:`no_op` to unmap a keyboard shortcut that is
assigned in the default configuration.
@@ -443,6 +452,10 @@ Note that borders are displayed only when more than one window is visible. They
o('window_margin_width', 0.0, option_type=positive_float, long_text=_('''
The window margin (in pts) (blank area outside the border)'''))
o('single_window_margin_width', -1000.0, option_type=float, long_text=_('''
The window margin (in pts) to use when only a single window is visible.
Negative values will cause the value of :opt:`window_margin_width` to be used instead.'''))
o('window_padding_width', 0.0, option_type=positive_float, long_text=_('''
The window padding (in pts) (blank area between the text and the window border)'''))

View File

@@ -10,7 +10,7 @@ from collections import namedtuple
from .fast_data_types import set_boss as set_c_boss
appname = 'kitty'
version = (0, 11, 1)
version = (0, 11, 2)
str_version = '.'.join(map(str, version))
_plat = sys.platform.lower()
is_macos = 'darwin' in _plat
@@ -21,6 +21,22 @@ ScreenGeometry = namedtuple('ScreenGeometry', 'xstart ystart xnum ynum dx dy')
WindowGeometry = namedtuple('WindowGeometry', 'left top right bottom xnum ynum')
def kitty_exe():
ans = getattr(kitty_exe, 'ans', None)
if ans is None:
rpath = getattr(sys, 'bundle_exe_dir', None)
if not rpath:
items = frozenset(os.environ['PATH'].split(os.pathsep))
for candidate in items:
if os.access(os.path.join(candidate, 'kitty'), os.X_OK):
rpath = candidate
break
else:
rpath = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'launcher')
ans = kitty_exe.ans = os.path.join(rpath, 'kitty')
return ans
def _get_config_dir():
if 'KITTY_CONFIG_DIRECTORY' in os.environ:
return os.path.abspath(os.path.expanduser(os.environ['KITTY_CONFIG_DIRECTORY']))

View File

@@ -105,21 +105,29 @@ stop_profiler(PyObject UNUSED *self) {
#endif
static inline bool
put_tty_in_raw_mode(int fd, const struct termios* termios_p) {
put_tty_in_raw_mode(int fd, const struct termios* termios_p, bool read_with_timeout) {
struct termios raw_termios = *termios_p;
cfmakeraw(&raw_termios);
raw_termios.c_cc[VMIN] = 1; raw_termios.c_cc[VTIME] = 0;
if (read_with_timeout) {
raw_termios.c_cc[VMIN] = 0; raw_termios.c_cc[VTIME] = 1;
} else {
raw_termios.c_cc[VMIN] = 1; raw_termios.c_cc[VTIME] = 0;
}
if (tcsetattr(fd, TCSAFLUSH, &raw_termios) != 0) { PyErr_SetFromErrno(PyExc_OSError); return false; }
return true;
}
static PyObject*
open_tty(PyObject *self UNUSED, PyObject *args UNUSED) {
int fd = open("/dev/tty", O_RDWR | O_NONBLOCK | O_CLOEXEC | O_NOCTTY);
open_tty(PyObject *self UNUSED, PyObject *args) {
int read_with_timeout = 0;
if (!PyArg_ParseTuple(args, "|p", &read_with_timeout)) return NULL;
int flags = O_RDWR | O_CLOEXEC | O_NOCTTY;
if (!read_with_timeout) flags |= O_NONBLOCK;
int fd = open("/dev/tty", flags);
struct termios *termios_p = calloc(1, sizeof(struct termios));
if (!termios_p) return PyErr_NoMemory();
if (tcgetattr(fd, termios_p) != 0) { free(termios_p); PyErr_SetFromErrno(PyExc_OSError); return NULL; }
if (!put_tty_in_raw_mode(fd, termios_p)) { free(termios_p); return NULL; }
if (!put_tty_in_raw_mode(fd, termios_p, read_with_timeout != 0)) { free(termios_p); return NULL; }
return Py_BuildValue("iN", fd, PyLong_FromVoidPtr(termios_p));
}
@@ -138,7 +146,7 @@ normal_tty(PyObject *self UNUSED, PyObject *args) {
static PyObject*
raw_tty(PyObject *self UNUSED, PyObject *args) {
TTY_ARGS
if (!put_tty_in_raw_mode(fd, termios_p)) return NULL;
if (!put_tty_in_raw_mode(fd, termios_p, false)) return NULL;
Py_RETURN_NONE;
}
@@ -155,7 +163,7 @@ close_tty(PyObject *self UNUSED, PyObject *args) {
#undef TTY_ARGS
static PyMethodDef module_methods[] = {
{"open_tty", open_tty, METH_NOARGS, ""},
{"open_tty", open_tty, METH_VARARGS, ""},
{"normal_tty", normal_tty, METH_VARARGS, ""},
{"raw_tty", raw_tty, METH_VARARGS, ""},
{"close_tty", close_tty, METH_VARARGS, ""},

View File

@@ -286,7 +286,7 @@ void focus_in_event();
void wakeup_io_loop(bool);
void scroll_event(double, double);
void fake_scroll(int, bool);
void set_special_key_combo(int glfw_key, int mods);
void set_special_key_combo(int glfw_key, int mods, bool is_native);
void on_key_input(int key, int scancode, int action, int mods, const char*, int);
void request_window_attention(id_type, bool);
SPRITE_MAP_HANDLE alloc_sprite_map(unsigned int, unsigned int);

View File

@@ -15,6 +15,7 @@ from kitty.fast_data_types import (
test_render_line, test_shape
)
from kitty.fonts.box_drawing import render_box_char, render_missing_glyph
from kitty.utils import log_error
if is_macos:
from .core_text import get_font_files, font_for_family
@@ -42,7 +43,26 @@ def descriptor_for_idx(idx):
return current_faces[idx]
def set_font_family(opts=None, override_font_size=None):
def dump_faces(ftypes, indices):
def face_str(f):
f = f[0]
if is_macos:
return f
return '{}:{}'.format(f['path'], f['index'])
log_error('Preloaded font faces:')
log_error('normal face:', face_str(current_faces[0]))
for ftype in ftypes:
if indices[ftype]:
log_error(ftype, 'face:', face_str(current_faces[indices[ftype]]))
si_faces = current_faces[max(indices.values())+1:]
if si_faces:
log_error('Symbol map faces:')
for face in si_faces:
log_error(face_str(face))
def set_font_family(opts=None, override_font_size=None, debug_font_matching=False):
global current_faces
opts = opts or defaults
sz = override_font_size or opts.font_size
@@ -57,6 +77,8 @@ def set_font_family(opts=None, override_font_size=None):
before = len(current_faces)
sm = create_symbol_map(opts)
num_symbol_fonts = len(current_faces) - before
if debug_font_matching:
dump_faces(ftypes, indices)
set_font_data(
render_box_drawing, prerender_function, descriptor_for_idx,
indices['bold'], indices['italic'], indices['bi'], num_symbol_fonts,

5656
kitty/gl-wrapper.c generated

File diff suppressed because it is too large Load Diff

14
kitty/gl-wrapper.h generated
View File

@@ -1,5 +1,5 @@
/*
OpenGL loader generated by glad 0.1.20a0 on Fri May 4 18:11:55 2018.
OpenGL loader generated by glad 0.1.24a0 on Wed Jun 20 12:32:33 2018.
Language/Generator: C/C++ Debug
Specification: gl
APIs: gl=3.3
@@ -146,16 +146,8 @@ typedef unsigned int GLhandleARB;
typedef unsigned short GLhalfARB;
typedef unsigned short GLhalf;
typedef GLint GLfixed;
#if defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) && (__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ > 1060)
typedef long GLintptr;
#else
typedef ptrdiff_t GLintptr;
#endif
#if defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) && (__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ > 1060)
typedef long GLsizeiptr;
#else
typedef ptrdiff_t GLsizeiptr;
#endif
typedef khronos_intptr_t GLintptr;
typedef khronos_ssize_t GLsizeiptr;
typedef int64_t GLint64;
typedef uint64_t GLuint64;
#if defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) && (__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ > 1060)

2
kitty/glfw-wrapper.c generated
View File

@@ -371,6 +371,8 @@ load_glfw(const char* path) {
*(void **) (&glfwGetX11SelectionString_impl) = dlsym(handle, "glfwGetX11SelectionString");
*(void **) (&glfwGetXKBScancode_impl) = dlsym(handle, "glfwGetXKBScancode");
return NULL;
}

5
kitty/glfw-wrapper.h generated
View File

@@ -818,6 +818,7 @@ typedef int (* GLFWapplicationshouldhandlereopenfun)(int);
/*! @addtogroup init
* @{ */
#define GLFW_JOYSTICK_HAT_BUTTONS 0x00050001
#define GLFW_DEBUG_KEYBOARD 0x00050002
#define GLFW_COCOA_CHDIR_RESOURCES 0x00051001
#define GLFW_COCOA_MENUBAR 0x00051002
@@ -1862,4 +1863,8 @@ typedef const char* (*glfwGetX11SelectionString_func)();
glfwGetX11SelectionString_func glfwGetX11SelectionString_impl;
#define glfwGetX11SelectionString glfwGetX11SelectionString_impl
typedef int (*glfwGetXKBScancode_func)(const char*, int);
glfwGetXKBScancode_func glfwGetXKBScancode_impl;
#define glfwGetXKBScancode glfwGetXKBScancode_impl
const char* load_glfw(const char* path);

View File

@@ -515,6 +515,9 @@ create_os_window(PyObject UNUSED *self, PyObject *args) {
void
destroy_os_window(OSWindow *w) {
if (w->handle) {
// Ensure mouse cursor is visible and reset to default shape, needed on macOS
show_mouse_cursor(w->handle);
glfwSetCursor(w->handle, NULL);
glfwDestroyWindow(w->handle);
if (current_os_window_ctx == w->handle) current_os_window_ctx = NULL;
}
@@ -555,10 +558,12 @@ error_callback(int error, const char* description) {
PyObject*
glfw_init(PyObject UNUSED *self, PyObject *args) {
const char* path;
if (!PyArg_ParseTuple(args, "s", &path)) return NULL;
int debug_keyboard = 0;
if (!PyArg_ParseTuple(args, "s|p", &path, &debug_keyboard)) return NULL;
const char* err = load_glfw(path);
if (err) { PyErr_SetString(PyExc_RuntimeError, err); return NULL; }
glfwSetErrorCallback(error_callback);
glfwInitHint(GLFW_DEBUG_KEYBOARD, debug_keyboard);
#ifdef __APPLE__
glfwInitHint(GLFW_COCOA_CHDIR_RESOURCES, 0);
glfwInitHint(GLFW_COCOA_MENUBAR, 0);

View File

@@ -26,12 +26,26 @@ key_to_bytes(int glfw_key, bool smkx, bool extended, int mods, int action) {
#define SPECIAL_INDEX(key) ((key & 0x7f) | ( (mods & 0xF) << 7))
#define IS_ALT_MODS(mods) (mods == GLFW_MOD_ALT || mods == (GLFW_MOD_ALT | GLFW_MOD_SHIFT))
typedef struct { int mods, scancode; } NativeKey;
static NativeKey *native_special_keys = NULL;
static size_t native_special_keys_capacity = 0, native_special_keys_count = 0;
void
set_special_key_combo(int glfw_key, int mods) {
uint16_t key = key_map[glfw_key];
if (key != UINT8_MAX) {
key = SPECIAL_INDEX(key);
needs_special_handling[key] = true;
set_special_key_combo(int glfw_key, int mods, bool is_native) {
if (is_native) {
if (native_special_keys_count >= native_special_keys_capacity) {
native_special_keys_capacity = MAX(128, 2 * native_special_keys_capacity);
native_special_keys = realloc(native_special_keys, sizeof(native_special_keys[0]) * native_special_keys_capacity);
if (native_special_keys == NULL) fatal("Out of memory");
}
native_special_keys[native_special_keys_count].mods = mods;
native_special_keys[native_special_keys_count++].scancode = glfw_key;
} else {
uint16_t key = key_map[glfw_key];
if (key != UINT8_MAX) {
key = SPECIAL_INDEX(key);
needs_special_handling[key] = true;
}
}
}
@@ -81,6 +95,24 @@ is_ascii_control_char(char c) {
return c == 0 || (1 <= c && c <= 31) || c == 127;
}
static inline bool
check_if_special(int key, int mods, int scancode) {
uint16_t qkey = (0 <= key && key < (ssize_t)arraysz(key_map)) ? key_map[key] : UINT8_MAX;
bool special = false;
if (qkey != UINT8_MAX) {
qkey = SPECIAL_INDEX(qkey);
special = needs_special_handling[qkey];
}
#ifdef __APPLE__
(void)scancode;
#else
for (size_t i = 0; !special && i < native_special_keys_count; i++) {
if (scancode == native_special_keys[i].scancode && mods == native_special_keys[i].mods) special = true;
}
#endif
return special;
}
void
on_key_input(int key, int scancode, int action, int mods, const char* text, int state UNUSED) {
Window *w = active_window();
@@ -95,14 +127,7 @@ on_key_input(int key, int scancode, int action, int mods, const char* text, int
Screen *screen = w->render_data.screen;
bool has_text = text && !is_ascii_control_char(text[0]);
if (action == GLFW_PRESS || action == GLFW_REPEAT) {
uint16_t qkey = (0 <= key && key < (ssize_t)arraysz(key_map)) ? key_map[key] : UINT8_MAX;
bool special = false;
if (qkey != UINT8_MAX) {
qkey = SPECIAL_INDEX(qkey);
special = needs_special_handling[qkey];
}
/* printf("key: %s mods: %d special: %d\n", key_name(lkey), mods, special); */
if (special) {
if (check_if_special(key, mods, scancode)) {
PyObject *ret = PyObject_CallMethod(global_state.boss, "dispatch_special_key", "iiii", key, scancode, action, mods);
if (ret == NULL) { PyErr_Print(); }
else {
@@ -149,13 +174,38 @@ PYWRAP1(key_to_bytes) {
return Py_BuildValue("y#", ans + 1, *ans);
}
PYWRAP1(key_for_native_key_name) {
const char *name;
int case_sensitive = 0;
PA("s|p", &name, case_sensitive);
#ifdef __APPLE__
Py_RETURN_NONE;
#else
if (glfwGetXKBScancode) { // if this function is called before GLFW is initialized glfwGetXKBScancode will be NULL
int scancode = glfwGetXKBScancode(name, case_sensitive);
if (scancode) return Py_BuildValue("i", scancode);
}
Py_RETURN_NONE;
#endif
}
static PyMethodDef module_methods[] = {
M(key_to_bytes, METH_VARARGS),
M(key_for_native_key_name, METH_VARARGS),
{0}
};
void
finalize(void) {
free(native_special_keys);
}
bool
init_keys(PyObject *module) {
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
if (Py_AtExit(finalize) != 0) {
PyErr_SetString(PyExc_RuntimeError, "Failed to register the keys at exit handler");
return false;
}
return true;
}

View File

@@ -8,6 +8,7 @@ from . import fast_data_types as defines
from .key_encoding import KEY_MAP
from .terminfo import key_as_bytes
from .utils import base64_encode
from .constants import is_macos
def modify_key_bytes(keybytes, amt):
@@ -271,11 +272,17 @@ def interpret_key_event(key, scancode, mods, window, action):
def get_shortcut(keymap, mods, key, scancode):
return keymap.get((mods & 0b1111, key))
mods &= 0b1111
ans = keymap.get((mods, False, key))
if ans is None and not is_macos:
ans = keymap.get((mods, True, scancode))
return ans
def shortcut_matches(s, mods, key, scancode):
return s[0] & 0b1111 == mods & 0b1111 and s[1] == key
mods &= 0b1111
q = scancode if s[1] else key
return s[0] & 0b1111 == mods & 0b1111 and s[2] == q
def generate_key_table():

View File

@@ -7,6 +7,93 @@
#include "data-types.h"
#define CMD_BUF_SZ 2048
static inline bool
append_buf(char buf[CMD_BUF_SZ], size_t *pos, PyObject *ans) {
if (*pos) {
PyObject *bytes = PyBytes_FromStringAndSize(buf, *pos);
if (!bytes) { PyErr_NoMemory(); return false; }
int ret = PyList_Append(ans, bytes);
Py_CLEAR(bytes);
if (ret != 0) return false;
*pos = 0;
}
return true;
}
static inline bool
add_char(char buf[CMD_BUF_SZ], size_t *pos, char ch, PyObject *ans) {
if (*pos >= CMD_BUF_SZ) {
if (!append_buf(buf, pos, ans)) return false;
}
buf[*pos] = ch;
*pos += 1;
return true;
}
static inline bool
read_response(int fd, double timeout, PyObject *ans) {
static char buf[CMD_BUF_SZ];
size_t pos = 0;
enum ReadState {START, STARTING_ESC, P, AT, K, I, T, T2, Y, HYPHEN, C, M, BODY, TRAILING_ESC};
enum ReadState state = START;
char ch;
double end_time = monotonic() + timeout;
while(monotonic() <= end_time) {
ssize_t len = read(fd, &ch, 1);
if (len == 0) continue;
if (len < 0) {
if (errno == EINTR || errno == EAGAIN) continue;
PyErr_SetFromErrno(PyExc_OSError);
return false;
}
end_time = monotonic() + timeout;
switch(state) {
case START:
if (ch == 0x1b) state = STARTING_ESC;
break;
#define CASE(curr, q, next) case curr: state = ch == q ? next : START; break;
CASE(STARTING_ESC, 'P', P);
CASE(P, '@', AT);
CASE(AT, 'k', K);
CASE(K, 'i', I);
CASE(I, 't', T);
CASE(T, 't', T2);
CASE(T2, 'y', Y);
CASE(Y, '-', HYPHEN);
CASE(HYPHEN, 'c', C);
CASE(C, 'm', M);
CASE(M, 'd', BODY);
case BODY:
if (ch == 0x1b) { state = TRAILING_ESC; }
else {
if (!add_char(buf, &pos, ch, ans)) return false;
}
break;
case TRAILING_ESC:
if (ch == '\\') return append_buf(buf, &pos, ans);
if (!add_char(buf, &pos, 0x1b, ans)) return false;
if (!add_char(buf, &pos, ch, ans)) return false;
state = BODY;
break;
}
}
PyErr_SetString(PyExc_TimeoutError, "Timed out while waiting to read cmd response");
return false;
}
static PyObject*
read_command_response(PyObject *self UNUSED, PyObject *args) {
double timeout;
int fd;
PyObject *ans;
if (!PyArg_ParseTuple(args, "idO!", &fd, &timeout, &PyList_Type, &ans)) return NULL;
if (!read_response(fd, timeout, ans)) return NULL;
Py_RETURN_NONE;
}
static PyObject*
parse_input_from_terminal(PyObject *self UNUSED, PyObject *args) {
enum State { NORMAL, ESC, CSI, ST, ESC_ST };
@@ -105,6 +192,7 @@ parse_input_from_terminal(PyObject *self UNUSED, PyObject *args) {
static PyMethodDef module_methods[] = {
METHODB(parse_input_from_terminal, METH_VARARGS),
METHODB(read_command_response, METH_VARARGS),
{NULL, NULL, 0, NULL} /* Sentinel */
};

View File

@@ -158,13 +158,14 @@ class Layout: # {{{
needs_window_borders = True
only_active_window_visible = False
def __init__(self, os_window_id, tab_id, margin_width, padding_width, border_width, layout_opts=''):
def __init__(self, os_window_id, tab_id, margin_width, single_window_margin_width, padding_width, border_width, layout_opts=''):
self.os_window_id = os_window_id
self.tab_id = tab_id
self.set_active_window_in_os_window = partial(set_active_window, os_window_id, tab_id)
self.swap_windows_in_os_window = partial(swap_windows, os_window_id, tab_id)
self.border_width = border_width
self.margin_width = margin_width
self.single_window_margin_width = single_window_margin_width
self.padding_width = padding_width
# A set of rectangles corresponding to the blank spaces at the edges of
# this layout, i.e. spaces that are not covered by any window
@@ -336,7 +337,8 @@ class Layout: # {{{
# Utils {{{
def layout_single_window(self, w):
wg = layout_single_window(self.margin_width, self.padding_width)
mw = self.margin_width if self.single_window_margin_width < 0 else self.single_window_margin_width
wg = layout_single_window(mw, self.padding_width)
w.set_geometry(0, wg)
self.blank_rects = blank_rects_for_window(w)
@@ -373,7 +375,8 @@ class Stack(Layout): # {{{
only_active_window_visible = True
def do_layout(self, windows, active_window_idx):
wg = layout_single_window(self.margin_width, self.padding_width)
mw = self.margin_width if self.single_window_margin_width < 0 else self.single_window_margin_width
wg = layout_single_window(mw, self.padding_width)
for i, w in enumerate(windows):
w.set_geometry(i, wg)
if w.is_visible_in_layout:
@@ -671,12 +674,13 @@ class Horizontal(Vertical):
all_layouts = {o.name: o for o in globals().values() if isinstance(o, type) and issubclass(o, Layout) and o is not Layout}
def create_layout_object_for(name, os_window_id, tab_id, margin_width, padding_width, border_width, layout_opts=''):
key = name, os_window_id, tab_id, margin_width, padding_width, border_width, layout_opts
def create_layout_object_for(name, os_window_id, tab_id, margin_width, single_window_margin_width, padding_width, border_width, layout_opts=''):
key = name, os_window_id, tab_id, margin_width, single_window_margin_width, padding_width, border_width, layout_opts
ans = create_layout_object_for.cache.get(key)
if ans is None:
name, layout_opts = name.partition(':')[::2]
ans = create_layout_object_for.cache[key] = all_layouts[name](os_window_id, tab_id, margin_width, padding_width, border_width, layout_opts)
ans = create_layout_object_for.cache[key] = all_layouts[name](
os_window_id, tab_id, margin_width, single_window_margin_width, padding_width, border_width, layout_opts)
return ans

View File

@@ -12,7 +12,8 @@ from .boss import Boss
from .cli import create_opts, parse_args
from .config import cached_values_for, initial_window_size_func
from .constants import (
appname, config_dir, glfw_path, is_macos, is_wayland, logo_data_file
appname, config_dir, glfw_path, is_macos, is_wayland, kitty_exe,
logo_data_file
)
from .fast_data_types import (
GLFW_MOD_SUPER, create_os_window, free_font_data, glfw_init,
@@ -69,14 +70,10 @@ def load_all_shaders(semi_transparent=0):
load_borders_program()
def init_graphics(debug_keyboard=False):
def init_glfw(debug_keyboard=False):
glfw_module = 'cocoa' if is_macos else ('wayland' if is_wayland else 'x11')
if debug_keyboard:
os.environ['GLFW_DEBUG_KEYBOARD'] = '1'
if not glfw_init(glfw_path(glfw_module)):
if not glfw_init(glfw_path(glfw_module), debug_keyboard):
raise SystemExit('GLFW initialization failed')
if debug_keyboard:
os.environ.pop('GLFW_DEBUG_KEYBOARD')
return glfw_module
@@ -95,7 +92,7 @@ def get_new_os_window_trigger(opts):
from .fast_data_types import cocoa_set_new_window_trigger
new_os_window_shortcuts.sort(key=prefer_cmd_shortcuts, reverse=True)
for candidate in new_os_window_shortcuts:
if cocoa_set_new_window_trigger(*candidate):
if cocoa_set_new_window_trigger(candidate[0], candidate[2]):
new_os_window_trigger = candidate
break
return new_os_window_trigger
@@ -124,7 +121,7 @@ def _run_app(opts, args):
def run_app(opts, args):
set_scale(opts.box_drawing_scale)
set_options(opts, is_wayland, args.debug_gl, args.debug_font_fallback)
set_font_family(opts)
set_font_family(opts, debug_font_matching=args.debug_font_fallback)
try:
_run_app(opts, args)
finally:
@@ -203,14 +200,8 @@ def _main():
print('Failed to set locale with no LANG, ignoring', file=sys.stderr)
# Ensure kitty is in PATH
rpath = getattr(sys, 'bundle_exe_dir', None)
rpath = os.path.dirname(kitty_exe())
items = frozenset(os.environ['PATH'].split(os.pathsep))
if not rpath:
for candidate in items:
if os.access(os.path.join(candidate, 'kitty'), os.X_OK):
break
else:
rpath = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'launcher')
if rpath and rpath not in items:
os.environ['PATH'] += os.pathsep + rpath
@@ -227,6 +218,7 @@ def _main():
args, rest = parse_args(args=args)
args.args = rest
if args.debug_config:
init_glfw(args.debug_keyboard) # needed for parsing native keysyms
create_opts(args, debug_config=True)
return
if getattr(args, 'detach', False):
@@ -240,10 +232,10 @@ def _main():
if not is_first:
talk_to_instance(args)
return
init_glfw(args.debug_keyboard) # needed for parsing native keysyms
opts = create_opts(args)
if opts.editor != '.':
os.environ['EDITOR'] = opts.editor
init_graphics(args.debug_keyboard)
try:
with setup_profiling(args):
# Avoid needing to launch threads to reap zombies

View File

@@ -11,6 +11,7 @@ from functools import partial
from .cli import emph, parse_args
from .cmds import cmap, parse_subcommand_cli
from .constants import appname, version
from .fast_data_types import read_command_response
from .utils import TTYIO, parse_address_spec
@@ -57,7 +58,10 @@ class SocketIO:
def __exit__(self, *a):
import socket
self.socket.shutdown(socket.SHUT_RDWR)
try:
self.socket.shutdown(socket.SHUT_RDWR)
except EnvironmentError:
pass # on some OSes such as macOS the socket is already closed at this point
self.socket.close()
def send(self, data):
@@ -73,13 +77,23 @@ class SocketIO:
out.flush()
self.socket.shutdown(socket.SHUT_WR)
def recv(self, more_needed, timeout):
# We dont bother with more_needed since the server will close the
# connection after transmission
def recv(self, timeout):
dcs = re.compile(br'\x1bP@kitty-cmd([^\x1b]+)\x1b\\')
self.socket.settimeout(timeout)
with self.socket.makefile('rb') as src:
data = src.read()
more_needed(data)
m = dcs.search(data)
if m is None:
raise TimeoutError('Timed out while waiting to read cmd response')
return m.group(1)
class RCIO(TTYIO):
def recv(self, timeout):
ans = []
read_command_response(self.tty_fd, timeout, ans)
return b''.join(ans)
def do_io(to, send, no_response):
@@ -93,28 +107,14 @@ def do_io(to, send, no_response):
yield encode_send(send)
send_data = send_generator()
io = SocketIO(to) if to else TTYIO()
io = SocketIO(to) if to else RCIO()
with io:
io.send(send_data)
if no_response:
return {'ok': True}
received = io.recv(timeout=10)
dcs = re.compile(br'\x1bP@kitty-cmd([^\x1b]+)\x1b\\')
received = b''
match = None
def more_needed(data):
nonlocal received, match
received += data
match = dcs.search(received)
return match is None
io.recv(more_needed, timeout=10)
if match is None:
raise SystemExit('Failed to receive response from ' + appname)
response = json.loads(match.group(1).decode('ascii'))
response = json.loads(received.decode('ascii'))
return response

View File

@@ -1674,7 +1674,7 @@ static PyObject*
_select_graphic_rendition(Screen *self, PyObject *args) {
unsigned int params[256] = {0};
for (int i = 0; i < PyTuple_GET_SIZE(args); i++) { params[i] = PyLong_AsUnsignedLong(PyTuple_GET_ITEM(args, i)); }
select_graphic_rendition(self, params, PyList_GET_SIZE(args), NULL);
select_graphic_rendition(self, params, PyTuple_GET_SIZE(args), NULL);
Py_RETURN_NONE;
}

View File

@@ -5,7 +5,7 @@
import shlex
from .config_data import to_layout_names
from .constants import shell_path
from .constants import shell_path, kitty_exe
from .layout import all_layouts
from .utils import log_error
@@ -124,6 +124,8 @@ def create_session(opts, args=None, special_window=None, cwd_from=None, respect_
ans.tabs[-1].layout = current_layout
if special_window is None:
cmd = args.args if args and args.args else resolved_shell(opts)
if args and args.hold:
cmd = [kitty_exe(), '+hold'] + cmd
from kitty.tabs import SpecialWindow
k = {'cwd_from': cwd_from}
if respect_cwd:

View File

@@ -84,8 +84,10 @@ class Completer:
return self.matches[state]
def __enter__(self):
if os.path.exists(self.history_path):
try:
readline.read_history_file(self.history_path)
except Exception:
pass
readline.set_completer(self.complete)
delims = readline.get_completer_delims()
readline.set_completer_delims(delims.replace('-', ''))

View File

@@ -325,8 +325,9 @@ set_special_keys(PyObject *dict) {
dict_iter(dict) {
if (!PyTuple_Check(key)) { PyErr_SetString(PyExc_TypeError, "dict keys for special keys must be tuples"); return; }
int mods = PyLong_AsLong(PyTuple_GET_ITEM(key, 0));
int glfw_key = PyLong_AsLong(PyTuple_GET_ITEM(key, 1));
set_special_key_combo(glfw_key, mods);
bool is_native = PyTuple_GET_ITEM(key, 1) == Py_True;
int glfw_key = PyLong_AsLong(PyTuple_GET_ITEM(key, 2));
set_special_key_combo(glfw_key, mods, is_native);
}}
}

View File

@@ -37,8 +37,9 @@ class Tab: # {{{
if not self.id:
raise Exception('No OS window with id {} found, or tab counter has wrapped'.format(self.os_window_id))
self.opts, self.args = tab_manager.opts, tab_manager.args
self.margin_width, self.padding_width = pt_to_px(
self.opts.window_margin_width, self.os_window_id), pt_to_px(self.opts.window_padding_width, self.os_window_id)
self.margin_width, self.padding_width, self.single_window_margin_width = map(
lambda x: pt_to_px(getattr(self.opts, x), self.os_window_id), (
'window_margin_width', 'window_padding_width', 'single_window_margin_width'))
self.name = getattr(session_tab, 'name', '')
self.enabled_layouts = [x.lower() for x in getattr(session_tab, 'enabled_layouts', None) or self.opts.enabled_layouts]
self.borders = Borders(self.os_window_id, self.id, self.opts, pt_to_px(self.opts.window_border_width, self.os_window_id), self.padding_width)
@@ -139,7 +140,10 @@ class Tab: # {{{
w.change_titlebar_color()
def create_layout_object(self, name):
return create_layout_object_for(name, self.os_window_id, self.id, self.margin_width, self.padding_width, self.borders.border_width)
return create_layout_object_for(
name, self.os_window_id, self.id, self.margin_width,
self.single_window_margin_width, self.padding_width,
self.borders.border_width)
def next_layout(self):
if len(self.enabled_layouts) > 1:

View File

@@ -39,7 +39,7 @@ def safe_print(*a, **k):
def log_error(*a, **k):
try:
msg = k.get('sep', ' ').join(map(str, a)) + k.get('end', '\n')
msg = k.get('sep', ' ').join(map(str, a)) + k.get('end', '')
log_error_string(msg.replace('\0', ''))
except Exception:
pass
@@ -369,7 +369,7 @@ def write_all(fd, data):
class TTYIO:
def __enter__(self):
self.tty_fd, self.original_termios = open_tty()
self.tty_fd, self.original_termios = open_tty(True)
return self
def __exit__(self, *a):
@@ -382,22 +382,28 @@ class TTYIO:
for chunk in data:
write_all(self.tty_fd, chunk)
def recv(self, more_needed, timeout):
from io import DEFAULT_BUFFER_SIZE
import select
def recv(self, more_needed, timeout, sz=1):
fd = self.tty_fd
start_time = monotonic()
while timeout > monotonic() - start_time:
rd = select.select([fd], [], [], max(0, timeout - (monotonic() - start_time)))[0]
if not rd:
break
data = os.read(fd, DEFAULT_BUFFER_SIZE)
if not data:
break # eof
if not more_needed(data):
# will block for 0.1 secs waiting for data because we have set
# VMIN=0 VTIME=1 in termios
data = os.read(fd, sz)
if data and not more_needed(data):
break
def natsort_ints(iterable):
def convert(text):
return int(text) if text.isdigit() else text
def alphanum_key(key):
return tuple(map(convert, re.split(r'(\d+)', key)))
return sorted(iterable, key=alphanum_key)
def get_editor():
import shlex
return shlex.split(os.environ.get('EDITOR', 'vim'))

View File

@@ -133,6 +133,10 @@ class Window:
cwd=self.child.current_cwd or self.child.cwd, cmdline=self.child.cmdline
)
@property
def current_colors(self):
return self.screen.color_profile.as_dict()
def matches(self, field, pat):
if field == 'id':
return pat.pattern == str(self.id)
@@ -434,7 +438,7 @@ class Window:
if self.screen.in_bracketed_paste_mode:
while True:
new_text = text.replace(b'\033[201~', b'').replace(b'\x9b201~', b'')
if text == new_text:
if len(text) == len(new_text):
break
text = new_text
self.screen.paste(text)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 463 KiB

16
kitty_tests/gr.py Normal file → Executable file
View File

@@ -49,24 +49,24 @@ def display_png_file(path):
def main():
photo = sys.argv[-1]
base = os.path.dirname(os.path.abspath(__file__))
if not photo.lower().endswith('.png'):
raise SystemExit('Must specify a PNG file to display')
clear_screen()
display(b'\xdd\xdd\xdd\xff', 1, 1, 0, 0, -10, 40, 20)
display(open('logo/kitty.rgba', 'rb').read(), 256, 256, 0, 5, -9)
display(open(os.path.join(base, '../logo/kitty.rgba'), 'rb').read(), 256, 256, 0, 5, -9)
display(b'\0\0\0\xaa', 1, 1, 0, 7, -8, 40, 3)
move_cursor(5, 8)
print('kitty is \033[3m\033[32mawesome\033[m!')
move_cursor(0, 21)
print('Lenna...')
display_png_file('kitty_tests/Lenna.png')
print('Photo...')
display_png_file(photo)
try:
try:
raw_input()
except NameError:
input()
input()
except (EOFError, KeyboardInterrupt):
pass
if __name__ == '__main__':
os.chdir(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
main()

View File

@@ -27,7 +27,7 @@ def create_layout(cls, opts=None, border_width=2):
if opts is None:
opts = defaults
mw, pw = map(pt_to_px, (opts.window_margin_width, opts.window_padding_width))
ans = cls(1, 1, mw, pw, border_width)
ans = cls(1, 1, mw, mw, pw, border_width)
ans.set_active_window_in_os_window = lambda idx: None
ans.swap_windows_in_os_window = lambda a, b: None
return ans

View File

@@ -1,832 +0,0 @@
#!/bin/sh
# linux-installer.sh
# Copyright (C) 2018 Kovid Goyal <kovid at kovidgoyal.net>
search_for_python() {
# We have to search for python as Ubuntu, in its infinite wisdom decided
# to release 18.04 with no python symlink, making it impossible to run polyglot
# python scripts.
# We cannot use command -v as it is not implemented in the posh shell shipped with
# Ubuntu/Debian. Similarly, there is no guarantee that which is installed.
# Shell scripting is a horrible joke, thank heavens for python.
local IFS=:
if [ $ZSH_VERSION ]; then
# zsh does not split by default
setopt sh_word_split
fi
local candidate_path
local candidate_python
local pythons=python3:python2
# disable pathname expansion (globbing)
set -f
for candidate_path in $PATH
do
if [ ! -z $candidate_path ]
then
for candidate_python in $pythons
do
if [ ! -z "$candidate_path" ]
then
if [ -x "$candidate_path/$candidate_python" ]
then
printf "$candidate_path/$candidate_python"
return
fi
fi
done
fi
done
set +f
printf "python"
}
PYTHON=$(search_for_python)
echo Using python executable: $PYTHON
$PYTHON -c "import sys; script_launch=lambda:sys.exit('Download of installer failed!'); exec(sys.stdin.read()); script_launch()" "$@" <<'CALIBRE_LINUX_INSTALLER_HEREDOC'
# {{{
# HEREDOC_START
#!/usr/bin/env python2
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import sys, os, shutil, subprocess, re, platform, signal, tempfile, hashlib, errno
import ssl, socket, stat
from contextlib import closing
is64bit = platform.architecture()[0] == '64bit'
DLURL = 'https://calibre-ebook.com/dist/linux'+('64' if is64bit else '32')
DLURL = os.environ.get('CALIBRE_INSTALLER_LOCAL_URL', DLURL)
py3 = sys.version_info[0] > 2
enc = getattr(sys.stdout, 'encoding', 'utf-8') or 'utf-8'
if enc.lower() == 'ascii':
enc = 'utf-8'
calibre_version = signature = None
urllib = __import__('urllib.request' if py3 else 'urllib', fromlist=1)
has_ssl_verify = hasattr(ssl, 'create_default_context')
if py3:
unicode = str
raw_input = input
from urllib.parse import urlparse
import http.client as httplib
encode_for_subprocess = lambda x:x
else:
from future_builtins import map
from urlparse import urlparse
import httplib
def encode_for_subprocess(x):
if isinstance(x, unicode):
x = x.encode(enc)
return x
class TerminalController: # {{{
BOL = '' #: Move the cursor to the beginning of the line
UP = '' #: Move the cursor up one line
DOWN = '' #: Move the cursor down one line
LEFT = '' #: Move the cursor left one char
RIGHT = '' #: Move the cursor right one char
# Deletion:
CLEAR_SCREEN = '' #: Clear the screen and move to home position
CLEAR_EOL = '' #: Clear to the end of the line.
CLEAR_BOL = '' #: Clear to the beginning of the line.
CLEAR_EOS = '' #: Clear to the end of the screen
# Output modes:
BOLD = '' #: Turn on bold mode
BLINK = '' #: Turn on blink mode
DIM = '' #: Turn on half-bright mode
REVERSE = '' #: Turn on reverse-video mode
NORMAL = '' #: Turn off all modes
# Cursor display:
HIDE_CURSOR = '' #: Make the cursor invisible
SHOW_CURSOR = '' #: Make the cursor visible
# Terminal size:
COLS = None #: Width of the terminal (None for unknown)
LINES = None #: Height of the terminal (None for unknown)
# Foreground colors:
BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = ''
# Background colors:
BG_BLACK = BG_BLUE = BG_GREEN = BG_CYAN = ''
BG_RED = BG_MAGENTA = BG_YELLOW = BG_WHITE = ''
_STRING_CAPABILITIES = """
BOL=cr UP=cuu1 DOWN=cud1 LEFT=cub1 RIGHT=cuf1
CLEAR_SCREEN=clear CLEAR_EOL=el CLEAR_BOL=el1 CLEAR_EOS=ed BOLD=bold
BLINK=blink DIM=dim REVERSE=rev UNDERLINE=smul NORMAL=sgr0
HIDE_CURSOR=cinvis SHOW_CURSOR=cnorm""".split()
_COLORS = """BLACK BLUE GREEN CYAN RED MAGENTA YELLOW WHITE""".split()
_ANSICOLORS = "BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE".split()
def __init__(self, term_stream=sys.stdout):
# Curses isn't available on all platforms
try:
import curses
except:
return
# If the stream isn't a tty, then assume it has no capabilities.
if not hasattr(term_stream, 'isatty') or not term_stream.isatty():
return
# Check the terminal type. If we fail, then assume that the
# terminal has no capabilities.
try:
curses.setupterm()
except:
return
# Look up numeric capabilities.
self.COLS = curses.tigetnum('cols')
self.LINES = curses.tigetnum('lines')
# Look up string capabilities.
for capability in self._STRING_CAPABILITIES:
(attrib, cap_name) = capability.split('=')
setattr(self, attrib, self._escape_code(self._tigetstr(cap_name)))
# Colors
set_fg = self._tigetstr('setf')
if set_fg:
if not isinstance(set_fg, bytes):
set_fg = set_fg.encode('utf-8')
for i,color in zip(range(len(self._COLORS)), self._COLORS):
setattr(self, color,
self._escape_code(curses.tparm((set_fg), i)))
set_fg_ansi = self._tigetstr('setaf')
if set_fg_ansi:
if not isinstance(set_fg_ansi, bytes):
set_fg_ansi = set_fg_ansi.encode('utf-8')
for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
setattr(self, color,
self._escape_code(curses.tparm((set_fg_ansi),
i)))
set_bg = self._tigetstr('setb')
if set_bg:
if not isinstance(set_bg, bytes):
set_bg = set_bg.encode('utf-8')
for i,color in zip(range(len(self._COLORS)), self._COLORS):
setattr(self, 'BG_'+color,
self._escape_code(curses.tparm((set_bg), i)))
set_bg_ansi = self._tigetstr('setab')
if set_bg_ansi:
if not isinstance(set_bg_ansi, bytes):
set_bg_ansi = set_bg_ansi.encode('utf-8')
for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
setattr(self, 'BG_'+color,
self._escape_code(curses.tparm((set_bg_ansi),
i)))
def _escape_code(self, raw):
if not raw:
raw = ''
if not isinstance(raw, unicode):
raw = raw.decode('ascii')
return raw
def _tigetstr(self, cap_name):
# String capabilities can include "delays" of the form "$<2>".
# For any modern terminal, we should be able to just ignore
# these, so strip them out.
import curses
if isinstance(cap_name, bytes):
cap_name = cap_name.decode('utf-8')
cap = self._escape_code(curses.tigetstr(cap_name))
return re.sub(r'\$<\d+>[/*]?', b'', cap)
def render(self, template):
return re.sub(r'\$\$|\${\w+}', self._render_sub, template)
def _render_sub(self, match):
s = match.group()
if s == '$$':
return s
else:
return getattr(self, s[2:-1])
class ProgressBar:
BAR = '%3d%% ${GREEN}[${BOLD}%s%s${NORMAL}${GREEN}]${NORMAL}\n'
HEADER = '${BOLD}${CYAN}%s${NORMAL}\n\n'
def __init__(self, term, header):
self.term = term
if not (self.term.CLEAR_EOL and self.term.UP and self.term.BOL):
raise ValueError("Terminal isn't capable enough -- you "
"should use a simpler progress display.")
self.width = self.term.COLS or 75
self.bar = term.render(self.BAR)
self.header = self.term.render(self.HEADER % header.center(self.width))
self.cleared = 1 # : true if we haven't drawn the bar yet.
def update(self, percent, message=''):
out = (sys.stdout.buffer if py3 else sys.stdout)
if self.cleared:
out.write(self.header.encode(enc))
self.cleared = 0
n = int((self.width-10)*percent)
msg = message.center(self.width)
msg = (self.term.BOL + self.term.UP + self.term.CLEAR_EOL + (
self.bar % (100*percent, '='*n, '-'*(self.width-10-n))) + self.term.CLEAR_EOL + msg).encode(enc)
out.write(msg)
out.flush()
def clear(self):
out = (sys.stdout.buffer if py3 else sys.stdout)
if not self.cleared:
out.write((self.term.BOL + self.term.CLEAR_EOL + self.term.UP + self.term.CLEAR_EOL + self.term.UP + self.term.CLEAR_EOL).encode(enc))
self.cleared = 1
out.flush()
# }}}
def prints(*args, **kwargs): # {{{
f = kwargs.get('file', sys.stdout.buffer if py3 else sys.stdout)
end = kwargs.get('end', b'\n')
enc = getattr(f, 'encoding', 'utf-8') or 'utf-8'
if isinstance(end, unicode):
end = end.encode(enc)
for x in args:
if isinstance(x, unicode):
x = x.encode(enc)
f.write(x)
f.write(b' ')
f.write(end)
if py3 and f is sys.stdout.buffer:
f.flush()
# }}}
class Reporter: # {{{
def __init__(self, fname):
try:
self.pb = ProgressBar(TerminalController(), 'Downloading '+fname)
except ValueError:
prints('Downloading', fname)
self.pb = None
self.last_percent = 0
def __call__(self, blocks, block_size, total_size):
percent = (blocks*block_size)/float(total_size)
if self.pb is None:
if percent - self.last_percent > 0.05:
self.last_percent = percent
prints('Downloaded {0:%}'.format(percent))
else:
try:
self.pb.update(percent)
except:
import traceback
traceback.print_exc()
# }}}
# Downloading {{{
def clean_cache(cache, fname):
for x in os.listdir(cache):
if fname not in x:
os.remove(os.path.join(cache, x))
def check_signature(dest, signature):
if not os.path.exists(dest):
return None
m = hashlib.sha512()
with open(dest, 'rb') as f:
raw = f.read()
m.update(raw)
if m.hexdigest().encode('ascii') == signature:
return raw
class URLOpener(urllib.FancyURLopener):
def http_error_206(self, url, fp, errcode, errmsg, headers, data=None):
''' 206 means partial content, ignore it '''
pass
def do_download(dest):
prints('Will download and install', os.path.basename(dest))
reporter = Reporter(os.path.basename(dest))
offset = 0
urlopener = URLOpener()
if os.path.exists(dest):
offset = os.path.getsize(dest)
# Get content length and check if range is supported
rq = urllib.urlopen(DLURL)
headers = rq.info()
size = int(headers['content-length'])
accepts_ranges = headers.get('accept-ranges', None) == 'bytes'
mode = 'wb'
if accepts_ranges and offset > 0:
rurl = rq.geturl()
mode = 'ab'
rq.close()
urlopener.addheader('Range', 'bytes=%s-'%offset)
rq = urlopener.open(rurl)
with open(dest, mode) as f:
while f.tell() < size:
raw = rq.read(8192)
if not raw:
break
f.write(raw)
reporter(f.tell(), 1, size)
rq.close()
if os.path.getsize(dest) < size:
print ('Download failed, try again later')
raise SystemExit(1)
prints('Downloaded %s bytes'%os.path.getsize(dest))
def download_tarball():
fname = 'calibre-%s-i686.%s'%(calibre_version, 'txz')
if is64bit:
fname = fname.replace('i686', 'x86_64')
tdir = tempfile.gettempdir()
cache = os.path.join(tdir, 'calibre-installer-cache')
if not os.path.exists(cache):
os.makedirs(cache)
clean_cache(cache, fname)
dest = os.path.join(cache, fname)
raw = check_signature(dest, signature)
if raw is not None:
print ('Using previously downloaded', fname)
return raw
cached_sigf = dest +'.signature'
cached_sig = None
if os.path.exists(cached_sigf):
with open(cached_sigf, 'rb') as sigf:
cached_sig = sigf.read()
if cached_sig != signature and os.path.exists(dest):
os.remove(dest)
try:
with open(cached_sigf, 'wb') as f:
f.write(signature)
except IOError as e:
if e.errno != errno.EACCES:
raise
print ('The installer cache directory has incorrect permissions.'
' Delete %s and try again.'%cache)
raise SystemExit(1)
do_download(dest)
prints('Checking downloaded file integrity...')
raw = check_signature(dest, signature)
if raw is None:
os.remove(dest)
print ('The downloaded files\' signature does not match. '
'Try the download again later.')
raise SystemExit(1)
return raw
# }}}
# Get tarball signature securely {{{
def get_proxies(debug=True):
proxies = urllib.getproxies()
for key, proxy in list(proxies.items()):
if not proxy or '..' in proxy:
del proxies[key]
continue
if proxy.startswith(key+'://'):
proxy = proxy[len(key)+3:]
if key == 'https' and proxy.startswith('http://'):
proxy = proxy[7:]
if proxy.endswith('/'):
proxy = proxy[:-1]
if len(proxy) > 4:
proxies[key] = proxy
else:
prints('Removing invalid', key, 'proxy:', proxy)
del proxies[key]
if proxies and debug:
prints('Using proxies:', repr(proxies))
return proxies
class HTTPError(ValueError):
def __init__(self, url, code):
msg = '%s returned an unsupported http response code: %d (%s)' % (
url, code, httplib.responses.get(code, None))
ValueError.__init__(self, msg)
self.code = code
self.url = url
class CertificateError(ValueError):
pass
def _dnsname_match(dn, hostname, max_wildcards=1):
"""Matching according to RFC 6125, section 6.4.3
http://tools.ietf.org/html/rfc6125#section-6.4.3
"""
pats = []
if not dn:
return False
parts = dn.split(r'.')
leftmost, remainder = parts[0], parts[1:]
wildcards = leftmost.count('*')
if wildcards > max_wildcards:
# Issue #17980: avoid denials of service by refusing more
# than one wildcard per fragment. A survery of established
# policy among SSL implementations showed it to be a
# reasonable choice.
raise CertificateError(
"too many wildcards in certificate DNS name: " + repr(dn))
# speed up common case w/o wildcards
if not wildcards:
return dn.lower() == hostname.lower()
# RFC 6125, section 6.4.3, subitem 1.
# The client SHOULD NOT attempt to match a presented identifier in which
# the wildcard character comprises a label other than the left-most label.
if leftmost == '*':
# When '*' is a fragment by itself, it matches a non-empty dotless
# fragment.
pats.append('[^.]+')
elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
# RFC 6125, section 6.4.3, subitem 3.
# The client SHOULD NOT attempt to match a presented identifier
# where the wildcard character is embedded within an A-label or
# U-label of an internationalized domain name.
pats.append(re.escape(leftmost))
else:
# Otherwise, '*' matches any dotless string, e.g. www*
pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
# add the remaining fragments, ignore any wildcards
for frag in remainder:
pats.append(re.escape(frag))
pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
return pat.match(hostname)
def match_hostname(cert, hostname):
"""Verify that *cert* (in decoded format as returned by
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125
rules are followed, but IP addresses are not accepted for *hostname*.
CertificateError is raised on failure. On success, the function
returns nothing.
"""
if not cert:
raise ValueError("empty or no certificate")
dnsnames = []
san = cert.get('subjectAltName', ())
for key, value in san:
if key == 'DNS':
if _dnsname_match(value, hostname):
return
dnsnames.append(value)
if not dnsnames:
# The subject is only checked when there is no dNSName entry
# in subjectAltName
for sub in cert.get('subject', ()):
for key, value in sub:
# XXX according to RFC 2818, the most specific Common Name
# must be used.
if key == 'commonName':
if _dnsname_match(value, hostname):
return
dnsnames.append(value)
if len(dnsnames) > 1:
raise CertificateError("hostname %r "
"doesn't match either of %s"
% (hostname, ', '.join(map(repr, dnsnames))))
elif len(dnsnames) == 1:
# python 2.7.2 does not read subject alt names thanks to this
# bug: http://bugs.python.org/issue13034
# And the utter lunacy that is the linux landscape could have
# any old version of python whatsoever with or without a hot fix for
# this bug. Not to mention that python 2.6 may or may not
# read alt names depending on its patchlevel. So we just bail on full
# verification if the python version is less than 2.7.3.
# Linux distros are one enormous, honking disaster.
if sys.version_info[:3] < (2, 7, 3) and dnsnames[0] == 'calibre-ebook.com':
return
raise CertificateError("hostname %r "
"doesn't match %r"
% (hostname, dnsnames[0]))
else:
raise CertificateError("no appropriate commonName or "
"subjectAltName fields were found")
if has_ssl_verify:
class HTTPSConnection(httplib.HTTPSConnection):
def __init__(self, ssl_version, *args, **kwargs):
kwargs['context'] = ssl.create_default_context(cafile=kwargs.pop('cert_file'))
httplib.HTTPSConnection.__init__(self, *args, **kwargs)
else:
class HTTPSConnection(httplib.HTTPSConnection):
def __init__(self, ssl_version, *args, **kwargs):
httplib.HTTPSConnection.__init__(self, *args, **kwargs)
self.calibre_ssl_version = ssl_version
def connect(self):
"""Connect to a host on a given (SSL) port, properly verifying the SSL
certificate, both that it is valid and that its declared hostnames
match the hostname we are connecting to."""
if hasattr(self, 'source_address'):
sock = socket.create_connection((self.host, self.port),
self.timeout, self.source_address)
else:
# python 2.6 has no source_address
sock = socket.create_connection((self.host, self.port), self.timeout)
if self._tunnel_host:
self.sock = sock
self._tunnel()
self.sock = ssl.wrap_socket(sock, cert_reqs=ssl.CERT_REQUIRED, ca_certs=self.cert_file, ssl_version=self.calibre_ssl_version)
getattr(ssl, 'match_hostname', match_hostname)(self.sock.getpeercert(), self.host)
CACERT = b'''\
-----BEGIN CERTIFICATE-----
MIIFzjCCA7agAwIBAgIJAKfuFL6Cvpn4MA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNV
BAYTAklOMRQwEgYDVQQIDAtNYWhhcmFzaHRyYTEPMA0GA1UEBwwGTXVtYmFpMRAw
DgYDVQQKDAdjYWxpYnJlMRowGAYDVQQDDBFjYWxpYnJlLWVib29rLmNvbTAgFw0x
NTEyMjMwNTQ2NTlaGA8yMTE1MTEyOTA1NDY1OVowYjELMAkGA1UEBhMCSU4xFDAS
BgNVBAgMC01haGFyYXNodHJhMQ8wDQYDVQQHDAZNdW1iYWkxEDAOBgNVBAoMB2Nh
bGlicmUxGjAYBgNVBAMMEWNhbGlicmUtZWJvb2suY29tMIICIjANBgkqhkiG9w0B
AQEFAAOCAg8AMIICCgKCAgEAtlbeAxQKyWhoxwaGqMh5ktRhqsLR6uzjuqWmB+Mm
fC0Ni45mOSo2R/usFQTZesrYUoo2yBhMN58CsLeuaaQfsPeDss7zJ9jX0v/GYUS3
vM7qE55ruRWu0g11NpuWLZkqvcw5gVi3ZJYx/yqTEGlCDGxjXVs9iEg+L75Bcm9y
87olbcZA6H/CbR5lP1/tXcyyb1TBINuTcg408SnieY/HpnA1r3NQB9MwfScdX08H
TB0Bc8e0qz+r1BNi3wZZcrNpqWhw6X9QkHigGaDNppmWqc1Q5nxxk2rC21GRg56n
p6t3ENQMctE3KTJfR8TwM33N/dfcgobDZ/ZTnogqdFQycFOgvT4mIZsXdsJv6smy
hlkUqye2PV8XcTNJr+wRzIN/+u23jC+CaT0U0A57D8PUZVhT+ZshXjB91Ko8hLE1
SmJkdv2bxFV42bsemhSxZWCtsc2Nv8/Ds+WVV00xfADym+LokzEqqfcK9vkkMGzF
h7wzd7YqPOrMGOCe9vH1CoL3VO5srPV+0Mp1fjIGgm5SIhklyRfaeIjFeyoDRA6e
8EXrI3xOsrkXXMJDvhndEJOYYqplY+4kLhW0XeTZjK7CmD59xRtFYWaV3dcMlaWb
VxuY7dgsiO7iUztYY0To5ZDExcHem7PEPUTyFii9LhbcSJeXDaqPFMxih+X0iqKv
ni8CAwEAAaOBhDCBgTAxBgNVHREEKjAoghFjYWxpYnJlLWVib29rLmNvbYITKi5j
YWxpYnJlLWVib29rLmNvbTAdBgNVHQ4EFgQURWqz5EOg5K1OrSKpleR+louVxsQw
HwYDVR0jBBgwFoAURWqz5EOg5K1OrSKpleR+louVxsQwDAYDVR0TBAUwAwEB/zAN
BgkqhkiG9w0BAQsFAAOCAgEAS1+Jx0VyTrEFUQ5jEIx/7WrL4GDnzxjeXWJTyKSk
YqcOvXZpwwrTHJSGHj7MpCqWIzQnHxICBFlUEVcb1g1UPvNB5OY69eLjlYdwfOK9
bfp/KnLCsn7Pf4UCATRslX9J1LV6r17X2ONWWmSutDeGP1azXVxwFsogvvqwPHCs
nlfvQycUcd4HWIZWBJ1n4Ry6OwdpFuHktRVtNtTlD34KUjzcN2GCA08Ur+1eiA9D
/Oru1X4hfA3gbiAlGJ/+3AQw0oYS0IEW1HENurkIDNs98CXTiau9OXRECgGjE3hC
viECb4beyhEOH5y1dQJZEynwvSepFG8wDJWmkVN7hMrfbZF4Ec0BmsJpbuq5GrdV
cXUXJbLrnADFV9vkciLb3pl7gAmHi1T19i/maWMiYqIAh7Ezi/h6ufGbPiG+vfLt
f4ywTKQeQKAamBW4P2oFgcmlPDlDeVFWdkF1aC0WFct5/R7Fea0D2bOVt52zm3v3
Ghni3NYEZzXHf08c8tzXZmM1Q39sSS1vn2B9PgiYj87Xg9Fxn1trKFdsiry1F2Qx
qDq1u+xTdjPKwVVB1zd5g3MM/YYTVRhuH2AZU/Z4qX8DAf9ESqLqUpEOpyvLkX3r
gENtRgsmhjlf/Qwymuz8nnzJD5c4TgCicVjPNArprVtmyfOXLVXJLC+KpkzTxvdr
nR0=
-----END CERTIFICATE-----
'''
def get_https_resource_securely(url, timeout=60, max_redirects=5, ssl_version=None):
'''
Download the resource pointed to by url using https securely (verify server
certificate). Ensures that redirects, if any, are also downloaded
securely. Needs a CA certificates bundle (in PEM format) to verify the
server's certificates.
'''
if ssl_version is None:
try:
ssl_version = ssl.PROTOCOL_TLSv1_2
except AttributeError:
ssl_version = ssl.PROTOCOL_TLSv1 # old python
with tempfile.NamedTemporaryFile(prefix='calibre-ca-cert-') as f:
f.write(CACERT)
f.flush()
p = urlparse(url)
if p.scheme != 'https':
raise ValueError('URL %s scheme must be https, not %r' % (url, p.scheme))
hostname, port = p.hostname, p.port
proxies = get_proxies()
has_proxy = False
for q in ('https', 'http'):
if q in proxies:
try:
h, po = proxies[q].rpartition(':')[::2]
po = int(po)
if h:
hostname, port, has_proxy = h, po, True
break
except Exception:
# Invalid proxy, ignore
pass
c = HTTPSConnection(ssl_version, hostname, port, cert_file=f.name, timeout=timeout)
if has_proxy:
c.set_tunnel(p.hostname, p.port)
with closing(c):
c.connect() # This is needed for proxy connections
path = p.path or '/'
if p.query:
path += '?' + p.query
c.request('GET', path)
response = c.getresponse()
if response.status in (httplib.MOVED_PERMANENTLY, httplib.FOUND, httplib.SEE_OTHER):
if max_redirects <= 0:
raise ValueError('Too many redirects, giving up')
newurl = response.getheader('Location', None)
if newurl is None:
raise ValueError('%s returned a redirect response with no Location header' % url)
return get_https_resource_securely(
newurl, timeout=timeout, max_redirects=max_redirects-1, ssl_version=ssl_version)
if response.status != httplib.OK:
raise HTTPError(url, response.status)
return response.read()
# }}}
def extract_tarball(raw, destdir):
prints('Extracting application files...')
with open('/dev/null', 'w') as null:
p = subprocess.Popen(
list(map(encode_for_subprocess, ['tar', 'xJof', '-', '-C', destdir])),
stdout=null, stdin=subprocess.PIPE, close_fds=True, preexec_fn=lambda:
signal.signal(signal.SIGPIPE, signal.SIG_DFL))
p.stdin.write(raw)
p.stdin.close()
if p.wait() != 0:
prints('Extracting of application files failed with error code: %s' % p.returncode)
raise SystemExit(1)
def get_tarball_info():
global signature, calibre_version
print ('Downloading tarball signature securely...')
raw = get_https_resource_securely(
'https://code.calibre-ebook.com/tarball-info/' + ('x86_64' if is64bit else 'i686'))
signature, calibre_version = raw.rpartition(b'@')[::2]
if not signature or not calibre_version:
raise ValueError('Failed to get install file signature, invalid signature returned')
calibre_version = calibre_version.decode('utf-8')
def download_and_extract(destdir):
get_tarball_info()
raw = download_tarball()
if os.path.exists(destdir):
shutil.rmtree(destdir)
os.makedirs(destdir)
print('Extracting files to %s ...'%destdir)
extract_tarball(raw, destdir)
def check_version():
global calibre_version
if calibre_version == '%version':
calibre_version = urllib.urlopen('http://code.calibre-ebook.com/latest').read()
def run_installer(install_dir, isolated, bin_dir, share_dir):
destdir = os.path.abspath(os.path.expanduser(install_dir or '/opt'))
if destdir == '/usr/bin':
prints(destdir, 'is not a valid install location. Choose', end='')
prints('a location like /opt or /usr/local')
return 1
destdir = os.path.realpath(os.path.join(destdir, 'calibre'))
if os.path.exists(destdir):
if not os.path.isdir(destdir):
prints(destdir, 'exists and is not a directory. Choose a location like /opt or /usr/local')
return 1
print ('Installing to', destdir)
download_and_extract(destdir)
if not isolated:
pi = [os.path.join(destdir, 'calibre_postinstall')]
if bin_dir is not None:
pi.extend(['--bindir', bin_dir])
if share_dir is not None:
pi.extend(['--sharedir', share_dir])
subprocess.call(pi)
prints('Run "calibre" to start calibre')
else:
prints('Run "%s/calibre" to start calibre' % destdir)
return 0
def check_umask():
# A bad umask can cause system breakage because of bugs in xdg-mime
# See https://www.mobileread.com/forums/showthread.php?t=277803
mask = os.umask(18) # 18 = 022
os.umask(mask)
forbid_user_read = mask & stat.S_IRUSR
forbid_group_read = mask & stat.S_IRGRP
forbid_other_read = mask & stat.S_IROTH
if forbid_user_read or forbid_group_read or forbid_other_read:
prints(
'WARNING: Your current umask disallows reading of files by some users,'
' this can cause system breakage when running the installer because'
' of bugs in common system utilities.'
)
sys.stdin = open('/dev/tty') # stdin is a pipe from wget
while True:
q = raw_input('Should the installer (f)ix the umask, (i)gnore it or (a)bort [f/i/a Default is abort]: ') or 'a'
if q in 'f i a'.split():
break
prints('Response', q, 'not understood')
if q == 'f':
mask = mask & ~stat.S_IRUSR & ~stat.S_IRGRP & ~stat.S_IROTH
os.umask(mask)
prints('umask changed to: {:03o}'.format(mask))
elif q == 'i':
prints('Ignoring bad umask and proceeding anyway, you have been warned!')
else:
raise SystemExit('The system umask is unsuitable, aborting')
def main(install_dir=None, isolated=False, bin_dir=None, share_dir=None, ignore_umask=False):
if not ignore_umask and not isolated:
check_umask()
machine = os.uname()[4]
if machine and machine.lower().startswith('arm'):
raise SystemExit(
'You are running on an ARM system. The calibre binaries are only'
' available for x86 systems. You will have to compile from'
' source.')
run_installer(install_dir, isolated, bin_dir, share_dir)
try:
__file__
from_file = True
except NameError:
from_file = False
def update_intaller_wrapper():
# To run: python3 -c "import runpy; runpy.run_path('setup/linux-installer.py', run_name='update_wrapper')"
src = open(__file__, 'rb').read().decode('utf-8')
wrapper = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'linux-installer.sh')
with open(wrapper, 'r+b') as f:
raw = f.read().decode('utf-8')
nraw = re.sub(r'^# HEREDOC_START.+^# HEREDOC_END', lambda m: '# HEREDOC_START\n{}\n# HEREDOC_END'.format(src), raw, flags=re.MULTILINE | re.DOTALL)
if 'update_intaller_wrapper()' not in nraw:
raise SystemExit('regex substitute of HEREDOC failed')
f.seek(0), f.truncate()
f.write(nraw.encode('utf-8'))
def script_launch():
def path(x):
return os.path.expanduser(x)
def to_bool(x):
return x.lower() in {'y', 'yes', '1', 'true'}
type_map = {x: path for x in 'install_dir isolated bin_dir share_dir ignore_umask'.split()}
type_map['isolated'] = type_map['ignore_umask'] = to_bool
kwargs = {}
for arg in sys.argv[1:]:
if arg:
m = re.match('([a-z_]+)=(.+)', arg)
if m is None:
raise SystemExit('Unrecognized command line argument: ' + arg)
k = m.group(1)
if k not in type_map:
raise SystemExit('Unrecognized command line argument: ' + arg)
kwargs[k] = type_map[k](m.group(2))
main(**kwargs)
if __name__ == '__main__' and from_file:
main()
elif __name__ == 'update_wrapper':
update_intaller_wrapper()
# HEREDOC_END
# }}}
CALIBRE_LINUX_INSTALLER_HEREDOC