Compare commits

...

107 Commits

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

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

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

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

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

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

This issue was originally filed as #900704.

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

Signed-off-by: Chris Lamb <lamby@debian.org>
2018-06-19 10:10:43 +02:00
Kovid Goyal
0398dbdcbe Fix extra blank lines when using log_error() 2018-06-19 12:51:54 +05:30
Kovid Goyal
84b2fbba28 --debug-font-fallback also prints out what basic fonts were matched
Fixes #656
2018-06-19 12:48:33 +05:30
Kovid Goyal
491f233d33 diff kitten: When triggering a search scroll to the first match automatically 2018-06-19 10:30:52 +05:30
Kovid Goyal
5e38af6d24 When closing a kitty window reset the mouse cursor to its default shape and ensure it is visible. Fixes #655 2018-06-19 10:19:17 +05:30
Kovid Goyal
4c0f68150a Merge branch 'master' of https://github.com/Luflosi/kitty 2018-06-18 18:41:12 +05:30
Luflosi
3f25f70783 Ignore .DS_Store 2018-06-18 14:39:12 +02:00
Kovid Goyal
d7007f77aa DRYer 2018-06-18 12:18:04 +05:30
Kovid Goyal
94273941ad ... 2018-06-18 12:10:42 +05:30
Kovid Goyal
c09542ea6a Speed up reading of command responses 2018-06-18 11:42:32 +05:30
Kovid Goyal
c5583d380d No need to use select() for TTYIO
Instead make read() block with timeout via termios
2018-06-18 11:10:16 +05:30
Kovid Goyal
f820f72768 Add a kitty @ get-colors command 2018-06-18 10:02:05 +05:30
Kovid Goyal
6db1f8e3e3 Dont read more data than needed in TTYIO 2018-06-18 08:36:47 +05:30
Kovid Goyal
2aba3e249c Linux installer: Workaround for bug in 3.0 <= python <= 3.5 2018-06-18 07:21:44 +05:30
Kovid Goyal
f66db65974 version 0.11.1 2018-06-17 10:57:55 +05:30
Kovid Goyal
5b6038db35 Dont include a literal dump of the conf files
Since all the config options are documented separately on the page in
any case, there is not much need for it. And now that the conf files are
downloadable, it is probably nicer to just view/edit them directly in
the users favorite pager/editor rather than the browser.
2018-06-16 22:31:21 +05:30
Kovid Goyal
9a365b0968 Merge branch 'docs' of https://github.com/blueyed/kitty 2018-06-16 22:26:45 +05:30
Daniel Hahler
8a0cd73821 docs: improve generation of default config files
- config files are placed into `docs/generated/conf/`, using the
  expected name, and without any indentation
- `literalinclude::` is used instead of `include::`
- uses `:download:` instead of `:file:` to allow for downloading the
  generated files easily
2018-06-16 18:35:58 +02:00
Kovid Goyal
49b065cbe7 Use conf_dir instead of cwd as base for session file 2018-06-16 10:22:46 +05:30
Kovid Goyal
77f651cb5d Expand env vars for the start_session config variable 2018-06-16 10:18:48 +05:30
Kovid Goyal
fedde51c12 ... 2018-06-16 10:16:06 +05:30
Kovid Goyal
f710d6906e Add a linkcheck target to check links in the docs 2018-06-16 09:27:39 +05:30
Kovid Goyal
c9a751360e Update CHANGELOG 2018-06-16 09:25:00 +05:30
Kovid Goyal
ad97315925 diff kitten: Misc fixes to msg display in the status line 2018-06-16 08:57:02 +05:30
Kovid Goyal
e8f7a2b345 Dont send on_text events with empty text 2018-06-16 08:55:10 +05:30
Kovid Goyal
5bc8d05f0a ... 2018-06-16 08:49:08 +05:30
Kovid Goyal
3669d4135c diff kitten: Add keybindings to search without regex 2018-06-16 08:45:42 +05:30
Kovid Goyal
73b870bb87 ... 2018-06-16 08:41:19 +05:30
Kovid Goyal
6b4217c12a Use a single build script on Travis
Allows the build to fail faster if any of the intermediate steps fails
2018-06-16 08:33:07 +05:30
Kovid Goyal
8b4fe494cf Format matches count the same as scroll percent 2018-06-16 08:24:32 +05:30
Kovid Goyal
35041e7709 Fix docs links 2018-06-16 08:19:33 +05:30
Kovid Goyal
12cbcf1c17 Add an option to kitty.conf to specify a default startup session
Fixes #641
2018-06-16 08:14:11 +05:30
Kovid Goyal
26f50a451e Add a section on configuration to the main page in the docs 2018-06-16 07:45:24 +05:30
Kovid Goyal
11f3d7e215 ... 2018-06-15 17:53:04 +05:30
Kovid Goyal
a7d533cfe5 Increase contrast of link color with text color in docs 2018-06-15 15:38:30 +05:30
Kovid Goyal
52ec4dc21b Allow cross-linking to @ command documentation 2018-06-15 15:31:05 +05:30
Kovid Goyal
6f7ea49008 Switch to high number OSC codes instead of DCS string codes for dynamic color push/pop 2018-06-15 15:21:46 +05:30
Kovid Goyal
84b99b1b3d diff kitten: Clear the search when pressing Esc 2018-06-15 15:00:09 +05:30
Kovid Goyal
1022d8a0ee diff kitten: Show the number of matches in the status bar when doing a search 2018-06-15 14:57:03 +05:30
Kovid Goyal
cd6a46730a ... 2018-06-15 14:48:59 +05:30
Kovid Goyal
59743fd7a1 diff kitten: Allow theming the selection background/foreground as well 2018-06-15 14:43:39 +05:30
Kovid Goyal
07bd371e21 Re-org diff kitten docs
Move the auto-generated bits to the end
2018-06-15 14:31:13 +05:30
Kovid Goyal
fe3b10a8fb diff kitten: Implement searching for text in the diff
Fixes #574
2018-06-15 14:28:42 +05:30
Kovid Goyal
c994bc1d89 Add more clarification about kitty's performance goals 2018-06-15 13:51:49 +05:30
Kovid Goyal
22cffef7c8 diff kitten: Use a single-width replacement for control chars
Preserves text width better, and conveys the essential message, namely
non-printable characters. Most people wont know what <hex code> is in
any case.
2018-06-15 08:08:21 +05:30
Kovid Goyal
5dfb8b1d80 diff kitten: Display CRLF line endings using the unicode return symbol instead of <d> as it is less intrusive.
Fixes #638
2018-06-15 07:08:57 +05:30
Kovid Goyal
82f9f002bd diff kitten: Fix default foreground/background colors not being restored when kitten quits. Fixes #637 2018-06-14 23:25:13 +05:30
Kovid Goyal
9f51506422 ... 2018-06-14 22:07:34 +05:30
Kovid Goyal
2a8295d71c diff kitten: Better error message when one of the arguments does not exist 2018-06-14 21:53:13 +05:30
Kovid Goyal
7214b66aa5 A new tui API for simple line editing
Handles basic line-editing with the extended keyboard protocol and
support for wide chars.
Currently used by the unicode input kitten.
2018-06-14 13:11:48 +05:30
Kovid Goyal
a7d9c63a4e wcwidth() for not assigned code points should be -4
That way, wcswidth() computes their width as 1, which matches how they
are rendered
2018-06-14 10:27:42 +05:30
Kovid Goyal
61dd52b50f Ignore the non-characters from the unicode standard in addition to ignoring the control characters 2018-06-14 10:20:13 +05:30
Kovid Goyal
a157f3c5e3 Fix kitty @ set-colors --all not working when more than one window present
Fixes #632
2018-06-13 14:21:55 +05:30
Kovid Goyal
fc02246373 ... 2018-06-13 07:46:11 +05:30
Kovid Goyal
adb8a4676c Fix a regression that broke the legacy increase/decrease_font_size actions 2018-06-13 07:42:12 +05:30
Kovid Goyal
f78d05a57f Clear scrollback on reset
Fixes #631
2018-06-13 07:20:19 +05:30
Kovid Goyal
301d34fecf Ignore errors in talk socket shutdown
macOS apparently shuts down the socket before shutdown can be called.
2018-06-12 21:15:43 +05:30
Kovid Goyal
0848816644 ... 2018-06-12 21:03:02 +05:30
Kovid Goyal
ba7f0132f2 Add a command line option to make --single-instance wait for the closing of the newly opened window before quitting. Fixes #630 2018-06-12 20:53:25 +05:30
Kovid Goyal
561fe81d00 Use base instead of cwd when checking for docs 2018-06-12 09:24:36 +05:30
Kovid Goyal
2db83ecf1e Run make docs for linux-package only if docs are not found 2018-06-12 09:20:18 +05:30
Kovid Goyal
1b8c6b23e1 Dont run make docs to build linux-package
The pre-built docs are now included in the official kitty source code
release. This means that building linux-package does not require
sphinx anymore.
2018-06-12 09:15:43 +05:30
Kovid Goyal
b6622b9458 Fix paths to built bundles 2018-06-12 08:53:22 +05:30
90 changed files with 33498 additions and 32212 deletions

1
.gitignore vendored
View File

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

View File

