Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bf0e0ea23 | ||
|
|
c7879d06c9 | ||
|
|
05227e7dfb | ||
|
|
9805d164d0 | ||
|
|
05384c46e7 | ||
|
|
c2679eff9a | ||
|
|
3ec66c0b9f | ||
|
|
9277b80fad | ||
|
|
568a4c9b5d | ||
|
|
4de18688ca | ||
|
|
12975347ee | ||
|
|
e35c3cc913 | ||
|
|
65fef81f29 | ||
|
|
e3f60e6fa7 | ||
|
|
0c88e11cd7 | ||
|
|
d689c21283 | ||
|
|
5362d1a7a6 | ||
|
|
3b1490020d | ||
|
|
b8ba25b81f | ||
|
|
8bfdc47998 | ||
|
|
2491fafe98 | ||
|
|
b5bc466aef | ||
|
|
cfd6ea4301 | ||
|
|
2cdf41f190 | ||
|
|
9169d0aa9b | ||
|
|
05a25e3531 | ||
|
|
2852ce9a4d | ||
|
|
20f8f2c401 | ||
|
|
0995187080 | ||
|
|
d10eac1b02 | ||
|
|
2cc2f803f1 | ||
|
|
66a52ac588 | ||
|
|
32f22e0ada | ||
|
|
c037f53534 | ||
|
|
aba84581f5 | ||
|
|
82f6fbff77 | ||
|
|
d16215ab6e | ||
|
|
45b0233f60 | ||
|
|
1ce4366972 | ||
|
|
c8fc21d336 | ||
|
|
5dd3243674 | ||
|
|
815dd99f7c | ||
|
|
bf0dcb1d2e | ||
|
|
031fc10859 | ||
|
|
a644e4c31e | ||
|
|
0398dbdcbe | ||
|
|
84b2fbba28 | ||
|
|
491f233d33 | ||
|
|
5e38af6d24 | ||
|
|
4c0f68150a | ||
|
|
3f25f70783 | ||
|
|
d7007f77aa | ||
|
|
94273941ad | ||
|
|
c09542ea6a | ||
|
|
c5583d380d | ||
|
|
f820f72768 | ||
|
|
6db1f8e3e3 | ||
|
|
2aba3e249c |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -15,3 +15,4 @@ __pycache__
|
||||
glfw/wayland-*-client-protocol.[ch]
|
||||
docs/_build
|
||||
docs/generated
|
||||
.DS_Store
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: %YEAR%, %USER% <%MAIL%>
|
||||
|
||||
%HERE%
|
||||
@@ -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():
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
------------------------------
|
||||
|
||||
|
||||
@@ -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 -------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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``
|
||||
===================== ======================
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
|
||||
1
glfw/cocoa_platform.h
vendored
1
glfw/cocoa_platform.h
vendored
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
1
glfw/glfw3.h
vendored
@@ -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
5
glfw/init.c
vendored
@@ -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
1
glfw/internal.h
vendored
@@ -242,6 +242,7 @@ struct _GLFWerror
|
||||
struct _GLFWinitconfig
|
||||
{
|
||||
GLFWbool hatButtons;
|
||||
GLFWbool debugKeyboard;
|
||||
struct {
|
||||
GLFWbool menubar;
|
||||
GLFWbool chdir;
|
||||
|
||||
2
glfw/wl_init.c
vendored
2
glfw/wl_init.c
vendored
@@ -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
4
glfw/wl_window.c
vendored
@@ -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
4
glfw/x11_window.c
vendored
@@ -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
19
glfw/xkb_glfw.c
vendored
@@ -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
1
glfw/xkb_glfw.h
vendored
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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__'):
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
22
kitty/cli.py
22
kitty/cli.py
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)'''))
|
||||
|
||||
|
||||
@@ -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']))
|
||||
|
||||
@@ -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, ""},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
5656
kitty/gl-wrapper.c
generated
File diff suppressed because it is too large
Load Diff
14
kitty/gl-wrapper.h
generated
14
kitty/gl-wrapper.h
generated
@@ -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
2
kitty/glfw-wrapper.c
generated
@@ -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
5
kitty/glfw-wrapper.h
generated
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
76
kitty/keys.c
76
kitty/keys.c
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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 */
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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('-', ''))
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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
16
kitty_tests/gr.py
Normal file → Executable 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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user