Compare commits

..

49 Commits

Author SHA1 Message Date
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
43 changed files with 30056 additions and 28231 deletions

View File

@@ -150,9 +150,17 @@ before_script:
- echo LD_LIBRARY_PATH=$LD_LIBRARY_PATH PKG_CONFIG_PATH=$PKG_CONFIG_PATH - echo LD_LIBRARY_PATH=$LD_LIBRARY_PATH PKG_CONFIG_PATH=$PKG_CONFIG_PATH
- $PYTHON setup.py build --debug --verbose $SANITIZE_ARG; - $PYTHON setup.py build --debug --verbose $SANITIZE_ARG;
script: script: |
- if grep -Inr '\s$' kitty kitty_tests kittens docs *.py *.asciidoc *.rst .gitattributes .gitignore; then echo Trailing whitespace found, aborting.; exit 1; fi set -e
- if [[ -z $SANITIZE_ARG ]]; then $PYTHON test.py; else ./asan-launcher test.py; fi
- if [[ "$RUN_FLAKE" == "1" ]]; then flake8 --count .; fi if grep -Inr '\s$' kitty kitty_tests kittens docs *.py *.asciidoc *.rst .gitattributes .gitignore; then echo Trailing whitespace found, aborting.; exit 1; 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 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

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

View File

@@ -82,8 +82,9 @@ without needing to install all of kitty.
.. note:: .. note::
You need a couple of extra dependencies to build linux-package. You need a couple of extra dependencies to build linux-package.
:file:`tic` to compile terminfo files, usually found in the :file:`tic` to compile terminfo files, usually found in the
development package of :file:`ncurses`. Also, :file:`sphinx-build` development package of :file:`ncurses`. Also, if you are building form
from the `Sphinx documentation generator 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/>`_. <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

View File

@@ -3,6 +3,34 @@ Changelog
|kitty| is a feature full, cross-platform, *fast*, GPU based terminal emulator. |kitty| is a feature full, cross-platform, *fast*, GPU based terminal emulator.
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] 0.11.0 [2018-06-12]
------------------------------ ------------------------------

View File