@@ -103,7 +103,7 @@ install: |
if [[ "$USE_BREW" == "1" ]]; then
brew update;
brew upgrade python;
brew install harfbuzz --without-glib --without-gobject-introspection --without-graphite2 --without-icu4c --without-freetype;
brew install harfbuzz --without-graphite2 --without-icu4c --without-freetype;
brew install imagemagick
brew install optipng
brew install librsvg
@@ -150,9 +150,17 @@ before_script:
- echo LD_LIBRARY_PATH=$LD_LIBRARY_PATH PKG_CONFIG_PATH=$PKG_CONFIG_PATH
- $PYTHON setup.py build --debug --verbose $SANITIZE_ARG;
script:
- if grep -Inr '\s$' kitty kitty_tests kittens docs *.py *.asciidoc *.rst .gitattributes .gitignore; then echo Trailing whitespace found, aborting.; exit 1; fi
- if [[ -z $SANITIZE_ARG ]]; then $PYTHON test.py; else ./asan-launcher test.py; fi
- if [[ "$RUN_FLAKE" == "1" ]]; then flake8 --count .; fi
- if [[ "$BUILD_PKG" == "linux-package" ]]; then make FAIL_WARN=-W man && make FAIL_WARN=-W html; fi
- if [[ ! -z "$BUILD_PKG" ]]; then $PYTHON setup.py $BUILD_PKG; fi
script: |
set -e
if grep -Inr '\s$' kitty kitty_tests kittens docs *.py *.asciidoc *.rst .gitattributes .gitignore; then echo Trailing whitespace found, aborting.; exit 1; fi
if [[ -z "$SANITIZE_ARG" ]]; then $PYTHON test.py; else ./asan-launcher test.py; fi
if [[ "$RUN_FLAKE" == "1" ]]; then flake8 --count .; fi
if [[ "$BUILD_PKG" == "linux-package" ]]; then make FAIL_WARN=-W man && make FAIL_WARN=-W html; fi
if [[ ! -z "$BUILD_PKG" ]]; then $PYTHON setup.py $BUILD_PKG; fi
set +e

View File

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

View File

@@ -39,4 +39,7 @@ man:
html:
$(MAKE) FAIL_WARN=$(FAIL_WARN) -C docs html
linkcheck:
$(MAKE) FAIL_WARN=$(FAIL_WARN) -C docs linkcheck
docs: man html

View File

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

View File

@@ -60,31 +60,35 @@ 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.
:file:`tic` to compile terminfo files, usually found in the
development package of :file:`ncurses`. Also, :file:`sphinx-build`
from the `Sphinx documentation generator
development package of :file:`ncurses`. Also, if you are building form
a git checkout instead of the released source code tarball, you will
need :file:`sphinx-build` from the `Sphinx documentation generator
<http://www.sphinx-doc.org/>`_.
This applies to creating packages for kitty for macOS package managers such as
This applies to creating packages for |kitty| for macOS package managers such as
brew or MacPorts as well.

View File

@@ -3,6 +3,71 @@ 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]
------------------------------
- diff kitten: Implement searching for text in the diff (:iss:`574`)
- Add an option :opt:`startup_session` to :file:`kitty.conf` to specify a
default startup session (:iss:`641`)
- Add a command line option :option:`kitty --wait-for-single-instance-window-close`
to make :option:`kitty --single-instance` wait for the closing of the newly opened
window before quitting (:iss:`630`)
- diff kitten: Allow theming the selection background/foreground as well
- diff kitten: Display CRLF line endings using the unicode return symbol
instead of <d> as it is less intrusive (:iss:`638`)
- diff kitten: Fix default foreground/background colors not being restored when
kitten quits (:iss:`637`)
- Fix :option:`kitty @ set-colors --all` not working when more than one window
present (:iss:`632`)
- Fix a regression that broke the legacy increase/decrease_font_size actions
- Clear scrollback on reset (:iss:`631`)
0.11.0 [2018-06-12]
------------------------------

View File

@@ -85,6 +85,8 @@ pygments_style = 'sphinx'
rst_prolog = '''
.. |kitty| replace:: *kitty*
.. |version| replace:: VERSION
.. _tarball: https://github.com/kovidgoyal/kitty/releases/download/vVERSION/kitty-VERSION.tar.xz
.. role:: green
.. role:: italic
.. role:: bold
@@ -92,7 +94,7 @@ rst_prolog = '''
.. role:: title
.. role:: env
'''
'''.replace('VERSION', str_version)
# -- Options for HTML output -------------------------------------------------
@@ -115,6 +117,9 @@ html_theme_options = {
'github_banner': True,
'github_user': 'kovidgoyal',
'github_repo': 'kitty',
# increase contrast of link color with text color
'link': '#00587d',
'link_hover': 'green',
}
@@ -245,6 +250,7 @@ def write_cli_docs(all_kitten_names):
from kitty.cmds import cli_params_for
for cmd_name in all_commands:
func = cmap[cmd_name]
p(f'.. _at_{func.name}:\n')
p('kitty @', func.name + '\n' + '-' * 120)
p('.. program::', 'kitty @', func.name)
p('\n\n' + as_rst(*cli_params_for(func)))
@@ -496,35 +502,30 @@ def write_conf_docs(app, all_kitten_names):
sc_role.warn_dangling = True
sc_role.process_link = process_shortcut_link
def generate(all_options, name='kitty'):
def generate_default_config(all_options, name):
from kitty.conf.definition import as_conf_file
from textwrap import indent
with open(f'generated/conf-{name}.rst', 'w', encoding='utf-8') as f:
print('.. highlight:: conf\n', file=f)
f.write(render_conf(name, all_options.values()))
with open(f'generated/conf-{name}-literal.rst', 'w', encoding='utf-8') as f:
print('.. code-block:: conf\n', file=f)
conf_name = re.sub(r'^kitten-', '', name) + '.conf'
with open(f'generated/conf/{conf_name}', 'w', encoding='utf-8') as f:
text = '\n'.join(as_conf_file(all_options.values()))
text = indent(text, ' ', lambda l: True)
print(text, file=f)
from kitty.config_data import all_options
generate(all_options)
generate_default_config(all_options, 'kitty')
from kittens.runner import get_kitten_conf_docs
for kitten in all_kitten_names:
all_options = get_kitten_conf_docs(kitten)
if all_options:
generate(all_options, f'kitten-{kitten}')
generate_default_config(all_options, f'kitten-{kitten}')
# }}}
def setup(app):
try:
os.mkdir('generated')
except FileExistsError:
pass
os.makedirs('generated/conf', exist_ok=True)
from kittens.runner import all_kitten_names
all_kitten_names = all_kitten_names()
write_cli_docs(all_kitten_names)

View File

@@ -35,6 +35,6 @@ expanded, so :code:`${USER}.conf` becomes :file:`name.conf` if
Sample kitty.conf
^^^^^^^^^^^^^^^^^^^^^
Below is a sample :file:`kitty.conf` with all default settings.
.. include:: /generated/conf-kitty-literal.rst
You can download a sample :file:`kitty.conf` file with all default settings and
comments describing each setting by clicking: :download:`sample kitty.conf
</generated/conf/kitty.conf>`.

View File

@@ -83,7 +83,7 @@ How do I change the colors in a running kitty instance?
You can either use the
`OSC terminal escape codes <http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Operating-System-Commands>`_
to set colors or you can enable :doc:`remote control <remote-control>`
for |kitty| and use ``kitty @ set-colors --help``.
for |kitty| and use :ref:`at_set-colors`.
How do I specify command line options for kitty on macOS?

View File

@@ -1,7 +1,7 @@
:tocdepth: 2
==========================================================
kitty - the fast, featureful, GPU based, terminal emulator
kitty - the fast, featureful, GPU based terminal emulator
==========================================================
.. container:: major-features
@@ -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
@@ -250,14 +250,22 @@ Some prominent kittens:
Copy/paste to the clipboard from shell scripts, even over SSH.
Configuring kitty
-------------------
|kitty| is highly configurable, everything from keyboard shortcuts to
painting frames-per-second. For details and a sample :file:`kitty.conf`,
see the :doc:`configuration docs <conf>`.
.. _sessions:
Startup Sessions
------------------
You can control the tabs, window layout, working directory, startup
programs, etc. by creating a "session" file and using the :option:`kitty --session`
command line flag. For example:
You can control the tabs, window layout, working directory, startup programs,
etc. by creating a "session" file and using the :option:`kitty --session`
command line flag or the :opt:`startup_session` option in :file:`kitty.conf`.
For example:
.. code-block:: session

View File

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

View File

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

View File