@@ -115,6 +115,9 @@ html_theme_options = {
'github_banner': True, 'github_banner': True,
'github_user': 'kovidgoyal', 'github_user': 'kovidgoyal',
'github_repo': 'kitty', 'github_repo': 'kitty',
# increase contrast of link color with text color
'link': '#00587d',
'link_hover': 'green',
} }
@@ -245,6 +248,7 @@ def write_cli_docs(all_kitten_names):
from kitty.cmds import cli_params_for from kitty.cmds import cli_params_for
for cmd_name in all_commands: for cmd_name in all_commands:
func = cmap[cmd_name] func = cmap[cmd_name]
p(f'.. _at_{func.name}:\n')
p('kitty @', func.name + '\n' + '-' * 120) p('kitty @', func.name + '\n' + '-' * 120)
p('.. program::', 'kitty @', func.name) p('.. program::', 'kitty @', func.name)
p('\n\n' + as_rst(*cli_params_for(func))) p('\n\n' + as_rst(*cli_params_for(func)))
@@ -496,35 +500,30 @@ def write_conf_docs(app, all_kitten_names):
sc_role.warn_dangling = True sc_role.warn_dangling = True
sc_role.process_link = process_shortcut_link 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 kitty.conf.definition import as_conf_file
from textwrap import indent
with open(f'generated/conf-{name}.rst', 'w', encoding='utf-8') as f: with open(f'generated/conf-{name}.rst', 'w', encoding='utf-8') as f:
print('.. highlight:: conf\n', file=f) print('.. highlight:: conf\n', file=f)
f.write(render_conf(name, all_options.values())) f.write(render_conf(name, all_options.values()))
with open(f'generated/conf-{name}-literal.rst', 'w', encoding='utf-8') as f: conf_name = re.sub(r'^kitten-', '', name) + '.conf'
print('.. code-block:: conf\n', file=f) with open(f'generated/conf/{conf_name}', 'w', encoding='utf-8') as f:
text = '\n'.join(as_conf_file(all_options.values())) text = '\n'.join(as_conf_file(all_options.values()))
text = indent(text, ' ', lambda l: True)
print(text, file=f) print(text, file=f)
from kitty.config_data import all_options from kitty.config_data import all_options
generate(all_options) generate_default_config(all_options, 'kitty')
from kittens.runner import get_kitten_conf_docs from kittens.runner import get_kitten_conf_docs
for kitten in all_kitten_names: for kitten in all_kitten_names:
all_options = get_kitten_conf_docs(kitten) all_options = get_kitten_conf_docs(kitten)
if all_options: if all_options:
generate(all_options, f'kitten-{kitten}') generate_default_config(all_options, f'kitten-{kitten}')
# }}} # }}}
def setup(app): def setup(app):
try: os.makedirs('generated/conf', exist_ok=True)
os.mkdir('generated')
except FileExistsError:
pass
from kittens.runner import all_kitten_names from kittens.runner import all_kitten_names
all_kitten_names = all_kitten_names() all_kitten_names = all_kitten_names()
write_cli_docs(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 Sample kitty.conf
^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
Below is a sample :file:`kitty.conf` with all default settings. You can download a sample :file:`kitty.conf` file with all default settings and
comments describing each setting by clicking: :download:`sample kitty.conf
.. include:: /generated/conf-kitty-literal.rst </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 You can either use the
`OSC terminal escape codes <http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Operating-System-Commands>`_ `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>` 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? How do I specify command line options for kitty on macOS?

View File

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

View File

@@ -31,7 +31,7 @@ Major Features
Installation 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 to have either the `git <https://git-scm.com/>`_ program or the ``diff`` program
installed. Additionally, for syntax highlighting to work, installed. Additionally, for syntax highlighting to work,
`pygments <http://pygments.org/>`_ must be installed (note that pygments is `pygments <http://pygments.org/>`_ must be installed (note that pygments is
@@ -67,37 +67,29 @@ Keyboard controls
========================= =========================== ========================= ===========================
Action Shortcut Action Shortcut
========================= =========================== ========================= ===========================
Quit ``q, Ctrl+c`` Quit :kbd:`q, Ctrl+c, Esc`
Scroll line up ``k, up`` Scroll line up :kbd:`k, up`
Scroll line down ``j, down`` Scroll line down :kbd:`j, down`
Scroll page up ``PgUp`` Scroll page up :kbd:`PgUp`
Scroll page down ``PgDn`` Scroll page down :kbd:`PgDn`
Scroll to top ``Home`` Scroll to top :kbd:`Home`
Scroll to bottom ``End`` Scroll to bottom :kbd:`End`
Scroll to next page ``Space, PgDn`` Scroll to next page :kbd:`Space, PgDn`
Scroll to previous page ``PgUp`` Scroll to previous page :kbd:`PgUp`
Scroll to next change ``n`` Scroll to next change :kbd:`n`
Scroll to previous change ``p`` Scroll to previous change :kbd:`p`
Increase lines of context ``+`` Increase lines of context :kbd:`+`
Decrease lines of context ``-`` Decrease lines of context :kbd:`-`
All lines of context ``a`` All lines of context :kbd:`a`
Restore default context ``=`` 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 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. 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? 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 :) 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 Sample diff.conf
----------------- -----------------
Below is a sample :file:`diff.conf` with all default settings. You can download a sample :file:`diff.conf` file with all default settings and
comments describing each setting by clicking: :download:`sample diff.conf
.. include:: /generated/conf-kitten-diff-literal.rst </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 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, its scrolling "smoothness" is much better than that of xterm (at least to my,
admittedly biased, eyes). 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:: 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 <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__))) 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'): def get_data(fname, folder='UCD'):
url = f'https://www.unicode.org/Public/{folder}/latest/{fname}' url = f'https://www.unicode.org/Public/{folder}/latest/{fname}'
@@ -189,11 +195,12 @@ def gen_emoji():
p('\treturn false;\n}') 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 '' static = 'static inline ' if static else ''
chars = set() chars = set()
for c in classes: for c in classes:
chars |= class_maps[c] chars |= class_maps[c]
chars |= extra_chars
p(f'{static}bool\n{name}(char_type code) {{') p(f'{static}bool\n{name}(char_type code) {{')
p(f'\t// {comment} ({len(chars)} codepoints)' + ' {{' '{') p(f'\t// {comment} ({len(chars)} codepoints)' + ' {{' '{')
p('\tswitch(code) {') p('\tswitch(code) {')
@@ -245,7 +252,7 @@ def gen_ucd():
with create_header('kitty/unicode-data.c') as p: with create_header('kitty/unicode-data.c') as p:
p('#include "unicode-data.h"') 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_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_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_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)') 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) {') p('combining_type mark_for_codepoint(char_type c) {')
rmap = codepoint_to_mark_map(p, mark_map) rmap = codepoint_to_mark_map(p, mark_map)
p('}\n') 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( 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] rmap[0xfe0e], rmap[0xfe0f]
)) ))
@@ -393,7 +400,7 @@ def gen_wcwidth():
add(p, 'East Asian double width', doublewidth, 2) add(p, 'East Asian double width', doublewidth, 2)
add(p, 'Emoji Presentation', emoji_categories['Emoji_Presentation'], 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\tdefault: return 1;')
p('\t}') p('\t}')

View File

@@ -116,12 +116,9 @@ def collect_files(collection, left, right):
sanitize_pat = re.compile('[\x00-\x09\x0b-\x1f\x7f\x80-\x9f]') 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): 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) @lru_cache(maxsize=1024)

View File

@@ -52,7 +52,7 @@ def parse_scroll_by(func, rest):
@func_with_args('scroll_to') @func_with_args('scroll_to')
def parse_scroll_to(func, rest): def parse_scroll_to(func, rest):
rest = rest.lower() 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' rest = 'start'
return func, rest return func, rest
@@ -69,6 +69,14 @@ def parse_change_context(func, rest):
return func, amount 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): def special_handling(key, val, ans):
if key == 'map': if key == 'map':
action, *key_def = parse_kittens_key(val, args_funcs) 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_margin_bg', '#dbedff', long_text=_('Hunk header colors'))
c('hunk_bg', '#f1f8ff') 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') g('shortcuts')
k('quit', 'q', 'quit', _('Quit')) 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('increase_context', '+', 'change_context 5', _('Increase context'))
k('decrease_context', '-', 'change_context -5', _('Decrease 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')} 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 functools import partial
from gettext import gettext as _ 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.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.handler import Handler
from ..tui.images import ImageManager from ..tui.images import ImageManager
from ..tui.line_edit import LineEdit
from ..tui.loop import Loop from ..tui.loop import Loop
from ..tui.operations import styled from ..tui.operations import styled
from .collect import ( 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 .config import init_config
from .patch import Differ, set_diff_command from .patch import Differ, set_diff_command
from .render import ImageSupportWarning, LineRef, render_diff from .render import ImageSupportWarning, LineRef, render_diff
from .search import BadRegex, Search
try: try:
from .highlight import initialize_highlighter, highlight_collection from .highlight import initialize_highlighter, highlight_collection
@@ -30,7 +34,7 @@ except ImportError:
initialize_highlighter = None initialize_highlighter = None
INITIALIZING, COLLECTED, DIFFED = range(3) INITIALIZING, COLLECTED, DIFFED, COMMAND, MESSAGE = range(5)
def generate_diff(collection, context): def generate_diff(collection, context):
@@ -51,6 +55,10 @@ class DiffHandler(Handler):
def __init__(self, args, opts, left, right): def __init__(self, args, opts, left, right):
self.state = INITIALIZING self.state = INITIALIZING
self.message = ''
self.current_search_is_regex = True
self.current_search = None
self.line_edit = LineEdit()
self.opts = opts self.opts = opts
self.left, self.right = left, right self.left, self.right = left, right
self.report_traceback_on_exit = None self.report_traceback_on_exit = None
@@ -76,6 +84,8 @@ class DiffHandler(Handler):
where = args[0] where = args[0]
if 'change' in where: if 'change' in where:
return self.scroll_to_next_change(backwards='prev' 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: if 'page' in where:
amt = self.num_lines * (1 if 'next' in where else -1) amt = self.num_lines * (1 if 'next' in where else -1)
else: else:
@@ -91,6 +101,9 @@ class DiffHandler(Handler):
else: else:
new_ctx += to new_ctx += to
return self.change_context_count(new_ctx) return self.change_context_count(new_ctx)
if func == 'start_search':
self.start_search(*args)
return
def create_collection(self): def create_collection(self):
self.start_job('collect', create_collection, self.left, self.right) self.start_job('collect', create_collection, self.left, self.right)
@@ -107,10 +120,13 @@ class DiffHandler(Handler):
def render_diff(self): 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.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) self.ref_path_map = defaultdict(list)
for i, l in enumerate(self.diff_lines): for i, l in enumerate(self.diff_lines):
self.ref_path_map[l.ref.path].append((i, l.ref)) self.ref_path_map[l.ref.path].append((i, l.ref))
self.max_scroll_pos = len(self.diff_lines) - self.num_lines 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 @property
def current_position(self): def current_position(self):
@@ -152,6 +168,18 @@ class DiffHandler(Handler):
return return
self.cmd.bell() self.cmd.bell()
def scroll_to_next_match(self, backwards=False):
if self.current_search is not None:
if backwards:
r = range(self.scroll_pos - 1, -1, -1)
else:
r = range(self.scroll_pos + 1, 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): def set_scrolling_region(self):
self.cmd.set_scrolling_region(self.screen_size, 0, self.num_lines - 2) self.cmd.set_scrolling_region(self.screen_size, 0, self.num_lines - 2)
@@ -178,7 +206,16 @@ class DiffHandler(Handler):
def init_terminal_state(self): def init_terminal_state(self):
self.cmd.set_line_wrapping(False) self.cmd.set_line_wrapping(False)
self.cmd.set_window_title(main.title) 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): def initialize(self):
self.init_terminal_state() self.init_terminal_state()
@@ -187,12 +224,7 @@ class DiffHandler(Handler):
self.create_collection() self.create_collection()
def enforce_cursor_state(self): def enforce_cursor_state(self):
self.cmd.set_cursor_visible(self.state > DIFFED) self.cmd.set_cursor_visible(self.state == COMMAND)
def finalize(self):
self.cmd.set_cursor_visible(True)
self.cmd.set_default_colors()
self.cmd.set_scrolling_region()
def draw_lines(self, num, offset=0): def draw_lines(self, num, offset=0):
offset += self.scroll_pos offset += self.scroll_pos
@@ -208,6 +240,8 @@ class DiffHandler(Handler):
if line.image_data is not None: if line.image_data is not None:
image_involved = True image_involved = True
self.write('\r\x1b[K' + text + '\x1b[0m') 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: if i < num - 1:
self.write('\n') self.write('\n')
if image_involved: if image_involved:
@@ -262,19 +296,29 @@ class DiffHandler(Handler):
def draw_status_line(self): def draw_status_line(self):
if self.state < DIFFED: if self.state < DIFFED:
return return
self.enforce_cursor_state()
self.cmd.set_cursor_position(0, self.num_lines) self.cmd.set_cursor_position(0, self.num_lines)
self.cmd.clear_to_eol() self.cmd.clear_to_eol()
scroll_frac = styled('{:.0%}'.format(self.scroll_pos / (self.max_scroll_pos or 1)), fg=self.opts.margin_fg) if self.state is COMMAND:
counts = '{}{}{}'.format( self.line_edit.write(self.write)
styled(str(self.added_count), fg=self.opts.highlight_added_bg), elif self.state is MESSAGE:
styled(',', fg=self.opts.margin_fg), self.cmd.styled(self.message, reverse=True)
styled(str(self.removed_count), fg=self.opts.highlight_removed_bg) else:
) sp = '{:.0%}'.format(self.scroll_pos/self.max_scroll_pos) if self.scroll_pos and self.max_scroll_pos else '0%'
suffix = counts + ' ' + scroll_frac scroll_frac = styled(sp, fg=self.opts.margin_fg)
prefix = styled(':', fg=self.opts.margin_fg) if self.current_search is None:
filler = self.screen_size.cols - wcswidth(prefix) - wcswidth(suffix) counts = '{}{}{}'.format(
text = '{}{}{}'.format(prefix, ' ' * filler, suffix) styled(str(self.added_count), fg=self.opts.highlight_added_bg),
self.write(text) 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): def change_context_count(self, new_ctx):
new_ctx = max(0, new_ctx) new_ctx = max(0, new_ctx)
@@ -285,14 +329,75 @@ class DiffHandler(Handler):
self.restore_position = self.current_position self.restore_position = self.current_position
self.draw_screen() 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 not self.current_search(self.diff_lines, self.margin_size, self.screen_size.cols):
self.state = MESSAGE
self.message = sanitize(_('No matches found'))
self.cmd.bell()
def on_text(self, text, in_bracketed_paste=False): 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) action = self.shortcut_action(text)
if action is not None: if action is not None:
return self.perform_action(action) return self.perform_action(action)
def on_key(self, key_event): 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: if key_event.type is RELEASE:
return 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) action = self.shortcut_action(key_event)
if action is not None: if action is not None:
return self.perform_action(action) return self.perform_action(action)
@@ -399,11 +504,9 @@ def main(args):
opts = init_config(args) opts = init_config(args)
set_diff_command(opts.diff_cmd) set_diff_command(opts.diff_cmd)
lines_for_path.replace_tab_by = opts.replace_tab_by lines_for_path.replace_tab_by = opts.replace_tab_by
# Ensure the kitten works even when stdin/stdout are redirected for f in left, right:
if not sys.stdin.isatty(): if not os.path.exists(f):
sys.stdin = open('/dev/tty') raise SystemExit('{} does not exist'.format(f))
if not sys.stdout.isatty():
sys.stdout = open('/dev/tty', 'w')
loop = Loop() loop = Loop()
handler = DiffHandler(args, opts, left, right) 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: if patch is not None:
largest_line_number = max(largest_line_number, patch.largest_line_number) 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 last_item_num = len(collection) - 1
for i, (path, item_type, other_path) in enumerate(collection): 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

@@ -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

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() self.handler.on_eot()
else: else:
self.handler.on_text(chunk, self.in_bracketed_paste) self.handler.on_text(chunk, self.in_bracketed_paste)
else: elif chunk:
self.handler.on_text(chunk, self.in_bracketed_paste) self.handler.on_text(chunk, self.in_bracketed_paste)
def _on_dcs(self, dcs): 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) 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: def set_scrolling_region(screen_size=None, top=None, bottom=None) -> str:
if screen_size is None: if screen_size is None:
return '\033[r' return '\033[r'
@@ -182,10 +189,11 @@ def init_state(alternate_screen=True):
reset_mode('IRM') + reset_mode('DECKM') + reset_mode('DECSCNM') + reset_mode('IRM') + reset_mode('DECKM') + reset_mode('DECSCNM') +
set_mode('DECARM') + reset_mode('DECOM') + set_mode('DECAWM') + set_mode('DECARM') + reset_mode('DECOM') + set_mode('DECAWM') +
set_mode('DECTCEM') + reset_mode('MOUSE_BUTTON_TRACKING') + set_mode('DECTCEM') + reset_mode('MOUSE_BUTTON_TRACKING') +
reset_mode('MOUSE_MOTION_TRACKING') + reset_mode('MOUSE_MOVE_TRACKING') reset_mode('MOUSE_MOTION_TRACKING') + reset_mode('MOUSE_MOVE_TRACKING') +
+ reset_mode('FOCUS_TRACKING') + reset_mode('MOUSE_UTF8_MODE') + reset_mode('FOCUS_TRACKING') + reset_mode('MOUSE_UTF8_MODE') +
reset_mode('MOUSE_SGR_MODE') + reset_mode('MOUSE_UTF8_MODE') + reset_mode('MOUSE_SGR_MODE') + reset_mode('MOUSE_UTF8_MODE') +
set_mode('BRACKETED_PASTE') + set_mode('EXTENDED_KEYBOARD') + set_mode('BRACKETED_PASTE') + set_mode('EXTENDED_KEYBOARD') +
'\033]30001\033\\' +
'\033[*x' # reset DECSACE to default region select '\033[*x' # reset DECSACE to default region select
) )
if alternate_screen: if alternate_screen:
@@ -200,6 +208,7 @@ def reset_state(normal_screen=True):
ans += reset_mode('ALTERNATE_SCREEN') ans += reset_mode('ALTERNATE_SCREEN')
ans += RESTORE_PRIVATE_MODE_VALUES ans += RESTORE_PRIVATE_MODE_VALUES
ans += RESTORE_CURSOR ans += RESTORE_CURSOR
ans += '\033]30101\033\\'
return ans return ans
@@ -218,16 +227,21 @@ def alternate_screen(f=None):
print(reset_mode('ALTERNATE_SCREEN'), end='', file=f) 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 = '' ans = ''
if fg is None:
ans += '\x1b]110\x1b\\' def item(which, num):
else: nonlocal ans
ans += '\x1b]10;{}\x1b\\'.format(color_as_sharp(fg if isinstance(fg, Color) else to_color(fg))) if which is None:
if bg is None: ans += '\x1b]1{}\x1b\\'.format(num)
ans += '\x1b]111\x1b\\' else:
else: ans += '\x1b]{};{}\x1b\\'.format(num, color_as_sharp(which if isinstance(which, Color) else to_color(which)))
ans += '\x1b]11;{}\x1b\\'.format(color_as_sharp(bg if isinstance(bg, Color) else to_color(bg)))
item(fg, 10)
item(bg, 11)
item(cursor, 12)
item(select_bg, 17)
item(select_fg, 19)
return ans return ans

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -81,6 +81,7 @@ class Boss:
self.pending_sequences = None self.pending_sequences = None
self.cached_values = cached_values self.cached_values = cached_values
self.os_window_map = {} self.os_window_map = {}
self.os_window_death_actions = {}
self.cursor_blinking = True self.cursor_blinking = True
self.shutting_down = False self.shutting_down = False
talk_fd = getattr(single_instance, 'socket', None) talk_fd = getattr(single_instance, 'socket', None)
@@ -95,7 +96,7 @@ class Boss:
) )
set_boss(self) set_boss(self)
self.opts, self.args = opts, args 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() self.keymap = self.opts.keymap.copy()
if new_os_window_trigger is not None: if new_os_window_trigger is not None:
self.keymap.pop(new_os_window_trigger, None) self.keymap.pop(new_os_window_trigger, None)
@@ -239,7 +240,9 @@ class Boss:
if not os.path.isabs(args.directory): if not os.path.isabs(args.directory):
args.directory = os.path.join(msg['cwd'], args.directory) args.directory = os.path.join(msg['cwd'], args.directory)
session = create_session(opts, args, respect_cwd=True) 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: else:
log_error('Unknown message received from peer, ignoring') log_error('Unknown message received from peer, ignoring')
@@ -311,10 +314,12 @@ class Boss:
tm.resize() tm.resize()
def increase_font_size(self): # legacy 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 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 def restore_font_size(self): # legacy
self.set_font_size(self.opts.font_size) self.set_font_size(self.opts.font_size)
@@ -527,6 +532,23 @@ class Boss:
tm.destroy() 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): 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) 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): def display_scrollback(self, window, data, cmd):
tab = self.active_tab tab = self.active_tab

View File

@@ -91,6 +91,14 @@ with the same :option:`kitty --instance-group` will result in new windows being
in the first :italic:`{appname}` instance within that group 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 --listen-on
Tell kitty to listen on the specified address for control Tell kitty to listen on the specified address for control
messages. For example, :option:`{appname} --listen-on`=unix:/tmp/mykitty or messages. For example, :option:`{appname} --listen-on`=unix:/tmp/mykitty or

View File

@@ -108,8 +108,8 @@ patch_color_profiles(PyObject *module UNUSED, PyObject *args) {
v = PyDict_GetItemString(spec, key); v = PyDict_GetItemString(spec, key);
if (v) { if (v) {
color_type color = PyLong_AsUnsignedLong(v); color_type color = PyLong_AsUnsignedLong(v);
for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(profiles); i++) { for (Py_ssize_t j = 0; j < PyTuple_GET_SIZE(profiles); j++) {
self = (ColorProfile*)PyTuple_GET_ITEM(profiles, i); self = (ColorProfile*)PyTuple_GET_ITEM(profiles, j);
self->color_table[i] = color; self->color_table[i] = color;
if (change_configured) self->orig_color_table[i] = color; if (change_configured) self->orig_color_table[i] = color;
self->dirty = true; self->dirty = true;
@@ -220,6 +220,21 @@ copy_color_table_to_buffer(ColorProfile *self, color_type *buf, int offset, size
self->dirty = false; 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* static PyObject*
color_table_address(ColorProfile *self, PyObject *a UNUSED) { color_table_address(ColorProfile *self, PyObject *a UNUSED) {
#define color_table_address_doc "Pointer address to start of color table" #define color_table_address_doc "Pointer address to start of color table"

View File

@@ -3,6 +3,7 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
# Utils {{{ # Utils {{{
import os
from gettext import gettext as _ from gettext import gettext as _
from . import fast_data_types as defines from . import fast_data_types as defines
@@ -10,7 +11,7 @@ from .conf.definition import option_func
from .conf.utils import ( from .conf.utils import (
choices, positive_float, positive_int, to_cmdline, to_color, unit_float 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 .fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE
from .layout import all_layouts from .layout import all_layouts
from .rgb import color_as_int, color_as_sharp, color_from_int from .rgb import color_as_int, color_as_sharp, color_from_int
@@ -616,6 +617,25 @@ opening new windows, closing windows, reading the content of windows, etc.
Note that this even works over ssh connections. 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=_(''' 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 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: 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 from .fast_data_types import set_boss as set_c_boss
appname = 'kitty' appname = 'kitty'
version = (0, 11, 0) version = (0, 11, 1)
str_version = '.'.join(map(str, version)) str_version = '.'.join(map(str, version))
_plat = sys.platform.lower() _plat = sys.platform.lower()
is_macos = 'darwin' in _plat is_macos = 'darwin' in _plat

View File

@@ -211,6 +211,8 @@ typedef struct {
bool dirty; bool dirty;
uint32_t color_table[256]; uint32_t color_table[256];
uint32_t orig_color_table[256]; uint32_t orig_color_table[256];
DynamicColor dynamic_color_stack[10];
size_t dynamic_color_stack_idx;
DynamicColor configured, overridden; DynamicColor configured, overridden;
} ColorProfile; } ColorProfile;
@@ -275,6 +277,8 @@ bool set_iutf8(int, bool);
color_type colorprofile_to_color(ColorProfile *self, color_type entry, color_type defval); 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 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 set_mouse_cursor(MouseShape);
void mouse_event(int, int); void mouse_event(int, 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 // see gen-wcwidth.py
#pragma once #pragma once
#include "data-types.h" #include "data-types.h"

View File

@@ -15,17 +15,55 @@ from .constants import (
appname, config_dir, glfw_path, is_macos, is_wayland, logo_data_file appname, config_dir, glfw_path, is_macos, is_wayland, logo_data_file
) )
from .fast_data_types import ( from .fast_data_types import (
create_os_window, free_font_data, glfw_init, glfw_terminate, GLFW_MOD_SUPER, create_os_window, free_font_data, glfw_init,
set_default_window_icon, set_options, GLFW_MOD_SUPER glfw_terminate, set_default_window_icon, set_options
) )
from .fonts.box_drawing import set_scale from .fonts.box_drawing import set_scale
from .fonts.render import set_font_family from .fonts.render import set_font_family
from .utils import ( 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 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): def load_all_shaders(semi_transparent=0):
load_shader_programs(semi_transparent) load_shader_programs(semi_transparent)
load_borders_program() load_borders_program()
@@ -200,12 +238,7 @@ def _main():
if args.single_instance: if args.single_instance:
is_first = single_instance(args.instance_group) is_first = single_instance(args.instance_group)
if not is_first: if not is_first:
import json talk_to_instance(args)
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)
return return
opts = create_opts(args) opts = create_opts(args)
if opts.editor != '.': if opts.editor != '.':

View File

@@ -344,6 +344,14 @@ dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
case 52: case 52:
DISPATCH_OSC(clipboard_control); DISPATCH_OSC(clipboard_control);
break; 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: default:
REPORT_ERROR("Unknown OSC code: %u", code); REPORT_ERROR("Unknown OSC code: %u", code);
break; break;

View File

@@ -112,6 +112,7 @@ void
screen_reset(Screen *self) { screen_reset(Screen *self) {
if (self->linebuf == self->alt_linebuf) screen_toggle_screen_buffer(self); if (self->linebuf == self->alt_linebuf) screen_toggle_screen_buffer(self);
linebuf_clear(self->linebuf, BLANK_CHAR); linebuf_clear(self->linebuf, BLANK_CHAR);
historybuf_clear(self->historybuf);
grman_clear(self->grman, false, self->cell_size); grman_clear(self->grman, false, self->cell_size);
self->modes = empty_modes; self->modes = empty_modes;
#define R(name) self->color_profile->overridden.name = 0 #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); 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 void
screen_handle_print(Screen *self, PyObject *msg) { screen_handle_print(Screen *self, PyObject *msg) {
CALLBACK("handle_remote_print", "O", msg); CALLBACK("handle_remote_print", "O", msg);

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_set_margins(Screen *self, unsigned int top, unsigned int bottom);
void screen_change_charset(Screen *, uint32_t to); void screen_change_charset(Screen *, uint32_t to);
void screen_handle_cmd(Screen *, PyObject *cmd); 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_handle_print(Screen *, PyObject *cmd);
void screen_designate_charset(Screen *, uint32_t which, uint32_t as); void screen_designate_charset(Screen *, uint32_t which, uint32_t as);
void screen_use_latin1(Screen *, bool); void screen_use_latin1(Screen *, bool);

View File

@@ -7,6 +7,7 @@ import shlex
from .config_data import to_layout_names from .config_data import to_layout_names
from .constants import shell_path from .constants import shell_path
from .layout import all_layouts from .layout import all_layouts
from .utils import log_error
class Tab: class Tab:
@@ -105,10 +106,18 @@ def parse_session(raw, opts):
return ans 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: if args and args.session:
with open(args.session) as f: with open(args.session) as f:
return parse_session(f.read(), opts) 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() ans = Session()
current_layout = opts.enabled_layouts[0] if opts.enabled_layouts else 'tall' current_layout = opts.enabled_layouts[0] if opts.enabled_layouts else 'tall'
ans.add_tab(opts) ans.add_tab(opts)

File diff suppressed because one or more lines are too long

View File

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

View File

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

1464
kitty/wcwidth-std.h generated

File diff suppressed because it is too large Load Diff

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

@@ -311,9 +311,9 @@ def run_upload(args):
files = { files = {
os.path.join(build_path, 'build', f.format(version)): desc os.path.join(build_path, 'build', f.format(version)): desc
for f, desc in { 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', '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() }.items()
} }
files[f'build/kitty-{version}.tar.xz'] = 'Source code' 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')) shutil.rmtree(os.path.join(mandir, 'man1'))
except FileNotFoundError: except FileNotFoundError:
pass pass
src = 'docs/_build/man' src = os.path.join(base, 'docs/_build/man')
if not os.path.exists(src): if not os.path.exists(src):
raise SystemExit('''\ 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) (needs the sphinx documentation system to be installed)
''') ''')
shutil.copytree(src, os.path.join(mandir, 'man1')) shutil.copytree(src, os.path.join(mandir, 'man1'))
@@ -559,10 +560,11 @@ def copy_html_docs(ddir):
shutil.rmtree(htmldir) shutil.rmtree(htmldir)
except FileNotFoundError: except FileNotFoundError:
pass pass
src = 'docs/_build/html' src = os.path.join(base, 'docs/_build/html')
if not os.path.exists(src): if not os.path.exists(src):
raise SystemExit('''\ 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) (needs the sphinx documentation system to be installed)
''') ''')
shutil.copytree(src, htmldir) shutil.copytree(src, htmldir)
@@ -788,7 +790,7 @@ def main():
) )
elif args.action == 'linux-package': elif args.action == 'linux-package':
build(args, native_optimizations=False) 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']) run_tool(['make', 'docs'])
package(args) package(args)
elif args.action == 'osx-bundle': elif args.action == 'osx-bundle':