@@ -31,7 +31,7 @@ Major Features
Installation
---------------
Simply install :ref:`kitty <quickstart>`. You also need
Simply :ref:`install kitty <quickstart>`. You also need
to have either the `git <https://git-scm.com/>`_ program or the ``diff`` program
installed. Additionally, for syntax highlighting to work,
`pygments <http://pygments.org/>`_ must be installed (note that pygments is
@@ -67,37 +67,29 @@ Keyboard controls
========================= ===========================
Action Shortcut
========================= ===========================
Quit ``q, Ctrl+c``
Scroll line up ``k, up``
Scroll line down ``j, down``
Scroll page up ``PgUp``
Scroll page down ``PgDn``
Scroll to top ``Home``
Scroll to bottom ``End``
Scroll to next page ``Space, PgDn``
Scroll to previous page ``PgUp``
Scroll to next change ``n``
Scroll to previous change ``p``
Increase lines of context ``+``
Decrease lines of context ``-``
All lines of context ``a``
Restore default context ``=``
Quit :kbd:`q, Ctrl+c, Esc`
Scroll line up :kbd:`k, up`
Scroll line down :kbd:`j, down`
Scroll page up :kbd:`PgUp`
Scroll page down :kbd:`PgDn`
Scroll to top :kbd:`Home`
Scroll to bottom :kbd:`End`
Scroll to next page :kbd:`Space, PgDn`
Scroll to previous page :kbd:`PgUp`
Scroll to next change :kbd:`n`
Scroll to previous change :kbd:`p`
Increase lines of context :kbd:`+`
Decrease lines of context :kbd:`-`
All lines of context :kbd:`a`
Restore default context :kbd:`=`
Search forwards :kbd:`/`
Search backwards :kbd:`?`
Clear search :kbd:`Esc`
Scroll to next match :kbd:`>, .`
Scroll to previous match :kbd:`<, ,`
========================= ===========================
Configuration
------------------------
You can configure the colors used, keyboard shortcuts, the diff implementation,
the default lines of context, etc. by creating a :file:`diff.conf` file in
your :ref:`kitty config folder <confloc>`. See below for the supported
configuration directives.
.. include:: /generated/conf-kitten-diff.rst
Integrating with git
-----------------------
@@ -122,11 +114,6 @@ Now to use kitty-diff to view git diffs, you can simply do::
Once again, creating an alias for this command is useful.
Command Line Interface
-------------------------
.. include:: /generated/cli-kitten-diff.rst
Why does this work only in kitty?
----------------------------------------
@@ -142,9 +129,29 @@ And fundamentally, it's kitty only because I wrote it for myself, and I am
highly unlikely to use any other terminals :)
Configuration
------------------------
You can configure the colors used, keyboard shortcuts, the diff implementation,
the default lines of context, etc. by creating a :file:`diff.conf` file in
your :ref:`kitty config folder <confloc>`. See below for the supported
configuration directives.
.. include:: /generated/conf-kitten-diff.rst
Command Line Interface
-------------------------
.. include:: /generated/cli-kitten-diff.rst
Sample diff.conf
-----------------
Below is a sample :file:`diff.conf` with all default settings.
.. include:: /generated/conf-kitten-diff-literal.rst
You can download a sample :file:`diff.conf` file with all default settings and
comments describing each setting by clicking: :download:`sample diff.conf
</generated/conf/diff.conf>`.

View File

@@ -43,3 +43,17 @@ konsole 29 - 31%
As you can see, |kitty| uses much less CPU than all terminals, except xterm, but
its scrolling "smoothness" is much better than that of xterm (at least to my,
admittedly biased, eyes).
.. _perf-cat:
.. note::
Some people have asked why kitty does not perform better than terminal XXX in
the test of sinking large amounts of data, such as catting a large text
file. The answer is because this is not a goal for kitty. kitty
deliberately throttles input parsing and output rendering to minimize
resource usage while still being able to sink output faster than any real
world program can produce it. Reducing CPU usage, and hence battery drain
while achieving instant response times and smooth scrolling to a human eye
is a far more important goal.

View File

@@ -165,3 +165,22 @@ For example, to set the background color to blue in a
rectangular region of the screen from (3, 4) to (10, 11), you use::
<ESC>[2*x<ESC>[4;3;11;10;44$r<ESC>[*x
Saving and restoring the default foreground/background/selection/cursor colors
---------------------------------------------------------------------------------
It is often useful for a full screen application with its own color themes
to set the default foreground, background, selection and cursor colors. This
allows for various performance optimizations when drawing the screen. The
problem is that if the user previously used the escape codes to change these
colors herself, then running the full screen application will lose her
changes even after it exits. To avoid this, kitty introduces a new pair of
*OSC* escape codes to push and pop the current color values from a stack::
<ESC>]30001<ESC>\ # push onto stack
<ESC>]30101<ESC>\ # pop from stack
These escape codes save/restore the so called *dynamic colors*, default
background, default foreground, selection background, selection foreground and
cursor color.

View File

@@ -15,6 +15,12 @@ from urllib.request import urlopen
os.chdir(os.path.dirname(os.path.abspath(__file__)))
non_characters = frozenset(range(0xfffe, 0x10ffff, 0x10000))
non_characters |= frozenset(range(0xffff, 0x10ffff + 1, 0x10000))
non_characters |= frozenset(range(0xfdd0, 0xfdf0))
if len(non_characters) != 66:
raise SystemExit('non_characters table incorrect')
def get_data(fname, folder='UCD'):
url = f'https://www.unicode.org/Public/{folder}/latest/{fname}'
@@ -189,11 +195,12 @@ def gen_emoji():
p('\treturn false;\n}')
def category_test(name, p, classes, comment, static=False):
def category_test(name, p, classes, comment, static=False, extra_chars=frozenset()):
static = 'static inline ' if static else ''
chars = set()
for c in classes:
chars |= class_maps[c]
chars |= extra_chars
p(f'{static}bool\n{name}(char_type code) {{')
p(f'\t// {comment} ({len(chars)} codepoints)' + ' {{' '{')
p('\tswitch(code) {')
@@ -245,7 +252,7 @@ def gen_ucd():
with create_header('kitty/unicode-data.c') as p:
p('#include "unicode-data.h"')
category_test('is_combining_char', p, {c for c in class_maps if c.startswith('M')}, 'M category (marks)')
category_test('is_ignored_char', p, 'Cc Cf Cs'.split(), 'Control characters (Cc Cf Cs)')
category_test('is_ignored_char', p, 'Cc Cf Cs'.split(), 'Control characters and non-characters', extra_chars=non_characters)
category_test('is_word_char', p, {c for c in class_maps if c[0] in 'LN'}, 'L and N categories')
category_test('is_CZ_category', p, cz, 'C and Z categories')
category_test('is_P_category', p, {c for c in class_maps if c[0] == 'P'}, 'P category (punctuation)')
@@ -258,7 +265,7 @@ def gen_ucd():
p('combining_type mark_for_codepoint(char_type c) {')
rmap = codepoint_to_mark_map(p, mark_map)
p('}\n')
if rmap[0xfe0e] != 1275:
if rmap[0xfe0e] != 1280:
raise ValueError('The mark for 0xfe0e has changed, you have to update VS15 to {} and VS16 to {} in unicode-data.h'.format(
rmap[0xfe0e], rmap[0xfe0f]
))
@@ -393,7 +400,7 @@ def gen_wcwidth():
add(p, 'East Asian double width', doublewidth, 2)
add(p, 'Emoji Presentation', emoji_categories['Emoji_Presentation'], 2)
add(p, 'Not assigned in the unicode character database', not_assigned, -1)
add(p, 'Not assigned in the unicode character database', not_assigned, -4)
p('\t\tdefault: return 1;')
p('\t}')

View File

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

View File

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

View File

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

View File

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

View File

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

1
glfw/glfw3.h vendored
View File

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

5
glfw/init.c vendored
View File

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

1
glfw/internal.h vendored
View File

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

2
glfw/wl_init.c vendored
View File

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

4
glfw/wl_window.c vendored
View File

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

4
glfw/x11_window.c vendored
View File

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

19
glfw/xkb_glfw.c vendored
View File

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

1
glfw/xkb_glfw.h vendored
View File

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

View File

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

View File

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

View File

@@ -116,12 +116,9 @@ def collect_files(collection, left, right):
sanitize_pat = re.compile('[\x00-\x09\x0b-\x1f\x7f\x80-\x9f]')
def sanitize_sub(m):
return '<{:x}>'.format(ord(m.group()[0]))
def sanitize(text):
return sanitize_pat.sub(sanitize_sub, text)
ntext = text.replace('\r\n', '\n')
return sanitize_pat.sub('', ntext)
@lru_cache(maxsize=1024)

View File

@@ -52,7 +52,7 @@ def parse_scroll_by(func, rest):
@func_with_args('scroll_to')
def parse_scroll_to(func, rest):
rest = rest.lower()
if rest not in {'start', 'end', 'next-change', 'prev-change', 'next-page', 'prev-page'}:
if rest not in {'start', 'end', 'next-change', 'prev-change', 'next-page', 'prev-page', 'next-match', 'prev-match'}:
rest = 'start'
return func, rest
@@ -69,6 +69,14 @@ def parse_change_context(func, rest):
return func, amount
@func_with_args('start_search')
def parse_start_search(func, rest):
rest = rest.lower().split()
is_regex = rest and rest[0] == 'regex'
is_backward = len(rest) > 1 and rest[1] == 'backward'
return func, (is_regex, is_backward)
def special_handling(key, val, ans):
if key == 'map':
action, *key_def = parse_kittens_key(val, args_funcs)

View File

@@ -83,6 +83,10 @@ c('filler_bg', '#fafbfc', long_text=_('Filler (empty) line background'))
c('hunk_margin_bg', '#dbedff', long_text=_('Hunk header colors'))
c('hunk_bg', '#f1f8ff')
c('search_bg', '#444', long_text=_('Highlighting'))
c('search_fg', 'white')
c('select_bg', '#B4D5FE')
c('select_fg', 'black')
g('shortcuts')
k('quit', 'q', 'quit', _('Quit'))
@@ -108,4 +112,13 @@ k('default_context', '=', 'change_context default', _('Show default context'))
k('increase_context', '+', 'change_context 5', _('Increase context'))
k('decrease_context', '-', 'change_context -5', _('Decrease context'))
k('search_forward', '/', 'start_search regex forward', _('Search forward'))
k('search_backward', '?', 'start_search regex backward', _('Search backward'))
k('next_match', '.', 'scroll_to next-match', _('Scroll to next search match'))
k('prev_match', ',', 'scroll_to prev-match', _('Scroll to previous search match'))
k('next_match', '>', 'scroll_to next-match', _('Scroll to next search match'))
k('prev_match', '<', 'scroll_to prev-match', _('Scroll to previous search match'))
k('search_forward_simple', 'f', 'start_search substring forward', _('Search forward (no regex)'))
k('search_backward_simple', 'b', 'start_search substring backward', _('Search backward (no regex)'))
type_map = {o.name: o.option_type for o in all_options.values() if hasattr(o, 'option_type')}

View File

@@ -9,20 +9,24 @@ from collections import defaultdict
from functools import partial
from gettext import gettext as _
from kitty.cli import CONFIG_HELP, appname, parse_args
from kitty.cli import CONFIG_HELP, parse_args
from kitty.constants import appname
from kitty.fast_data_types import wcswidth
from kitty.key_encoding import RELEASE
from kitty.key_encoding import ESCAPE, RELEASE, enter_key
from ..tui.handler import Handler
from ..tui.images import ImageManager
from ..tui.line_edit import LineEdit
from ..tui.loop import Loop
from ..tui.operations import styled
from .collect import (
create_collection, data_for_path, lines_for_path, set_highlight_data
create_collection, data_for_path, lines_for_path, sanitize,
set_highlight_data
)
from .config import init_config
from .patch import Differ, set_diff_command
from .render import ImageSupportWarning, LineRef, render_diff
from .search import BadRegex, Search
try:
from .highlight import initialize_highlighter, highlight_collection
@@ -30,7 +34,7 @@ except ImportError:
initialize_highlighter = None
INITIALIZING, COLLECTED, DIFFED = range(3)
INITIALIZING, COLLECTED, DIFFED, COMMAND, MESSAGE = range(5)
def generate_diff(collection, context):
@@ -51,6 +55,10 @@ class DiffHandler(Handler):
def __init__(self, args, opts, left, right):
self.state = INITIALIZING
self.message = ''
self.current_search_is_regex = True
self.current_search = None
self.line_edit = LineEdit()
self.opts = opts
self.left, self.right = left, right
self.report_traceback_on_exit = None
@@ -76,6 +84,8 @@ class DiffHandler(Handler):
where = args[0]
if 'change' in where:
return self.scroll_to_next_change(backwards='prev' in where)
if 'match' in where:
return self.scroll_to_next_match(backwards='prev' in where)
if 'page' in where:
amt = self.num_lines * (1 if 'next' in where else -1)
else:
@@ -91,6 +101,9 @@ class DiffHandler(Handler):
else:
new_ctx += to
return self.change_context_count(new_ctx)
if func == 'start_search':
self.start_search(*args)
return
def create_collection(self):
self.start_job('collect', create_collection, self.left, self.right)
@@ -107,10 +120,13 @@ class DiffHandler(Handler):
def render_diff(self):
self.diff_lines = tuple(render_diff(self.collection, self.diff_map, self.args, self.screen_size.cols, self.image_manager))
self.margin_size = render_diff.margin_size
self.ref_path_map = defaultdict(list)
for i, l in enumerate(self.diff_lines):
self.ref_path_map[l.ref.path].append((i, l.ref))
self.max_scroll_pos = len(self.diff_lines) - self.num_lines
if self.current_search is not None:
self.current_search(self.diff_lines, self.margin_size, self.screen_size.cols)
@property
def current_position(self):
@@ -152,6 +168,19 @@ class DiffHandler(Handler):
return
self.cmd.bell()
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 - offset, -1, -1)
else:
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)
return
self.cmd.bell()
def set_scrolling_region(self):
self.cmd.set_scrolling_region(self.screen_size, 0, self.num_lines - 2)
@@ -178,7 +207,16 @@ class DiffHandler(Handler):
def init_terminal_state(self):
self.cmd.set_line_wrapping(False)
self.cmd.set_window_title(main.title)
self.cmd.set_default_colors(self.opts.foreground, self.opts.background)
self.cmd.set_default_colors(
fg=self.opts.foreground, bg=self.opts.background,
cursor=self.opts.foreground, select_fg=self.opts.select_fg,
select_bg=self.opts.select_bg)
self.cmd.set_cursor_shape('bar')
def finalize(self):
self.cmd.set_default_colors()
self.cmd.set_cursor_visible(True)
self.cmd.set_scrolling_region()
def initialize(self):
self.init_terminal_state()
@@ -187,12 +225,7 @@ class DiffHandler(Handler):
self.create_collection()
def enforce_cursor_state(self):
self.cmd.set_cursor_visible(self.state > DIFFED)
def finalize(self):
self.cmd.set_cursor_visible(True)
self.cmd.set_default_colors()
self.cmd.set_scrolling_region()
self.cmd.set_cursor_visible(self.state == COMMAND)
def draw_lines(self, num, offset=0):
offset += self.scroll_pos
@@ -208,6 +241,8 @@ class DiffHandler(Handler):
if line.image_data is not None:
image_involved = True
self.write('\r\x1b[K' + text + '\x1b[0m')
if self.current_search is not None:
self.current_search.highlight_line(self.write, lpos)
if i < num - 1:
self.write('\n')
if image_involved:
@@ -262,19 +297,29 @@ class DiffHandler(Handler):
def draw_status_line(self):
if self.state < DIFFED:
return
self.enforce_cursor_state()
self.cmd.set_cursor_position(0, self.num_lines)
self.cmd.clear_to_eol()
scroll_frac = styled('{:.0%}'.format(self.scroll_pos / (self.max_scroll_pos or 1)), fg=self.opts.margin_fg)
counts = '{}{}{}'.format(
styled(str(self.added_count), fg=self.opts.highlight_added_bg),
styled(',', fg=self.opts.margin_fg),
styled(str(self.removed_count), fg=self.opts.highlight_removed_bg)
)
suffix = counts + ' ' + scroll_frac
prefix = styled(':', fg=self.opts.margin_fg)
filler = self.screen_size.cols - wcswidth(prefix) - wcswidth(suffix)
text = '{}{}{}'.format(prefix, ' ' * filler, suffix)
self.write(text)
if self.state is COMMAND:
self.line_edit.write(self.write)
elif self.state is MESSAGE:
self.cmd.styled(self.message, reverse=True)
else:
sp = '{:.0%}'.format(self.scroll_pos/self.max_scroll_pos) if self.scroll_pos and self.max_scroll_pos else '0%'
scroll_frac = styled(sp, fg=self.opts.margin_fg)
if self.current_search is None:
counts = '{}{}{}'.format(
styled(str(self.added_count), fg=self.opts.highlight_added_bg),
styled(',', fg=self.opts.margin_fg),
styled(str(self.removed_count), fg=self.opts.highlight_removed_bg)
)
else:
counts = styled('{} matches'.format(len(self.current_search)), fg=self.opts.margin_fg)
suffix = counts + ' ' + scroll_frac
prefix = styled(':', fg=self.opts.margin_fg)
filler = self.screen_size.cols - wcswidth(prefix) - wcswidth(suffix)
text = '{}{}{}'.format(prefix, ' ' * filler, suffix)
self.write(text)
def change_context_count(self, new_ctx):
new_ctx = max(0, new_ctx)
@@ -285,14 +330,77 @@ class DiffHandler(Handler):
self.restore_position = self.current_position
self.draw_screen()
def start_search(self, is_regex, is_backward):
if self.state != DIFFED:
self.cmd.bell()
return
self.state = COMMAND
self.line_edit.clear()
self.line_edit.add_text('?' if is_backward else '/')
self.current_search_is_regex = is_regex
self.draw_status_line()
def do_search(self):
self.current_search = None
query = self.line_edit.current_input
if len(query) < 2:
return
try:
self.current_search = Search(self.opts, query[1:], self.current_search_is_regex, query[0] == '?')
except BadRegex:
self.state = MESSAGE
self.message = sanitize(_('Bad regex: {}').format(query[1:]))
self.cmd.bell()
else:
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()
def on_text(self, text, in_bracketed_paste=False):
if self.state is COMMAND:
self.line_edit.on_text(text, in_bracketed_paste)
self.draw_status_line()
return
if self.state is MESSAGE:
self.state = DIFFED
self.draw_status_line()
return
action = self.shortcut_action(text)
if action is not None:
return self.perform_action(action)
def on_key(self, key_event):
if self.state is MESSAGE:
if key_event.type is not RELEASE:
self.state = DIFFED
self.draw_status_line()
return
if self.state is COMMAND:
if self.line_edit.on_key(key_event):
if not self.line_edit.current_input:
self.state = DIFFED
self.draw_status_line()
return
if key_event.type is RELEASE:
return
if self.state is COMMAND:
if key_event.key is ESCAPE:
self.state = DIFFED
self.draw_status_line()
return
if key_event is enter_key:
self.state = DIFFED
self.do_search()
self.line_edit.clear()
self.draw_screen()
return
if self.state >= DIFFED and self.current_search is not None and key_event.key is ESCAPE:
self.current_search = None
self.draw_screen()
return
action = self.shortcut_action(key_event)
if action is not None:
return self.perform_action(action)
@@ -399,11 +507,9 @@ def main(args):
opts = init_config(args)
set_diff_command(opts.diff_cmd)
lines_for_path.replace_tab_by = opts.replace_tab_by
# Ensure the kitten works even when stdin/stdout are redirected
if not sys.stdin.isatty():
sys.stdin = open('/dev/tty')
if not sys.stdout.isatty():
sys.stdout = open('/dev/tty', 'w')
for f in left, right:
if not os.path.exists(f):
raise SystemExit('{} does not exist'.format(f))
loop = Loop()
handler = DiffHandler(args, opts, left, right)

View File

@@ -447,7 +447,7 @@ def render_diff(collection, diff_map, args, columns, image_manager):
if patch is not None:
largest_line_number = max(largest_line_number, patch.largest_line_number)
margin_size = max(3, len(str(largest_line_number)) + 1)
margin_size = render_diff.margin_size = max(3, len(str(largest_line_number)) + 1)
last_item_num = len(collection) - 1
for i, (path, item_type, other_path) in enumerate(collection):

67
kittens/diff/search.py Normal file
View File

@@ -0,0 +1,67 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import re
from kitty.fast_data_types import wcswidth
from ..tui.operations import styled
class BadRegex(ValueError):
pass
class Search:
def __init__(self, opts, query, is_regex, is_backward):
self.matches = {}
self.count = 0
self.style = styled('|', fg=opts.search_fg, bg=opts.search_bg).split('|', 1)[0]
if not is_regex:
query = re.escape(query)
try:
self.pat = re.compile(query, flags=re.UNICODE | re.IGNORECASE)
except Exception:
raise BadRegex('Not a valid regex: {}'.format(query))
def __call__(self, diff_lines, margin_size, cols):
self.matches = {}
self.count = 0
half_width = cols // 2
strip_pat = re.compile('\033[[].*?m')
right_offset = half_width + 1 + margin_size
find = self.pat.finditer
for i, line in enumerate(diff_lines):
text = strip_pat.sub('', line.text)
left, right = text[margin_size:half_width + 1], text[right_offset:]
matches = []
def add(which, offset):
for m in find(which):
before = which[:m.start()]
matches.append((wcswidth(before) + offset, m.group()))
self.count += 1
add(left, margin_size)
add(right, right_offset)
if matches:
self.matches[i] = matches
return bool(self.matches)
def __contains__(self, i):
return i in self.matches
def __len__(self):
return self.count
def highlight_line(self, write, line_num):
highlights = self.matches.get(line_num)
if not highlights:
return False
write(self.style)
for start, text in highlights:
write('\r\x1b[{}C{}'.format(start, text))
write('\x1b[m')
return True

View File

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

View File

@@ -1 +1 @@
url_delimiters = '\x00-\x09\x0b-\x20\x7f-\xa0\xad\u0600-\u0605\u061c\u06dd\u070f\u08e2\u1680\u180e\u2000-\u200f\u2028-\u202f\u205f-\u2064\u2066-\u206f\u3000\ud800-\uf8ff\ufeff\ufff9-\ufffb\U000110bd\U0001bca0-\U0001bca3\U0001d173-\U0001d17a\U000e0001\U000e0020-\U000e007f\U000f0000-\U000ffffd\U00100000-\U0010fffd' # noqa
url_delimiters = '\x00-\x09\x0b-\x20\x7f-\xa0\xad\u0600-\u0605\u061c\u06dd\u070f\u08e2\u1680\u180e\u2000-\u200f\u2028-\u202f\u205f-\u2064\u2066-\u206f\u3000\ud800-\uf8ff\ufeff\ufff9-\ufffb\U000110bd\U000110cd\U0001bca0-\U0001bca3\U0001d173-\U0001d17a\U000e0001\U000e0020-\U000e007f\U000f0000-\U000ffffd\U00100000-\U0010fffd' # noqa

View File

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

128
kittens/tui/line_edit.py Normal file
View File

@@ -0,0 +1,128 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
from kitty.fast_data_types import truncate_point_for_length, wcswidth
from kitty.key_encoding import RELEASE, HOME, END, BACKSPACE, DELETE, LEFT, RIGHT
class LineEdit:
def __init__(self):
self.clear()
def clear(self):
self.current_input = ''
self.cursor_pos = 0
self.pending_bell = False
def split_at_cursor(self, delta=0):
pos = max(0, self.cursor_pos + delta)
x = truncate_point_for_length(self.current_input, pos) if pos else 0
before, after = self.current_input[:x], self.current_input[x:]
return before, after
def write(self, write, prompt=''):
if self.pending_bell:
write('\a')
self.pending_bell = False
write(prompt)
write(self.current_input)
write('\r\x1b[{}C'.format(self.cursor_pos + wcswidth(prompt)))
def add_text(self, text):
if self.current_input:
x = truncate_point_for_length(self.current_input, self.cursor_pos) if self.cursor_pos else 0
self.current_input = self.current_input[:x] + text + self.current_input[x:]
else:
self.current_input = text
self.cursor_pos += wcswidth(text)
def on_text(self, text, in_bracketed_paste):
self.add_text(text)
def backspace(self, num=1):
before, after = self.split_at_cursor()
nbefore = before[:-num]
if nbefore != before:
self.current_input = nbefore + after
self.cursor_pos = wcswidth(nbefore)
return True
self.pending_bell = True
return False
def delete(self, num=1):
before, after = self.split_at_cursor()
nafter = after[num:]
if nafter != after:
self.current_input = before + nafter
self.cursor_pos = wcswidth(before)
return True
self.pending_bell = True
return False
def _left(self):
if not self.current_input:
self.cursor_pos = 0
return
if self.cursor_pos:
before, after = self.split_at_cursor(-1)
self.cursor_pos = wcswidth(before)
def _right(self):
if not self.current_input:
self.cursor_pos = 0
return
max_pos = wcswidth(self.current_input)
if self.cursor_pos >= max_pos:
self.cursor_pos = max_pos
return
before, after = self.split_at_cursor(1)
self.cursor_pos += 1 + int(wcswidth(before) == self.cursor_pos)
def _move_loop(self, func, num):
before = self.cursor_pos
while func() and num > 0:
num -= 1
changed = self.cursor_pos != before
if not changed:
self.pending_bell = True
return changed
def left(self, num=1):
return self._move_loop(self._left, num)
def right(self, num=1):
return self._move_loop(self._right, num)
def home(self):
if self.cursor_pos:
self.cursor_pos = 0
return True
return False
def end(self):
orig = self.cursor_pos
self.cursor_pos = wcswidth(self.current_input) + 1
return self.cursor_pos != orig
def on_key(self, key_event):
if key_event.type is RELEASE:
return False
elif key_event.key is HOME:
return self.home()
elif key_event.key is END:
return self.end()
elif key_event.key is BACKSPACE:
self.backspace()
return True
elif key_event.key is DELETE:
self.delete()
return True
elif key_event.key is LEFT:
self.left()
return True
elif key_event.key is RIGHT:
self.right()
return True
return False

View File

@@ -235,7 +235,7 @@ class Loop:
self.handler.on_eot()
else:
self.handler.on_text(chunk, self.in_bracketed_paste)
else:
elif chunk:
self.handler.on_text(chunk, self.in_bracketed_paste)
def _on_dcs(self, dcs):

View File

@@ -78,6 +78,13 @@ def set_cursor_position(x, y) -> str: # (0, 0) is top left
return '\033[{};{}H'.format(y + 1, x + 1)
def set_cursor_shape(shape='block', blink=True) -> str:
val = {'block': 1, 'underline': 3, 'bar': 5}.get(shape, 1)
if not blink:
val += 1
return '\033[{} q'.format(val)
def set_scrolling_region(screen_size=None, top=None, bottom=None) -> str:
if screen_size is None:
return '\033[r'
@@ -182,10 +189,11 @@ def init_state(alternate_screen=True):
reset_mode('IRM') + reset_mode('DECKM') + reset_mode('DECSCNM') +
set_mode('DECARM') + reset_mode('DECOM') + set_mode('DECAWM') +
set_mode('DECTCEM') + reset_mode('MOUSE_BUTTON_TRACKING') +
reset_mode('MOUSE_MOTION_TRACKING') + reset_mode('MOUSE_MOVE_TRACKING')
+ reset_mode('FOCUS_TRACKING') + reset_mode('MOUSE_UTF8_MODE') +
reset_mode('MOUSE_MOTION_TRACKING') + reset_mode('MOUSE_MOVE_TRACKING') +
reset_mode('FOCUS_TRACKING') + reset_mode('MOUSE_UTF8_MODE') +
reset_mode('MOUSE_SGR_MODE') + reset_mode('MOUSE_UTF8_MODE') +
set_mode('BRACKETED_PASTE') + set_mode('EXTENDED_KEYBOARD') +
'\033]30001\033\\' +
'\033[*x' # reset DECSACE to default region select
)
if alternate_screen:
@@ -200,6 +208,7 @@ def reset_state(normal_screen=True):
ans += reset_mode('ALTERNATE_SCREEN')
ans += RESTORE_PRIVATE_MODE_VALUES
ans += RESTORE_CURSOR
ans += '\033]30101\033\\'
return ans
@@ -218,16 +227,21 @@ def alternate_screen(f=None):
print(reset_mode('ALTERNATE_SCREEN'), end='', file=f)
def set_default_colors(fg=None, bg=None) -> str:
def set_default_colors(fg=None, bg=None, cursor=None, select_bg=None, select_fg=None) -> str:
ans = ''
if fg is None:
ans += '\x1b]110\x1b\\'
else:
ans += '\x1b]10;{}\x1b\\'.format(color_as_sharp(fg if isinstance(fg, Color) else to_color(fg)))
if bg is None:
ans += '\x1b]111\x1b\\'
else:
ans += '\x1b]11;{}\x1b\\'.format(color_as_sharp(bg if isinstance(bg, Color) else to_color(bg)))
def item(which, num):
nonlocal ans
if which is None:
ans += '\x1b]1{}\x1b\\'.format(num)
else:
ans += '\x1b]{};{}\x1b\\'.format(num, color_as_sharp(which if isinstance(which, Color) else to_color(which)))
item(fg, 10)
item(bg, 11)
item(cursor, 12)
item(select_bg, 17)
item(select_fg, 19)
return ans

View File

@@ -14,9 +14,10 @@ from kitty.utils import get_editor
from kitty.fast_data_types import wcswidth
from kitty.key_encoding import (
DOWN, ESCAPE, F1, F2, F3, F4, F12, LEFT, RELEASE, RIGHT, SHIFT, TAB, UP,
backspace_key, enter_key
enter_key
)
from ..tui.line_edit import LineEdit
from ..tui.handler import Handler
from ..tui.loop import Loop
from ..tui.operations import (
@@ -247,8 +248,8 @@ class UnicodeInput(Handler):
def __init__(self, cached_values):
self.cached_values = cached_values
self.line_edit = LineEdit()
self.recent = list(self.cached_values.get('recent', DEFAULT_SET))
self.current_input = ''
self.current_char = None
self.prompt_template = '{}> '
self.last_updated_code_point_at = None
@@ -269,9 +270,9 @@ class UnicodeInput(Handler):
codepoints = load_favorites()
q = self.mode, tuple(codepoints)
elif self.mode is NAME:
q = self.mode, self.current_input
q = self.mode, self.line_edit.current_input
if q != self.last_updated_code_point_at:
words = self.current_input.split()
words = self.line_edit.current_input.split()
words = [w for w in words if w != INDEX_CHAR]
index_words = [i for i, w in enumerate(words) if i > 0 and is_index(w)]
if index_words:
@@ -291,10 +292,10 @@ class UnicodeInput(Handler):
self.current_char = None
if self.mode is HEX:
try:
if self.current_input.startswith(INDEX_CHAR) and len(self.current_input) > 1:
self.current_char = chr(self.table.codepoint_at_hint(self.current_input[1:]))
if self.line_edit.current_input.startswith(INDEX_CHAR) and len(self.line_edit.current_input) > 1:
self.current_char = chr(self.table.codepoint_at_hint(self.line_edit.current_input[1:]))
else:
code = int(self.current_input, 16)
code = int(self.line_edit.current_input, 16)
self.current_char = chr(code)
except Exception:
pass
@@ -304,8 +305,8 @@ class UnicodeInput(Handler):
self.current_char = chr(cc)
else:
try:
if self.current_input:
self.current_char = chr(self.table.codepoint_at_hint(self.current_input.lstrip(INDEX_CHAR)))
if self.line_edit.current_input:
self.current_char = chr(self.table.codepoint_at_hint(self.line_edit.current_input.lstrip(INDEX_CHAR)))
except Exception:
pass
if self.current_char is not None:
@@ -366,8 +367,7 @@ class UnicodeInput(Handler):
writeln(_('Enter the hex code for the character'))
else:
writeln(_('Enter the index for the character you want from the list below'))
self.write(self.prompt)
self.write(self.current_input)
self.line_edit.write(self.write, self.prompt)
with cursor(self.write):
writeln()
writeln(self.choice_line)
@@ -385,14 +385,34 @@ class UnicodeInput(Handler):
self.draw_screen()
def on_text(self, text, in_bracketed_paste):
self.current_input += text
self.line_edit.on_text(text, in_bracketed_paste)
self.refresh()
def on_key(self, key_event):
if key_event is backspace_key:
self.current_input = self.current_input[:-1]
if self.mode is NAME and key_event.type is not RELEASE and not key_event.mods:
if key_event.key is TAB:
if key_event.mods == SHIFT:
self.table.move_current(cols=-1), self.refresh()
elif not key_event.mods:
self.table.move_current(cols=1), self.refresh()
return
elif key_event.key is LEFT and not key_event.mods:
self.table.move_current(cols=-1), self.refresh()
return
elif key_event.key is RIGHT and not key_event.mods:
self.table.move_current(cols=1), self.refresh()
return
elif key_event.key is UP and not key_event.mods:
self.table.move_current(rows=-1), self.refresh()
return
elif key_event.key is DOWN and not key_event.mods:
self.table.move_current(rows=1), self.refresh()
return
if self.line_edit.on_key(key_event):
self.refresh()
elif key_event is enter_key:
return
if key_event is enter_key:
self.quit_loop(0)
elif key_event.type is RELEASE and not key_event.mods:
if key_event.key is ESCAPE:
@@ -407,20 +427,6 @@ class UnicodeInput(Handler):
self.switch_mode(FAVORITES)
elif key_event.key is F12 and self.mode is FAVORITES:
self.edit_favorites()
elif self.mode is NAME:
if key_event.key is TAB:
if key_event.mods == SHIFT:
self.table.move_current(cols=-1), self.refresh()
elif not key_event.mods:
self.table.move_current(cols=1), self.refresh()
elif key_event.key is LEFT and not key_event.mods:
self.table.move_current(cols=-1), self.refresh()
elif key_event.key is RIGHT and not key_event.mods:
self.table.move_current(cols=1), self.refresh()
elif key_event.key is UP and not key_event.mods:
self.table.move_current(rows=-1), self.refresh()
elif key_event.key is DOWN and not key_event.mods:
self.table.move_current(rows=1), self.refresh()
def edit_favorites(self):
if not os.path.exists(favorites_path):
@@ -437,7 +443,7 @@ class UnicodeInput(Handler):
if mode is not self.mode:
self.mode = mode
self.cached_values['mode'] = mode
self.current_input = ''
self.line_edit.clear()
self.current_char = None
self.choice_line = ''
self.refresh()

File diff suppressed because one or more lines are too long

View File

@@ -15,7 +15,7 @@ from .conf.utils import to_cmdline
from .config import initial_window_size_func, prepare_config_file_for_editing
from .config_data import MINIMUM_FONT_SIZE
from .constants import (
appname, config_dir, set_boss, supports_primary_selection
appname, config_dir, kitty_exe, set_boss, supports_primary_selection
)
from .fast_data_types import (
ChildMonitor, background_opacity_of, change_background_opacity,
@@ -81,6 +81,7 @@ class Boss:
self.pending_sequences = None
self.cached_values = cached_values
self.os_window_map = {}
self.os_window_death_actions = {}
self.cursor_blinking = True
self.shutting_down = False
talk_fd = getattr(single_instance, 'socket', None)
@@ -95,7 +96,7 @@ class Boss:
)
set_boss(self)
self.opts, self.args = opts, args
startup_session = create_session(opts, args)
startup_session = create_session(opts, args, default_session=opts.startup_session)
self.keymap = self.opts.keymap.copy()
if new_os_window_trigger is not None:
self.keymap.pop(new_os_window_trigger, None)
@@ -239,7 +240,9 @@ class Boss:
if not os.path.isabs(args.directory):
args.directory = os.path.join(msg['cwd'], args.directory)
session = create_session(opts, args, respect_cwd=True)
self.add_os_window(session, wclass=args.cls, wname=args.name, opts_for_size=opts, startup_id=startup_id)
os_window_id = self.add_os_window(session, wclass=args.cls, wname=args.name, opts_for_size=opts, startup_id=startup_id)
if msg.get('notify_on_os_window_death'):
self.os_window_death_actions[os_window_id] = partial(self.notify_on_os_window_death, msg['notify_on_os_window_death'])
else:
log_error('Unknown message received from peer, ignoring')
@@ -311,10 +314,12 @@ class Boss:
tm.resize()
def increase_font_size(self): # legacy
self.set_font_size(min(self.opts.font_size * 5, self.current_font_size + 2.0))
cfs = global_font_size()
self.set_font_size(min(self.opts.font_size * 5, cfs + 2.0))
def decrease_font_size(self): # legacy
self.set_font_size(self.current_font_size - self.opts.font_size_delta)
cfs = global_font_size()
self.set_font_size(max(MINIMUM_FONT_SIZE, cfs - 2.0))
def restore_font_size(self): # legacy
self.set_font_size(self.opts.font_size)
@@ -527,6 +532,23 @@ class Boss:
tm.destroy()
for window_id in tuple(w.id for w in self.window_id_map.values() if getattr(w, 'os_window_id', None) == os_window_id):
self.window_id_map.pop(window_id, None)
action = self.os_window_death_actions.pop(os_window_id, None)
if action is not None:
action()
def notify_on_os_window_death(self, address):
import socket
s = socket.socket(family=socket.AF_UNIX)
try:
s.connect(address)
s.sendall(b'c')
try:
s.shutdown(socket.SHUT_RDWR)
except EnvironmentError:
pass
s.close()
except Exception:
pass
def display_scrollback(self, window, data, cmd):
tab = self.active_tab
@@ -539,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):
@@ -578,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),
@@ -625,7 +647,7 @@ class Boss:
break
def kitty_shell(self, window_type):
cmd = ['kitty', '@']
cmd = [kitty_exe(), '@']
if window_type == 'tab':
window = self._new_tab(cmd).active_window
elif window_type == 'os_window':

View File

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

View File

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

View File

@@ -7,9 +7,10 @@ import re
import sys
from collections import deque
from .config import defaults, load_config
from .conf.utils import resolve_config
from .config import defaults, load_config
from .constants import appname, defconf, is_macos, is_wayland, str_version
from .fast_data_types import GLFW_KEY_UNKNOWN, glfw_get_key_name
CONFIG_HELP = '''\
Specify a path to the configuration file(s) to use. All configuration files are
@@ -44,7 +45,7 @@ condition=not is_macos
Set the name part of the :italic:`WM_CLASS` property (defaults to using the value from :option:`{appname} --class`)
--title
--title -T
Set the window title. This will override any title set by the program running inside kitty. So
only use this if you are running a program that does not set titles.
@@ -76,6 +77,12 @@ Path to a file containing the startup :italic:`session` (tabs, windows, layout,
See the README file for details and an example.
--hold
type=bool-set
Remain open after child process exits. Note that this only affects the first
window. You can quit by either using the close window shortcut or :kbd:`Ctrl+d`.
--single-instance -1
type=bool-set
If specified only a single instance of :italic:`{appname}` will run. New invocations will
@@ -91,6 +98,14 @@ with the same :option:`kitty --instance-group` will result in new windows being
in the first :italic:`{appname}` instance within that group
--wait-for-single-instance-window-close
type=bool-set
Normally, when using :option:`--single-instance`, :italic:`{appname}` will open a new window in an existing
instance and quit immediately. With this option, it will not quit till the newly opened
window is closed. Note that if no previous instance is found, then :italic:`{appname}` will wait anyway,
regardless of this option.
--listen-on
Tell kitty to listen on the specified address for control
messages. For example, :option:`{appname} --listen-on`=unix:/tmp/mykitty or
@@ -205,6 +220,10 @@ def code(x):
return x
def kbd(x):
return x
def env(x):
return italic(x)
@@ -606,12 +625,15 @@ def print_shortcut(key_sequence, action):
keys = []
for key in key_sequence:
names = []
mods, key = key
mods, is_native, key = key
for name, val in mmap.items():
if mods & val:
names.append(name)
if key:
names.append(krmap[key])
if is_native:
names.append(glfw_get_key_name(GLFW_KEY_UNKNOWN, key))
else:
names.append(krmap[key])
keys.append('+'.join(names))
print('\t', ' > '.join(keys), action)

View File

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

View File

@@ -108,8 +108,8 @@ patch_color_profiles(PyObject *module UNUSED, PyObject *args) {
v = PyDict_GetItemString(spec, key);
if (v) {
color_type color = PyLong_AsUnsignedLong(v);
for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(profiles); i++) {
self = (ColorProfile*)PyTuple_GET_ITEM(profiles, i);
for (Py_ssize_t j = 0; j < PyTuple_GET_SIZE(profiles); j++) {
self = (ColorProfile*)PyTuple_GET_ITEM(profiles, j);
self->color_table[i] = color;
if (change_configured) self->orig_color_table[i] = color;
self->dirty = true;
@@ -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"
@@ -220,6 +251,21 @@ copy_color_table_to_buffer(ColorProfile *self, color_type *buf, int offset, size
self->dirty = false;
}
void
colorprofile_push_dynamic_colors(ColorProfile *self) {
if (self->dynamic_color_stack_idx >= arraysz(self->dynamic_color_stack)) {
memmove(self->dynamic_color_stack, self->dynamic_color_stack + 1, sizeof(self->dynamic_color_stack) - sizeof(self->dynamic_color_stack[0]));
self->dynamic_color_stack_idx = arraysz(self->dynamic_color_stack) - 1;
}
self->dynamic_color_stack[self->dynamic_color_stack_idx++] = self->overridden;
}
void
colorprofile_pop_dynamic_colors(ColorProfile *self) {
if (!self->dynamic_color_stack_idx) return;
self->overridden = self->dynamic_color_stack[--(self->dynamic_color_stack_idx)];
}
static PyObject*
color_table_address(ColorProfile *self, PyObject *a UNUSED) {
#define color_table_address_doc "Pointer address to start of color table"
@@ -260,6 +306,7 @@ static PyMemberDef members[] = {
static PyMethodDef methods[] = {
METHOD(update_ansi_color_table, METH_O)
METHOD(reset_color_table, METH_NOARGS)
METHOD(as_dict, METH_NOARGS)
METHOD(color_table_address, METH_NOARGS)
METHOD(as_color, METH_O)
METHOD(reset_color, METH_O)

View File

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

View File

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

View File

@@ -3,6 +3,7 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
# Utils {{{
import os
from gettext import gettext as _
from . import fast_data_types as defines
@@ -10,7 +11,7 @@ from .conf.definition import option_func
from .conf.utils import (
choices, positive_float, positive_int, to_cmdline, to_color, unit_float
)
from .constants import is_macos
from .constants import config_dir, is_macos
from .fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE
from .layout import all_layouts
from .rgb import color_as_int, color_as_sharp, color_from_int
@@ -82,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.
@@ -442,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)'''))
@@ -616,6 +630,25 @@ opening new windows, closing windows, reading the content of windows, etc.
Note that this even works over ssh connections.
'''))
def startup_session(x):
if x.lower() == 'none':
return
x = os.path.expanduser(x)
x = os.path.expandvars(x)
if not os.path.isabs(x):
x = os.path.join(config_dir, x)
return x
o('startup_session', 'none', option_type=startup_session, long_text=_('''
Path to a session file to use for all kitty instances. Can be overridden
by using the :option:`kitty --session` command line option for individual
instances. See :ref:`sessions` in the kitty documentation for details. Note
that relative paths are interpreted with respect to the kitty config directory.
Environment variables in the path are expanded.
'''))
o('clipboard_control', 'write-clipboard write-primary', option_type=lambda x: frozenset(x.lower().split()), long_text=_('''
Allow programs running in kitty to read and write from the clipboard. You can
control exactly which actions are allowed. The set of possible actions is:

View File

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

View File

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

View File

@@ -211,6 +211,8 @@ typedef struct {
bool dirty;
uint32_t color_table[256];
uint32_t orig_color_table[256];
DynamicColor dynamic_color_stack[10];
size_t dynamic_color_stack_idx;
DynamicColor configured, overridden;
} ColorProfile;
@@ -275,6 +277,8 @@ bool set_iutf8(int, bool);
color_type colorprofile_to_color(ColorProfile *self, color_type entry, color_type defval);
void copy_color_table_to_buffer(ColorProfile *self, color_type *address, int offset, size_t stride);
void colorprofile_push_dynamic_colors(ColorProfile*);
void colorprofile_pop_dynamic_colors(ColorProfile*);
void set_mouse_cursor(MouseShape);
void mouse_event(int, int);
@@ -282,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);

2
kitty/emoji.h generated
View File

@@ -1,4 +1,4 @@
// unicode data, built from the unicode standard on: 2018-05-01
// unicode data, built from the unicode standard on: 2018-06-14
// see gen-wcwidth.py
#pragma once
#include "data-types.h"

View File

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

5656
kitty/gl-wrapper.c generated

File diff suppressed because it is too large Load Diff

14
kitty/gl-wrapper.h generated
View File

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

2
kitty/glfw-wrapper.c generated
View File

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

5
kitty/glfw-wrapper.h generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,33 +12,68 @@ 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 (
create_os_window, free_font_data, glfw_init, glfw_terminate,
set_default_window_icon, set_options, GLFW_MOD_SUPER
GLFW_MOD_SUPER, create_os_window, free_font_data, glfw_init,
glfw_terminate, set_default_window_icon, set_options
)
from .fonts.box_drawing import set_scale
from .fonts.render import set_font_family
from .utils import (
detach, log_error, single_instance, startup_notification_handler
detach, log_error, single_instance, startup_notification_handler,
unix_socket_paths
)
from .window import load_shader_programs
def talk_to_instance(args):
import json
import socket
data = {'cmd': 'new_instance', 'args': tuple(sys.argv),
'startup_id': os.environ.get('DESKTOP_STARTUP_ID'),
'cwd': os.getcwd()}
notify_socket = None
if args.wait_for_single_instance_window_close:
address = '\0{}-os-window-close-notify-{}-{}'.format(appname, os.getpid(), os.geteuid())
notify_socket = socket.socket(family=socket.AF_UNIX)
try:
notify_socket.bind(address)
except FileNotFoundError:
for address in unix_socket_paths(address[1:], ext='.sock'):
notify_socket.bind(address)
break
data['notify_on_os_window_death'] = address
notify_socket.listen()
data = json.dumps(data, ensure_ascii=False).encode('utf-8')
single_instance.socket.sendall(data)
try:
single_instance.socket.shutdown(socket.SHUT_RDWR)
except EnvironmentError:
pass
single_instance.socket.close()
if args.wait_for_single_instance_window_close:
conn = notify_socket.accept()[0]
conn.recv(1)
try:
conn.shutdown(socket.SHUT_RDWR)
except EnvironmentError:
pass
conn.close()
def load_all_shaders(semi_transparent=0):
load_shader_programs(semi_transparent)
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
@@ -57,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
@@ -86,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:
@@ -165,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
@@ -189,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):
@@ -200,17 +230,12 @@ def _main():
if args.single_instance:
is_first = single_instance(args.instance_group)
if not is_first:
import json
data = {'cmd': 'new_instance', 'args': tuple(sys.argv),
'startup_id': os.environ.get('DESKTOP_STARTUP_ID'),
'cwd': os.getcwd()}
data = json.dumps(data, ensure_ascii=False).encode('utf-8')
single_instance.socket.sendall(data)
talk_to_instance(args)
return
init_glfw(args.debug_keyboard) # needed for parsing native keysyms
opts = create_opts(args)
if opts.editor != '.':
os.environ['EDITOR'] = opts.editor
init_graphics(args.debug_keyboard)
try:
with setup_profiling(args):
# Avoid needing to launch threads to reap zombies

View File

@@ -344,6 +344,14 @@ dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
case 52:
DISPATCH_OSC(clipboard_control);
break;
case 30001:
REPORT_COMMAND(screen_push_dynamic_colors);
screen_push_dynamic_colors(screen);
break;
case 30101:
REPORT_COMMAND(screen_pop_dynamic_colors);
screen_pop_dynamic_colors(screen);
break;
default:
REPORT_ERROR("Unknown OSC code: %u", code);
break;

View File

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

View File

@@ -112,6 +112,7 @@ void
screen_reset(Screen *self) {
if (self->linebuf == self->alt_linebuf) screen_toggle_screen_buffer(self);
linebuf_clear(self->linebuf, BLANK_CHAR);
historybuf_clear(self->historybuf);
grman_clear(self->grman, false, self->cell_size);
self->modes = empty_modes;
#define R(name) self->color_profile->overridden.name = 0
@@ -1263,6 +1264,16 @@ screen_handle_cmd(Screen *self, PyObject *cmd) {
CALLBACK("handle_remote_cmd", "O", cmd);
}
void
screen_push_dynamic_colors(Screen *self) {
colorprofile_push_dynamic_colors(self->color_profile);
}
void
screen_pop_dynamic_colors(Screen *self) {
colorprofile_pop_dynamic_colors(self->color_profile);
}
void
screen_handle_print(Screen *self, PyObject *msg) {
CALLBACK("handle_remote_print", "O", msg);
@@ -1663,7 +1674,7 @@ static PyObject*
_select_graphic_rendition(Screen *self, PyObject *args) {
unsigned int params[256] = {0};
for (int i = 0; i < PyTuple_GET_SIZE(args); i++) { params[i] = PyLong_AsUnsignedLong(PyTuple_GET_ITEM(args, i)); }
select_graphic_rendition(self, params, PyList_GET_SIZE(args), NULL);
select_graphic_rendition(self, params, PyTuple_GET_SIZE(args), NULL);
Py_RETURN_NONE;
}

View File

@@ -139,6 +139,8 @@ void screen_erase_characters(Screen *self, unsigned int count);
void screen_set_margins(Screen *self, unsigned int top, unsigned int bottom);
void screen_change_charset(Screen *, uint32_t to);
void screen_handle_cmd(Screen *, PyObject *cmd);
void screen_push_dynamic_colors(Screen *);
void screen_pop_dynamic_colors(Screen *);
void screen_handle_print(Screen *, PyObject *cmd);
void screen_designate_charset(Screen *, uint32_t which, uint32_t as);
void screen_use_latin1(Screen *, bool);

View File

@@ -5,8 +5,9 @@
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
class Tab:
@@ -105,16 +106,26 @@ def parse_session(raw, opts):
return ans
def create_session(opts, args=None, special_window=None, cwd_from=None, respect_cwd=False):
def create_session(opts, args=None, special_window=None, cwd_from=None, respect_cwd=False, default_session=None):
if args and args.session:
with open(args.session) as f:
return parse_session(f.read(), opts)
if default_session and default_session != 'none':
try:
with open(default_session) as f:
session_data = f.read()
except EnvironmentError:
log_error('Failed to read from session file, ignoring: {}'.format(default_session))
else:
return parse_session(session_data, opts)
ans = Session()
current_layout = opts.enabled_layouts[0] if opts.enabled_layouts else 'tall'
ans.add_tab(opts)
ans.tabs[-1].layout = current_layout
if special_window is None:
cmd = args.args if args and args.args else resolved_shell(opts)
if args and args.hold:
cmd = [kitty_exe(), '+hold'] + cmd
from kitty.tabs import SpecialWindow
k = {'cwd_from': cwd_from}
if respect_cwd:

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
#pragma once
#include "data-types.h"
#define VS15 1275
#define VS16 1276
#define VS15 1280
#define VS16 1281
bool is_combining_char(char_type ch);
bool is_ignored_char(char_type ch);

View File

@@ -39,7 +39,7 @@ def safe_print(*a, **k):
def log_error(*a, **k):
try:
msg = k.get('sep', ' ').join(map(str, a)) + k.get('end', '\n')
msg = k.get('sep', ' ').join(map(str, a)) + k.get('end', '')
log_error_string(msg.replace('\0', ''))
except Exception:
pass
@@ -266,8 +266,7 @@ def remove_socket_file(s, path=None):
pass
def single_instance_unix(name):
import socket
def unix_socket_paths(name, ext='.lock'):
import tempfile
home = os.path.expanduser('~')
candidates = [tempfile.gettempdir(), home]
@@ -276,34 +275,39 @@ def single_instance_unix(name):
candidates = [user_cache_dir(), '/Library/Caches']
for loc in candidates:
if os.access(loc, os.W_OK | os.R_OK | os.X_OK):
filename = ('.' if loc == home else '') + name + '.lock'
path = os.path.join(loc, filename)
socket_path = path.rpartition('.')[0] + '.sock'
fd = os.open(path, os.O_CREAT | os.O_WRONLY | os.O_TRUNC | os.O_CLOEXEC)
try:
fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except EnvironmentError as err:
if err.errno in (errno.EAGAIN, errno.EACCES):
# Client
s = socket.socket(family=socket.AF_UNIX)
s.connect(socket_path)
single_instance.socket = s
return False
raise
s = socket.socket(family=socket.AF_UNIX)
try:
filename = ('.' if loc == home else '') + name + ext
yield os.path.join(loc, filename)
def single_instance_unix(name):
import socket
for path in unix_socket_paths(name):
socket_path = path.rpartition('.')[0] + '.sock'
fd = os.open(path, os.O_CREAT | os.O_WRONLY | os.O_TRUNC | os.O_CLOEXEC)
try:
fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except EnvironmentError as err:
if err.errno in (errno.EAGAIN, errno.EACCES):
# Client
s = socket.socket(family=socket.AF_UNIX)
s.connect(socket_path)
single_instance.socket = s
return False
raise
s = socket.socket(family=socket.AF_UNIX)
try:
s.bind(socket_path)
except EnvironmentError as err:
if err.errno in (errno.EADDRINUSE, errno.EEXIST):
os.unlink(socket_path)
s.bind(socket_path)
except EnvironmentError as err:
if err.errno in (errno.EADDRINUSE, errno.EEXIST):
os.unlink(socket_path)
s.bind(socket_path)
else:
raise
single_instance.socket = s # prevent garbage collection from closing the socket
atexit.register(remove_socket_file, s, socket_path)
s.listen()
s.set_inheritable(False)
return True
else:
raise
single_instance.socket = s # prevent garbage collection from closing the socket
atexit.register(remove_socket_file, s, socket_path)
s.listen()
s.set_inheritable(False)
return True
def single_instance(group_id=None):
@@ -365,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):
@@ -378,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'))

1464
kitty/wcwidth-std.h generated

File diff suppressed because it is too large Load Diff

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 463 KiB

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

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

View File

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

44
kitty_tests/tui.py Normal file
View File

@@ -0,0 +1,44 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
from . import BaseTest
class TestTUI(BaseTest):
def test_line_edit(self):
from kittens.tui.line_edit import LineEdit
le = LineEdit()
le.on_text('abcd', False)
self.ae(le.cursor_pos, 4)
for i in range(5):
self.assertTrue(le.left()) if i < 4 else self.assertFalse(le.left())
self.ae(le.cursor_pos, max(0, 3 - i))
self.ae(le.pending_bell, True)
le.clear()
le.on_text('abcd', False), le.home()
self.ae(le.cursor_pos, 0)
for i in range(5):
self.assertTrue(le.right()) if i < 4 else self.assertFalse(le.right())
self.ae(le.cursor_pos, min(4, i + 1))
self.ae(le.pending_bell, True)
le.clear()
le.on_text('abcd', False)
self.ae(le.current_input, 'abcd')
self.ae(le.cursor_pos, 4)
self.ae(le.split_at_cursor(), ('abcd', ''))
le.backspace()
self.ae(le.current_input, 'abc')
self.ae(le.cursor_pos, 3)
self.assertFalse(le.pending_bell)
le.backspace(num=2)
self.ae(le.current_input, 'a')
self.ae(le.cursor_pos, 1)
self.assertFalse(le.pending_bell)
le.backspace(num=2)
self.ae(le.current_input, '')
self.ae(le.cursor_pos, 0)
le.backspace()
self.assertTrue(le.pending_bell)

View File

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

View File

@@ -311,9 +311,9 @@ def run_upload(args):
files = {
os.path.join(build_path, 'build', f.format(version)): desc
for f, desc in {
'linux/64/dist/kitty-{}-x86_64.txz': 'Linux amd64 binary bundle',
'linux/32/dist/kitty-{}-i686.txz': 'Linux x86 binary bundle',
'osx/dist/kitty-{}.dmg': 'macOS dmg',
'linux/64/sw/dist/kitty-{}-x86_64.txz': 'Linux amd64 binary bundle',
'linux/32/sw/dist/kitty-{}-i686.txz': 'Linux x86 binary bundle',
}.items()
}
files[f'build/kitty-{version}.tar.xz'] = 'Source code'

View File

@@ -543,10 +543,11 @@ def copy_man_pages(ddir):
shutil.rmtree(os.path.join(mandir, 'man1'))
except FileNotFoundError:
pass
src = 'docs/_build/man'
src = os.path.join(base, 'docs/_build/man')
if not os.path.exists(src):
raise SystemExit('''\
The kitty man page is missing. If you are building from git then run: make docs
The kitty man page is missing. If you are building from git then run:
make && make docs
(needs the sphinx documentation system to be installed)
''')
shutil.copytree(src, os.path.join(mandir, 'man1'))
@@ -559,10 +560,11 @@ def copy_html_docs(ddir):
shutil.rmtree(htmldir)
except FileNotFoundError:
pass
src = 'docs/_build/html'
src = os.path.join(base, 'docs/_build/html')
if not os.path.exists(src):
raise SystemExit('''\
The kitty html docs are missing. If you are building from git then run: make html
The kitty html docs are missing. If you are building from git then run:
make && make docs
(needs the sphinx documentation system to be installed)
''')
shutil.copytree(src, htmldir)
@@ -788,7 +790,7 @@ def main():
)
elif args.action == 'linux-package':
build(args, native_optimizations=False)
if not args.for_freeze:
if not os.path.exists(os.path.join(base, 'docs/_build/html')):
run_tool(['make', 'docs'])
package(args)
elif args.action == 'osx-bundle':