Compare commits
139 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57f8519a52 | ||
|
|
056be45dd5 | ||
|
|
3ee22e545e | ||
|
|
db5aa36f44 | ||
|
|
cc767061cc | ||
|
|
1d22b75090 | ||
|
|
4313531432 | ||
|
|
1faddeb402 | ||
|
|
2b035739f8 | ||
|
|
aeed20087e | ||
|
|
a239f41495 | ||
|
|
b36d6967a5 | ||
|
|
7c1d13b7db | ||
|
|
3e99949db8 | ||
|
|
aa12a65a8f | ||
|
|
18804efb7e | ||
|
|
a5d27196ea | ||
|
|
5af6d88ffb | ||
|
|
472a3bc69f | ||
|
|
f0f1c911ea | ||
|
|
fcb0033d25 | ||
|
|
c8b1e76783 | ||
|
|
b50f621e61 | ||
|
|
77c4f5fecc | ||
|
|
e27ea3337a | ||
|
|
23fbf6f157 | ||
|
|
321e5081ed | ||
|
|
33cac29f87 | ||
|
|
12be438dea | ||
|
|
00092db6ad | ||
|
|
619c3a0025 | ||
|
|
f12697c897 | ||
|
|
4bf0e0ea23 | ||
|
|
c7879d06c9 | ||
|
|
05227e7dfb | ||
|
|
9805d164d0 | ||
|
|
05384c46e7 | ||
|
|
c2679eff9a | ||
|
|
3ec66c0b9f | ||
|
|
9277b80fad | ||
|
|
568a4c9b5d | ||
|
|
4de18688ca | ||
|
|
12975347ee | ||
|
|
e35c3cc913 | ||
|
|
65fef81f29 | ||
|
|
e3f60e6fa7 | ||
|
|
0c88e11cd7 | ||
|
|
d689c21283 | ||
|
|
5362d1a7a6 | ||
|
|
3b1490020d | ||
|
|
b8ba25b81f | ||
|
|
8bfdc47998 | ||
|
|
2491fafe98 | ||
|
|
b5bc466aef | ||
|
|
cfd6ea4301 | ||
|
|
2cdf41f190 | ||
|
|
9169d0aa9b | ||
|
|
05a25e3531 | ||
|
|
2852ce9a4d | ||
|
|
20f8f2c401 | ||
|
|
0995187080 | ||
|
|
d10eac1b02 | ||
|
|
2cc2f803f1 | ||
|
|
66a52ac588 | ||
|
|
32f22e0ada | ||
|
|
c037f53534 | ||
|
|
aba84581f5 | ||
|
|
82f6fbff77 | ||
|
|
d16215ab6e | ||
|
|
45b0233f60 | ||
|
|
1ce4366972 | ||
|
|
c8fc21d336 | ||
|
|
5dd3243674 | ||
|
|
815dd99f7c | ||
|
|
bf0dcb1d2e | ||
|
|
031fc10859 | ||
|
|
a644e4c31e | ||
|
|
0398dbdcbe | ||
|
|
84b2fbba28 | ||
|
|
491f233d33 | ||
|
|
5e38af6d24 | ||
|
|
4c0f68150a | ||
|
|
3f25f70783 | ||
|
|
d7007f77aa | ||
|
|
94273941ad | ||
|
|
c09542ea6a | ||
|
|
c5583d380d | ||
|
|
f820f72768 | ||
|
|
6db1f8e3e3 | ||
|
|
2aba3e249c | ||
|
|
f66db65974 | ||
|
|
5b6038db35 | ||
|
|
9a365b0968 | ||
|
|
8a0cd73821 | ||
|
|
49b065cbe7 | ||
|
|
77f651cb5d | ||
|
|
fedde51c12 | ||
|
|
f710d6906e | ||
|
|
c9a751360e | ||
|
|
ad97315925 | ||
|
|
e8f7a2b345 | ||
|
|
5bc8d05f0a | ||
|
|
3669d4135c | ||
|
|
73b870bb87 | ||
|
|
6b4217c12a | ||
|
|
8b4fe494cf | ||
|
|
35041e7709 | ||
|
|
12cbcf1c17 | ||
|
|
26f50a451e | ||
|
|
11f3d7e215 | ||
|
|
a7d533cfe5 | ||
|
|
52ec4dc21b | ||
|
|
6f7ea49008 | ||
|
|
84b99b1b3d | ||
|
|
1022d8a0ee | ||
|
|
cd6a46730a | ||
|
|
59743fd7a1 | ||
|
|
07bd371e21 | ||
|
|
fe3b10a8fb | ||
|
|
c994bc1d89 | ||
|
|
22cffef7c8 | ||
|
|
5dfb8b1d80 | ||
|
|
82f9f002bd | ||
|
|
9f51506422 | ||
|
|
2a8295d71c | ||
|
|
7214b66aa5 | ||
|
|
a7d9c63a4e | ||
|
|
61dd52b50f | ||
|
|
a157f3c5e3 | ||
|
|
fc02246373 | ||
|
|
adb8a4676c | ||
|
|
f78d05a57f | ||
|
|
301d34fecf | ||
|
|
0848816644 | ||
|
|
ba7f0132f2 | ||
|
|
561fe81d00 | ||
|
|
2db83ecf1e | ||
|
|
1b8c6b23e1 | ||
|
|
b6622b9458 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -15,3 +15,4 @@ __pycache__
|
||||
glfw/wayland-*-client-protocol.[ch]
|
||||
docs/_build
|
||||
docs/generated
|
||||
.DS_Store
|
||||
|
||||
22
.travis.yml
22
.travis.yml
@@ -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
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: %YEAR%, %USER% <%MAIL%>
|
||||
|
||||
%HERE%
|
||||
3
Makefile
3
Makefile
@@ -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
|
||||
|
||||
@@ -26,6 +26,13 @@ def runpy(args):
|
||||
exec(args[1])
|
||||
|
||||
|
||||
def hold(args):
|
||||
import subprocess
|
||||
ret = subprocess.Popen(args[1:]).wait()
|
||||
sys.stdin.read()
|
||||
raise SystemExit(ret)
|
||||
|
||||
|
||||
def launch(args):
|
||||
import runpy
|
||||
sys.argv = args[1:]
|
||||
@@ -61,6 +68,7 @@ entry_points = {
|
||||
'+': namespaced,
|
||||
}
|
||||
namespaced_entry_points = {k: v for k, v in entry_points.items() if k[0] not in '+@'}
|
||||
namespaced_entry_points['hold'] = hold
|
||||
|
||||
|
||||
def setup_openssl_environment():
|
||||
|
||||
@@ -60,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.
|
||||
|
||||
@@ -3,6 +3,105 @@ Changelog
|
||||
|
||||
|kitty| is a feature full, cross-platform, *fast*, GPU based terminal emulator.
|
||||
|
||||
0.11.3 [2018-07-10]
|
||||
------------------------------
|
||||
|
||||
- Draw only the minimum borders needed for inactive windows. That is only the borders
|
||||
that separate the inactive window from a neighbor. Note that setting
|
||||
a non-zero window margin overrides this and causes all borders to be drawn.
|
||||
The old behavior of drawing all borders can be restored via the
|
||||
:opt:`draw_minimal_borders` setting in kitty.conf.
|
||||
|
||||
- macOS: Add an option :opt:`macos_window_resizable` to control if kitty
|
||||
top-level windows are resizable using the mouse or not (:iss:`698`)
|
||||
|
||||
- macOS: Use a custom mouse cursor that shows up well on both light and dark backgrounds
|
||||
(:iss:`359`)
|
||||
|
||||
- macOS: Workaround for switching from fullscreen to windowed mode with the
|
||||
titlebar hidden causing window resizing to not work. (:iss:`711`)
|
||||
|
||||
- Fix triple-click to select line not working when the entire line is filled
|
||||
(:iss:`703`)
|
||||
|
||||
- When dragging to select with the mouse "grab" the mouse so that if it strays
|
||||
into neighboring windows, the selection is still updated (:pull:`624`)
|
||||
|
||||
- When clicking in the margin/border area of a window, map the click to the
|
||||
nearest cell in the window. Avoids selection with the mouse failing when
|
||||
starting the selection just outside the window.
|
||||
|
||||
- When drag-scrolling stop the scroll when the mouse button is released.
|
||||
|
||||
- Fix a regression in the previous release that caused pasting large amounts
|
||||
of text to be duplicated (:iss:`709`)
|
||||
|
||||
|
||||
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]
|
||||
------------------------------
|
||||
|
||||
|
||||
25
docs/conf.py
25
docs/conf.py
@@ -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)
|
||||
|
||||
@@ -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>`.
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -72,8 +72,8 @@ class Reporter: # {{{
|
||||
def get_latest_release_data():
|
||||
print('Checking for latest release on GitHub...')
|
||||
req = urllib.Request('https://api.github.com/repos/kovidgoyal/kitty/releases/latest', headers={'Accept': 'application/vnd.github.v3+json'})
|
||||
res = urllib.urlopen(req)
|
||||
data = json.load(res)
|
||||
res = urllib.urlopen(req).read().decode('utf-8')
|
||||
data = json.loads(res)
|
||||
html_url = data['html_url'].replace('/tag/', '/download/').rstrip('/')
|
||||
for asset in data.get('assets', ()):
|
||||
name = asset['name']
|
||||
|
||||
@@ -5,7 +5,7 @@ See :ref:`extended-key-protocol` for more information and `this table in JSON
|
||||
format <https://github.com/kovidgoyal/kitty/blob/master/key_encoding.json>`_.
|
||||
|
||||
===================== ======================
|
||||
Name Encoded representation (base64)
|
||||
Name Encoded representation
|
||||
0 ``G``
|
||||
1 ``H``
|
||||
2 ``I``
|
||||
@@ -126,4 +126,3 @@ X ``p``
|
||||
Y ``q``
|
||||
Z ``r``
|
||||
===================== ======================
|
||||
|
||||
|
||||
@@ -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>`.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}')
|
||||
|
||||
@@ -43,6 +43,8 @@ def export():
|
||||
switch = ['glad_debug_{0} = glad_{0}'.format(f) for f in functions]
|
||||
c = c.replace('<glad/glad.h>', '"gl-wrapper.h"', 1)
|
||||
c = '#pragma GCC diagnostic ignored "-Wpedantic"\n' + c
|
||||
# See https://github.com/kovidgoyal/kitty/issues/654
|
||||
c = c.replace('strncpy(local_str, gl_str_tmp, len+1)', 'memcpy(local_str, gl_str_tmp, len+1)', 1)
|
||||
c += '''
|
||||
int
|
||||
init_glad(GLADloadproc load, int debug) {
|
||||
|
||||
@@ -69,7 +69,6 @@ static void changeToResourcesDirectory(void)
|
||||
static void createKeyTables(void)
|
||||
{
|
||||
int scancode;
|
||||
_glfw.ns.debug_keyboard = getenv("GLFW_DEBUG_KEYBOARD") != NULL;
|
||||
|
||||
memset(_glfw.ns.keycodes, -1, sizeof(_glfw.ns.keycodes));
|
||||
memset(_glfw.ns.scancodes, -1, sizeof(_glfw.ns.scancodes));
|
||||
|
||||
1
glfw/cocoa_platform.h
vendored
1
glfw/cocoa_platform.h
vendored
@@ -121,7 +121,6 @@ typedef struct _GLFWlibraryNS
|
||||
|
||||
char keyName[64];
|
||||
char text[256];
|
||||
GLFWbool debug_keyboard;
|
||||
short int keycodes[256];
|
||||
short int scancodes[GLFW_KEY_LAST + 1];
|
||||
char* clipboardString;
|
||||
|
||||
@@ -209,7 +209,7 @@ static int translateFlags(NSUInteger flags)
|
||||
return mods;
|
||||
}
|
||||
|
||||
#define debug_key(...) if (_glfw.ns.debug_keyboard) NSLog(__VA_ARGS__)
|
||||
#define debug_key(...) if (_glfw.hints.init.debugKeyboard) NSLog(__VA_ARGS__)
|
||||
|
||||
static inline const char*
|
||||
format_mods(int mods) {
|
||||
@@ -1886,44 +1886,44 @@ int _glfwPlatformGetKeyScancode(int key)
|
||||
|
||||
int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
|
||||
const GLFWimage* image,
|
||||
int xhot, int yhot)
|
||||
int xhot, int yhot, int count)
|
||||
{
|
||||
NSImage* native;
|
||||
NSBitmapImageRep* rep;
|
||||
|
||||
if (!initializeAppKit())
|
||||
return GLFW_FALSE;
|
||||
|
||||
rep = [[NSBitmapImageRep alloc]
|
||||
initWithBitmapDataPlanes:NULL
|
||||
pixelsWide:image->width
|
||||
pixelsHigh:image->height
|
||||
bitsPerSample:8
|
||||
samplesPerPixel:4
|
||||
hasAlpha:YES
|
||||
isPlanar:NO
|
||||
colorSpaceName:NSCalibratedRGBColorSpace
|
||||
bitmapFormat:NSAlphaNonpremultipliedBitmapFormat
|
||||
bytesPerRow:image->width * 4
|
||||
bitsPerPixel:32];
|
||||
|
||||
if (rep == nil)
|
||||
native = [[NSImage alloc] initWithSize:NSMakeSize(image->width, image->height)];
|
||||
if (native == nil)
|
||||
return GLFW_FALSE;
|
||||
|
||||
memcpy([rep bitmapData], image->pixels, image->width * image->height * 4);
|
||||
for (int i = 0; i < count; i++) {
|
||||
const GLFWimage *src = image + i;
|
||||
rep = [[NSBitmapImageRep alloc]
|
||||
initWithBitmapDataPlanes:NULL
|
||||
pixelsWide:src->width
|
||||
pixelsHigh:src->height
|
||||
bitsPerSample:8
|
||||
samplesPerPixel:4
|
||||
hasAlpha:YES
|
||||
isPlanar:NO
|
||||
colorSpaceName:NSCalibratedRGBColorSpace
|
||||
bitmapFormat:NSAlphaNonpremultipliedBitmapFormat
|
||||
bytesPerRow:src->width * 4
|
||||
bitsPerPixel:32];
|
||||
if (rep == nil)
|
||||
return GLFW_FALSE;
|
||||
|
||||
native = [[NSImage alloc] initWithSize:NSMakeSize(image->width, image->height)];
|
||||
[native addRepresentation:rep];
|
||||
memcpy([rep bitmapData], src->pixels, src->width * src->height * 4);
|
||||
[native addRepresentation:rep];
|
||||
[rep release];
|
||||
}
|
||||
|
||||
cursor->ns.object = [[NSCursor alloc] initWithImage:native
|
||||
hotSpot:NSMakePoint(xhot, yhot)];
|
||||
|
||||
[native release];
|
||||
[rep release];
|
||||
|
||||
if (cursor->ns.object == nil)
|
||||
return GLFW_FALSE;
|
||||
|
||||
return GLFW_TRUE;
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,8 @@ def init_env(env, pkg_config, at_least_version, module='x11'):
|
||||
at_least_version('wayland-protocols', *sinfo['wayland_protocols'])
|
||||
ans.wayland_packagedir = os.path.abspath(pkg_config('wayland-protocols', '--variable=pkgdatadir')[0])
|
||||
ans.wayland_scanner = os.path.abspath(pkg_config('wayland-scanner', '--variable=wayland_scanner')[0])
|
||||
scanner_version = tuple(map(int, pkg_config('wayland-scanner', '--modversion')[0].strip().split('.')))
|
||||
ans.wayland_scanner_code = 'private-code' if scanner_version >= (1, 14, 91) else 'code'
|
||||
ans.wayland_protocols = tuple(sinfo[module]['protocols'])
|
||||
for p in ans.wayland_protocols:
|
||||
ans.sources.append(wayland_protocol_file_name(p))
|
||||
@@ -87,7 +89,7 @@ def build_wayland_protocols(env, run_tool, emphasis, newer, dest_dir):
|
||||
dest = wayland_protocol_file_name(src, ext)
|
||||
dest = os.path.join(dest_dir, dest)
|
||||
if newer(dest, src):
|
||||
q = 'client-header' if ext == 'h' else 'code'
|
||||
q = 'client-header' if ext == 'h' else env.wayland_scanner_code
|
||||
run_tool([env.wayland_scanner, q, src, dest],
|
||||
desc='Generating {} ...'.format(emphasis(os.path.basename(dest))))
|
||||
|
||||
@@ -208,6 +210,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))
|
||||
|
||||
6
glfw/glfw3.h
vendored
6
glfw/glfw3.h
vendored
@@ -1057,6 +1057,7 @@ extern "C" {
|
||||
/*! @addtogroup init
|
||||
* @{ */
|
||||
#define GLFW_JOYSTICK_HAT_BUTTONS 0x00050001
|
||||
#define GLFW_DEBUG_KEYBOARD 0x00050002
|
||||
|
||||
#define GLFW_COCOA_CHDIR_RESOURCES 0x00051001
|
||||
#define GLFW_COCOA_MENUBAR 0x00051002
|
||||
@@ -4122,6 +4123,7 @@ GLFWAPI void glfwSetCursorPos(GLFWwindow* window, double xpos, double ypos);
|
||||
* @param[in] image The desired cursor image.
|
||||
* @param[in] xhot The desired x-coordinate, in pixels, of the cursor hotspot.
|
||||
* @param[in] yhot The desired y-coordinate, in pixels, of the cursor hotspot.
|
||||
* @param[in] count The number of images. Used on Cocoa for retina cursors. The first image should be the 1:1 scale image.
|
||||
* @return The handle of the created cursor, or `NULL` if an
|
||||
* [error](@ref error_handling) occurred.
|
||||
*
|
||||
@@ -4137,11 +4139,11 @@ GLFWAPI void glfwSetCursorPos(GLFWwindow* window, double xpos, double ypos);
|
||||
* @sa @ref glfwDestroyCursor
|
||||
* @sa @ref glfwCreateStandardCursor
|
||||
*
|
||||
* @since Added in version 3.1.
|
||||
* @since Added in version 3.1. Changed in 4.0 to add the count parameter.
|
||||
*
|
||||
* @ingroup input
|
||||
*/
|
||||
GLFWAPI GLFWcursor* glfwCreateCursor(const GLFWimage* image, int xhot, int yhot);
|
||||
GLFWAPI GLFWcursor* glfwCreateCursor(const GLFWimage* image, int xhot, int yhot, int count);
|
||||
|
||||
/*! @brief Creates a cursor with a standard shape.
|
||||
*
|
||||
|
||||
5
glfw/init.c
vendored
5
glfw/init.c
vendored
@@ -51,6 +51,7 @@ static GLFWerrorfun _glfwErrorCallback;
|
||||
static _GLFWinitconfig _glfwInitHints =
|
||||
{
|
||||
GLFW_TRUE, // hat buttons
|
||||
GLFW_FALSE, // debug keyboard
|
||||
{
|
||||
GLFW_TRUE, // macOS menu bar
|
||||
GLFW_TRUE // macOS bundle chdir
|
||||
@@ -255,6 +256,9 @@ GLFWAPI void glfwInitHint(int hint, int value)
|
||||
case GLFW_JOYSTICK_HAT_BUTTONS:
|
||||
_glfwInitHints.hatButtons = value;
|
||||
return;
|
||||
case GLFW_DEBUG_KEYBOARD:
|
||||
_glfwInitHints.debugKeyboard = value;
|
||||
return;
|
||||
case GLFW_COCOA_CHDIR_RESOURCES:
|
||||
_glfwInitHints.ns.chdir = value;
|
||||
return;
|
||||
@@ -311,4 +315,3 @@ GLFWAPI GLFWerrorfun glfwSetErrorCallback(GLFWerrorfun cbfun)
|
||||
_GLFW_SWAP_POINTERS(_glfwErrorCallback, cbfun);
|
||||
return cbfun;
|
||||
}
|
||||
|
||||
|
||||
5
glfw/input.c
vendored
5
glfw/input.c
vendored
@@ -799,11 +799,12 @@ GLFWAPI void glfwSetCursorPos(GLFWwindow* handle, double xpos, double ypos)
|
||||
}
|
||||
}
|
||||
|
||||
GLFWAPI GLFWcursor* glfwCreateCursor(const GLFWimage* image, int xhot, int yhot)
|
||||
GLFWAPI GLFWcursor* glfwCreateCursor(const GLFWimage* image, int xhot, int yhot, int count)
|
||||
{
|
||||
_GLFWcursor* cursor;
|
||||
|
||||
assert(image != NULL);
|
||||
assert(count > 0);
|
||||
|
||||
_GLFW_REQUIRE_INIT_OR_RETURN(NULL);
|
||||
|
||||
@@ -811,7 +812,7 @@ GLFWAPI GLFWcursor* glfwCreateCursor(const GLFWimage* image, int xhot, int yhot)
|
||||
cursor->next = _glfw.cursorListHead;
|
||||
_glfw.cursorListHead = cursor;
|
||||
|
||||
if (!_glfwPlatformCreateCursor(cursor, image, xhot, yhot))
|
||||
if (!_glfwPlatformCreateCursor(cursor, image, xhot, yhot, count))
|
||||
{
|
||||
glfwDestroyCursor((GLFWcursor*) cursor);
|
||||
return NULL;
|
||||
|
||||
3
glfw/internal.h
vendored
3
glfw/internal.h
vendored
@@ -242,6 +242,7 @@ struct _GLFWerror
|
||||
struct _GLFWinitconfig
|
||||
{
|
||||
GLFWbool hatButtons;
|
||||
GLFWbool debugKeyboard;
|
||||
struct {
|
||||
GLFWbool menubar;
|
||||
GLFWbool chdir;
|
||||
@@ -599,7 +600,7 @@ void _glfwPlatformGetCursorPos(_GLFWwindow* window, double* xpos, double* ypos);
|
||||
void _glfwPlatformSetCursorPos(_GLFWwindow* window, double xpos, double ypos);
|
||||
void _glfwPlatformSetCursorMode(_GLFWwindow* window, int mode);
|
||||
int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
|
||||
const GLFWimage* image, int xhot, int yhot);
|
||||
const GLFWimage* image, int xhot, int yhot, int count);
|
||||
int _glfwPlatformCreateStandardCursor(_GLFWcursor* cursor, int shape);
|
||||
void _glfwPlatformDestroyCursor(_GLFWcursor* cursor);
|
||||
void _glfwPlatformSetCursor(_GLFWwindow* window, _GLFWcursor* cursor);
|
||||
|
||||
2
glfw/null_window.c
vendored
2
glfw/null_window.c
vendored
@@ -267,7 +267,7 @@ void _glfwPlatformSetCursorMode(_GLFWwindow* window, int mode)
|
||||
|
||||
int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
|
||||
const GLFWimage* image,
|
||||
int xhot, int yhot)
|
||||
int xhot, int yhot, int count)
|
||||
{
|
||||
return GLFW_TRUE;
|
||||
}
|
||||
|
||||
2
glfw/win32_window.c
vendored
2
glfw/win32_window.c
vendored
@@ -1839,7 +1839,7 @@ int _glfwPlatformGetKeyScancode(int key)
|
||||
|
||||
int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
|
||||
const GLFWimage* image,
|
||||
int xhot, int yhot)
|
||||
int xhot, int yhot, int count)
|
||||
{
|
||||
cursor->win32.handle = (HCURSOR) createIcon(image, xhot, yhot, GLFW_FALSE);
|
||||
if (!cursor->win32.handle)
|
||||
|
||||
2
glfw/wl_init.c
vendored
2
glfw/wl_init.c
vendored
@@ -164,7 +164,7 @@ static void pointerHandleMotion(void* data,
|
||||
wl_fixed_t sy)
|
||||
{
|
||||
_GLFWwindow* window = _glfw.wl.pointerFocus;
|
||||
const char* cursorName;
|
||||
const char* cursorName = NULL;
|
||||
|
||||
if (!window)
|
||||
return;
|
||||
|
||||
6
glfw/wl_window.c
vendored
6
glfw/wl_window.c
vendored
@@ -1257,7 +1257,7 @@ int _glfwPlatformGetKeyScancode(int key)
|
||||
|
||||
int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
|
||||
const GLFWimage* image,
|
||||
int xhot, int yhot)
|
||||
int xhot, int yhot, int count)
|
||||
{
|
||||
cursor->wl.buffer = createShmBuffer(image);
|
||||
cursor->wl.width = image->width;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
6
glfw/x11_window.c
vendored
6
glfw/x11_window.c
vendored
@@ -2684,7 +2684,7 @@ int _glfwPlatformGetKeyScancode(int key)
|
||||
|
||||
int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
|
||||
const GLFWimage* image,
|
||||
int xhot, int yhot)
|
||||
int xhot, int yhot, int count)
|
||||
{
|
||||
cursor->x11.handle = _glfwCreateCursorX11(image, xhot, yhot);
|
||||
if (!cursor->x11.handle)
|
||||
@@ -2937,3 +2937,7 @@ GLFWAPI const char* glfwGetX11SelectionString(void)
|
||||
_GLFW_REQUIRE_INIT_OR_RETURN(NULL);
|
||||
return getSelectionString(_glfw.x11.PRIMARY);
|
||||
}
|
||||
|
||||
GLFWAPI int glfwGetXKBScancode(const char* keyName, GLFWbool caseSensitive) {
|
||||
return glfw_xkb_keysym_from_name(keyName, caseSensitive);
|
||||
}
|
||||
|
||||
19
glfw/xkb_glfw.c
vendored
19
glfw/xkb_glfw.c
vendored
@@ -30,8 +30,7 @@
|
||||
#include "internal.h"
|
||||
#include "xkb_glfw.h"
|
||||
|
||||
static GLFWbool debug_keyboard = GLFW_FALSE;
|
||||
#define debug(...) if (debug_keyboard) printf(__VA_ARGS__);
|
||||
#define debug(...) if (_glfw.hints.init.debugKeyboard) printf(__VA_ARGS__);
|
||||
|
||||
#define map_key(key) { \
|
||||
switch(key) { \
|
||||
@@ -206,7 +205,6 @@ glfw_xkb_release(_GLFWXKBData *xkb) {
|
||||
GLFWbool
|
||||
glfw_xkb_create_context(_GLFWXKBData *xkb) {
|
||||
xkb->context = xkb_context_new(0);
|
||||
debug_keyboard = getenv("GLFW_DEBUG_KEYBOARD") != NULL;
|
||||
if (!xkb->context)
|
||||
{
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR,
|
||||
@@ -361,6 +359,10 @@ glfw_xkb_keysym_name(xkb_keysym_t sym) {
|
||||
return name;
|
||||
}
|
||||
|
||||
int
|
||||
glfw_xkb_keysym_from_name(const char *name, GLFWbool case_sensitive) {
|
||||
return (int)xkb_keysym_from_name(name, case_sensitive ? XKB_KEYSYM_NO_FLAGS : XKB_KEYSYM_CASE_INSENSITIVE);
|
||||
}
|
||||
|
||||
static inline const char*
|
||||
format_mods(unsigned int mods) {
|
||||
@@ -445,15 +447,20 @@ glfw_xkb_handle_key_event(_GLFWwindow *window, _GLFWXKBData *xkb, xkb_keycode_t
|
||||
if (text[0]) { debug("%s: %s ", text_type, text); }
|
||||
}
|
||||
int glfw_keycode = glfw_key_for_sym(glfw_sym);
|
||||
GLFWbool is_fallback = GLFW_FALSE;
|
||||
if (glfw_keycode == GLFW_KEY_UNKNOWN && !text[0]) {
|
||||
int num_default_syms = xkb_state_key_get_syms(xkb->default_state, code_for_sym, &default_syms);
|
||||
if (num_default_syms > 0) {
|
||||
glfw_sym = default_syms[0];
|
||||
glfw_keycode = glfw_key_for_sym(glfw_sym);
|
||||
is_fallback = GLFW_TRUE;
|
||||
}
|
||||
debug("%sglfw_fallback_key: %s\n", format_mods(xkb->modifiers), _glfwGetKeyName(glfw_keycode));
|
||||
} else {
|
||||
debug("%sglfw_key: %s\n", format_mods(xkb->modifiers), _glfwGetKeyName(glfw_keycode));
|
||||
}
|
||||
debug(
|
||||
"%s%s: %s xkb_key_name: %s\n",
|
||||
format_mods(xkb->modifiers),
|
||||
is_fallback ? "glfw_fallback_key" : "glfw_key", _glfwGetKeyName(glfw_keycode),
|
||||
glfw_xkb_keysym_name(glfw_sym)
|
||||
);
|
||||
_glfwInputKeyboard(window, glfw_keycode, glfw_sym, action, xkb->modifiers, text, 0);
|
||||
}
|
||||
|
||||
1
glfw/xkb_glfw.h
vendored
1
glfw/xkb_glfw.h
vendored
@@ -83,3 +83,4 @@ GLFWbool glfw_xkb_should_repeat(_GLFWXKBData *xkb, xkb_keycode_t scancode);
|
||||
const char* glfw_xkb_keysym_name(xkb_keysym_t sym);
|
||||
xkb_keysym_t glfw_xkb_sym_for_key(int key);
|
||||
void glfw_xkb_handle_key_event(_GLFWwindow *window, _GLFWXKBData *xkb, xkb_keycode_t scancode, int action);
|
||||
int glfw_xkb_keysym_from_name(const char *name, GLFWbool case_sensitive);
|
||||
|
||||
@@ -1,129 +1 @@
|
||||
= Key encoding for extended keyboard protocol
|
||||
|
||||
See link:protocol-extensions.asciidoc#keyboard-handling[Keyboard Handling protocol extension]
|
||||
for more information and link:key_encoding.json[for this table in JSON format].
|
||||
|
||||
|===
|
||||
| Name | Encoded representation (base64)
|
||||
|
||||
| 0 | `G`
|
||||
| 1 | `H`
|
||||
| 2 | `I`
|
||||
| 3 | `J`
|
||||
| 4 | `K`
|
||||
| 5 | `L`
|
||||
| 6 | `M`
|
||||
| 7 | `N`
|
||||
| 8 | `O`
|
||||
| 9 | `P`
|
||||
| A | `S`
|
||||
| APOSTROPHE | `B`
|
||||
| B | `T`
|
||||
| BACKSLASH | `t`
|
||||
| BACKSPACE | `1`
|
||||
| C | `U`
|
||||
| CAPS LOCK | `:`
|
||||
| COMMA | `C`
|
||||
| D | `V`
|
||||
| DELETE | `3`
|
||||
| DOWN | `6`
|
||||
| E | `W`
|
||||
| END | `-`
|
||||
| ENTER | `z`
|
||||
| EQUAL | `R`
|
||||
| ESCAPE | `y`
|
||||
| F | `X`
|
||||
| F1 | `/`
|
||||
| F10 | `]`
|
||||
| F11 | `{`
|
||||
| F12 | `}`
|
||||
| F13 | `@`
|
||||
| F14 | `%`
|
||||
| F15 | `$`
|
||||
| F16 | `#`
|
||||
| F17 | `BA`
|
||||
| F18 | `BB`
|
||||
| F19 | `BC`
|
||||
| F2 | `*`
|
||||
| F20 | `BD`
|
||||
| F21 | `BE`
|
||||
| F22 | `BF`
|
||||
| F23 | `BG`
|
||||
| F24 | `BH`
|
||||
| F25 | `BI`
|
||||
| F3 | `?`
|
||||
| F4 | `&`
|
||||
| F5 | `<`
|
||||
| F6 | `>`
|
||||
| F7 | `(`
|
||||
| F8 | `)`
|
||||
| F9 | `[`
|
||||
| G | `Y`
|
||||
| GRAVE ACCENT | `v`
|
||||
| H | `Z`
|
||||
| HOME | `.`
|
||||
| I | `a`
|
||||
| INSERT | `2`
|
||||
| J | `b`
|
||||
| K | `c`
|
||||
| KP 0 | `BJ`
|
||||
| KP 1 | `BK`
|
||||
| KP 2 | `BL`
|
||||
| KP 3 | `BM`
|
||||
| KP 4 | `BN`
|
||||
| KP 5 | `BO`
|
||||
| KP 6 | `BP`
|
||||
| KP 7 | `BQ`
|
||||
| KP 8 | `BR`
|
||||
| KP 9 | `BS`
|
||||
| KP ADD | `BX`
|
||||
| KP DECIMAL | `BT`
|
||||
| KP DIVIDE | `BU`
|
||||
| KP ENTER | `BY`
|
||||
| KP EQUAL | `BZ`
|
||||
| KP MULTIPLY | `BV`
|
||||
| KP SUBTRACT | `BW`
|
||||
| L | `d`
|
||||
| LEFT | `5`
|
||||
| LEFT ALT | `Bc`
|
||||
| LEFT BRACKET | `s`
|
||||
| LEFT CONTROL | `Bb`
|
||||
| LEFT SHIFT | `Ba`
|
||||
| LEFT SUPER | `Bd`
|
||||
| M | `e`
|
||||
| MINUS | `D`
|
||||
| N | `f`
|
||||
| NUM LOCK | `=`
|
||||
| O | `g`
|
||||
| P | `h`
|
||||
| PAGE DOWN | `9`
|
||||
| PAGE UP | `8`
|
||||
| PAUSE | `!`
|
||||
| PERIOD | `E`
|
||||
| PRINT SCREEN | `^`
|
||||
| Q | `i`
|
||||
| R | `j`
|
||||
| RIGHT | `4`
|
||||
| RIGHT ALT | `Bg`
|
||||
| RIGHT BRACKET | `u`
|
||||
| RIGHT CONTROL | `Bf`
|
||||
| RIGHT SHIFT | `Be`
|
||||
| RIGHT SUPER | `Bh`
|
||||
| S | `k`
|
||||
| SCROLL LOCK | `+`
|
||||
| SEMICOLON | `Q`
|
||||
| SLASH | `F`
|
||||
| SPACE | `A`
|
||||
| T | `l`
|
||||
| TAB | `0`
|
||||
| U | `m`
|
||||
| UP | `7`
|
||||
| V | `n`
|
||||
| W | `o`
|
||||
| WORLD 1 | `w`
|
||||
| WORLD 2 | `x`
|
||||
| X | `p`
|
||||
| Y | `q`
|
||||
| Z | `r`
|
||||
|
||||
|===
|
||||
See https://sw.kovidgoyal.net/kitty/key-encoding.html
|
||||
|
||||
@@ -50,8 +50,10 @@ class HistoryCompleter:
|
||||
|
||||
def __enter__(self):
|
||||
if self.history_path:
|
||||
if os.path.exists(self.history_path):
|
||||
try:
|
||||
readline.read_history_file(self.history_path)
|
||||
except Exception:
|
||||
pass
|
||||
readline.set_completer(self.complete)
|
||||
return self
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
67
kittens/diff/search.py
Normal 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
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
@@ -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
128
kittens/tui/line_edit.py
Normal 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
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
55135
kittens/unicode_input/names.h
generated
55135
kittens/unicode_input/names.h
generated
File diff suppressed because one or more lines are too long
@@ -2,7 +2,6 @@
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from functools import partial
|
||||
from itertools import chain
|
||||
|
||||
from .fast_data_types import (
|
||||
@@ -10,6 +9,16 @@ from .fast_data_types import (
|
||||
)
|
||||
from .utils import load_shaders
|
||||
|
||||
try:
|
||||
from enum import IntFlag
|
||||
except Exception:
|
||||
from enum import IntEnum as IntFlag
|
||||
|
||||
|
||||
class BorderColor(IntFlag):
|
||||
# See the border vertex shader for how these flags become actual colors
|
||||
default_bg, active, inactive, window_bg, bell = ((1 << i) for i in range(5))
|
||||
|
||||
|
||||
def vertical_edge(os_window_id, tab_id, color, width, top, bottom, left):
|
||||
add_borders_rect(os_window_id, tab_id, left, top, left + width, bottom, color)
|
||||
@@ -19,15 +28,15 @@ def horizontal_edge(os_window_id, tab_id, color, height, left, right, top):
|
||||
add_borders_rect(os_window_id, tab_id, left, top, right, top + height, color)
|
||||
|
||||
|
||||
def edge(func, os_window_id, tab_id, color, sz, a, b):
|
||||
return partial(func, os_window_id, tab_id, color, sz, a, b)
|
||||
|
||||
|
||||
def border(os_window_id, tab_id, color, sz, left, top, right, bottom):
|
||||
horz = edge(horizontal_edge, os_window_id, tab_id, color, sz, left, right)
|
||||
horz(top), horz(bottom - sz) # top, bottom edges
|
||||
vert = edge(vertical_edge, os_window_id, tab_id, color, sz, top, bottom)
|
||||
vert(left), vert(right - sz) # left, right edges
|
||||
def draw_edges(os_window_id, tab_id, colors, width, geometry, base_width=0):
|
||||
left = geometry.left - (width + base_width)
|
||||
top = geometry.top - (width + base_width)
|
||||
right = geometry.right + (width + base_width)
|
||||
bottom = geometry.bottom + (width + base_width)
|
||||
horizontal_edge(os_window_id, tab_id, colors[1], width, left, right, top)
|
||||
horizontal_edge(os_window_id, tab_id, colors[3], width, left, right, geometry.bottom + base_width)
|
||||
vertical_edge(os_window_id, tab_id, colors[0], width, top, bottom, left)
|
||||
vertical_edge(os_window_id, tab_id, colors[2], width, top, bottom, geometry.right + base_width)
|
||||
|
||||
|
||||
def load_borders_program():
|
||||
@@ -52,28 +61,31 @@ class Borders:
|
||||
extra_blank_rects,
|
||||
draw_window_borders=True
|
||||
):
|
||||
add_borders_rect(self.os_window_id, self.tab_id, 0, 0, 0, 0, 1)
|
||||
add_borders_rect(self.os_window_id, self.tab_id, 0, 0, 0, 0, BorderColor.default_bg)
|
||||
for br in chain(current_layout.blank_rects, extra_blank_rects):
|
||||
add_borders_rect(self.os_window_id, self.tab_id, *br, 1)
|
||||
add_borders_rect(self.os_window_id, self.tab_id, *br, BorderColor.default_bg)
|
||||
bw, pw = self.border_width, self.padding_width
|
||||
fw = bw + pw
|
||||
if bw + pw <= 0:
|
||||
return
|
||||
draw_borders = bw > 0 and draw_window_borders and len(windows) > 1
|
||||
if draw_borders:
|
||||
border_data = current_layout.resolve_borders(windows, active_window)
|
||||
|
||||
if fw > 0:
|
||||
for w in windows:
|
||||
g = w.geometry
|
||||
if bw > 0 and draw_window_borders:
|
||||
# Draw the border rectangles
|
||||
color = 2 if w is active_window else (16 if w.needs_attention else 4)
|
||||
border(
|
||||
self.os_window_id, self.tab_id,
|
||||
color, bw, g.left - fw, g.top - fw, g.right + fw,
|
||||
g.bottom + fw
|
||||
)
|
||||
if pw > 0:
|
||||
# Draw the background rectangles over the padding region
|
||||
color = w.screen.color_profile.default_bg
|
||||
border(
|
||||
self.os_window_id, self.tab_id,
|
||||
(color << 8) | 8, pw, g.left - pw, g.top - pw, g.right + pw,
|
||||
g.bottom + pw
|
||||
)
|
||||
for i, w in enumerate(windows):
|
||||
g = w.geometry
|
||||
window_bg = w.screen.color_profile.default_bg
|
||||
window_bg = (window_bg << 8) | BorderColor.window_bg
|
||||
if draw_borders:
|
||||
# Draw the border rectangles
|
||||
if w is active_window:
|
||||
color = BorderColor.active
|
||||
else:
|
||||
color = BorderColor.bell if w.needs_attention else BorderColor.inactive
|
||||
colors = tuple(color if needed else window_bg for needed in next(border_data))
|
||||
draw_edges(
|
||||
self.os_window_id, self.tab_id, colors, bw, g, base_width=pw)
|
||||
if pw > 0:
|
||||
# Draw the background rectangles over the padding region
|
||||
colors = (window_bg, window_bg, window_bg, window_bg)
|
||||
draw_edges(
|
||||
self.os_window_id, self.tab_id, colors, pw, g)
|
||||
|
||||
@@ -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,
|
||||
@@ -25,6 +25,7 @@ from .fast_data_types import (
|
||||
set_clipboard_string, set_in_sequence_mode, toggle_fullscreen
|
||||
)
|
||||
from .keys import get_shortcut, shortcut_matches
|
||||
from .layout import set_draw_minimal_borders
|
||||
from .remote_control import handle_cmd
|
||||
from .rgb import Color, color_from_int
|
||||
from .session import create_session
|
||||
@@ -76,11 +77,13 @@ class DumpCommands: # {{{
|
||||
class Boss:
|
||||
|
||||
def __init__(self, os_window_id, opts, args, cached_values, new_os_window_trigger):
|
||||
set_draw_minimal_borders(opts)
|
||||
self.window_id_map = WeakValueDictionary()
|
||||
self.startup_colors = {k: opts[k] for k in opts if isinstance(opts[k], Color)}
|
||||
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 +98,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 +242,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 +316,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 +534,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 +563,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 +602,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 +649,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':
|
||||
|
||||
@@ -976,7 +976,12 @@ write_to_child(int fd, Screen *screen) {
|
||||
written = screen->write_buf_used;
|
||||
}
|
||||
}
|
||||
screen->write_buf_used -= written;
|
||||
if (written) {
|
||||
screen->write_buf_used -= written;
|
||||
if (screen->write_buf_used) {
|
||||
memmove(screen->write_buf, screen->write_buf + written, screen->write_buf_used);
|
||||
}
|
||||
}
|
||||
screen_mutex(unlock, write);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,13 @@ serialize_string_tuple(PyObject *src) {
|
||||
Py_ssize_t sz = PyTuple_GET_SIZE(src);
|
||||
char **ans = calloc(sz + 1, sizeof(char*));
|
||||
if (!ans) fatal("Out of memory");
|
||||
for (Py_ssize_t i = 0; i < sz; i++) ans[i] = PyUnicode_AsUTF8(PyTuple_GET_ITEM(src, i));
|
||||
for (Py_ssize_t i = 0; i < sz; i++) {
|
||||
const char *pysrc = PyUnicode_AsUTF8(PyTuple_GET_ITEM(src, i));
|
||||
size_t len = strlen(pysrc);
|
||||
ans[i] = calloc(len + 1, sizeof(char));
|
||||
if (ans[i] == NULL) fatal("Out of memory");
|
||||
memcpy(ans[i], pysrc, len);
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ class Child:
|
||||
if stdin is not None:
|
||||
os.close(stdin_read_fd)
|
||||
fast_data_types.thread_write(stdin_write_fd, stdin)
|
||||
fcntl.fcntl(self.child_fd, fcntl.F_SETFL, fcntl.fcntl(self.child_fd, fcntl.F_GETFL) | os.O_NONBLOCK)
|
||||
return pid
|
||||
|
||||
@property
|
||||
|
||||
32
kitty/cli.py
32
kitty/cli.py
@@ -7,9 +7,10 @@ import re
|
||||
import sys
|
||||
from collections import deque
|
||||
|
||||
from .config import defaults, load_config
|
||||
from .conf.utils import resolve_config
|
||||
from .config import defaults, load_config
|
||||
from .constants import appname, defconf, is_macos, is_wayland, str_version
|
||||
from .fast_data_types import GLFW_KEY_UNKNOWN, glfw_get_key_name
|
||||
|
||||
CONFIG_HELP = '''\
|
||||
Specify a path to the configuration file(s) to use. All configuration files are
|
||||
@@ -44,7 +45,7 @@ condition=not is_macos
|
||||
Set the name part of the :italic:`WM_CLASS` property (defaults to using the value from :option:`{appname} --class`)
|
||||
|
||||
|
||||
--title
|
||||
--title -T
|
||||
Set the window title. This will override any title set by the program running inside kitty. So
|
||||
only use this if you are running a program that does not set titles.
|
||||
|
||||
@@ -76,6 +77,12 @@ Path to a file containing the startup :italic:`session` (tabs, windows, layout,
|
||||
See the README file for details and an example.
|
||||
|
||||
|
||||
--hold
|
||||
type=bool-set
|
||||
Remain open after child process exits. Note that this only affects the first
|
||||
window. You can quit by either using the close window shortcut or :kbd:`Ctrl+d`.
|
||||
|
||||
|
||||
--single-instance -1
|
||||
type=bool-set
|
||||
If specified only a single instance of :italic:`{appname}` will run. New invocations will
|
||||
@@ -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)
|
||||
|
||||
@@ -435,7 +454,7 @@ def seq_as_rst(seq, usage, message, appname, heading_char='-'):
|
||||
if defval is not None:
|
||||
a(textwrap.indent('Default: :code:`{}`'.format(defval), ' ' * 4))
|
||||
if 'choices' in opt:
|
||||
a(textwrap.indent('Choices: :code:`{}`'.format(', '.join(opt['choices'])), ' ' * 4))
|
||||
a(textwrap.indent('Choices: :code:`{}`'.format(', '.join(sorted(opt['choices']))), ' ' * 4))
|
||||
a('')
|
||||
|
||||
text = '\n'.join(blocks)
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -202,14 +202,19 @@ cocoa_update_title(PyObject *pytitle) {
|
||||
}
|
||||
|
||||
bool
|
||||
cocoa_make_window_resizable(void *w) {
|
||||
cocoa_make_window_resizable(void *w, bool resizable) {
|
||||
NSWindow *window = (NSWindow*)w;
|
||||
|
||||
@try {
|
||||
[window setStyleMask:
|
||||
[window styleMask] | NSWindowStyleMaskResizable];
|
||||
if (resizable) {
|
||||
[window setStyleMask:
|
||||
[window styleMask] | NSWindowStyleMaskResizable];
|
||||
} else {
|
||||
[window setStyleMask:
|
||||
[window styleMask] & ~NSWindowStyleMaskResizable];
|
||||
}
|
||||
} @catch (NSException *e) {
|
||||
return PyErr_Format(PyExc_ValueError, "Failed to set style mask: %s: %s", [[e name] UTF8String], [[e reason] UTF8String]);
|
||||
log_error("Failed to set style mask: %s: %s", [[e name] UTF8String], [[e reason] UTF8String]);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -228,19 +228,28 @@ def parse_kittens_shortcut(sc):
|
||||
|
||||
|
||||
def parse_kittens_func_args(action, args_funcs):
|
||||
parts = action.split(' ', 1)
|
||||
parts = action.strip().split(' ', 1)
|
||||
func = parts[0]
|
||||
if len(parts) == 1:
|
||||
return func, ()
|
||||
rest = parts[1]
|
||||
parser = args_funcs.get(func)
|
||||
if parser is not None:
|
||||
try:
|
||||
func, args = parser(func, rest)
|
||||
except Exception:
|
||||
raise ValueError('Unknown key action: {}'.format(action))
|
||||
|
||||
try:
|
||||
parser = args_funcs[func]
|
||||
except KeyError as e:
|
||||
raise KeyError(
|
||||
'Unknown action: {}. Check if map action: '
|
||||
'{} is valid'.format(func, action)
|
||||
) from e
|
||||
|
||||
try:
|
||||
func, args = parser(func, rest)
|
||||
except Exception:
|
||||
raise ValueError('Unknown key action: {}'.format(action))
|
||||
|
||||
if not isinstance(args, (list, tuple)):
|
||||
args = (args,)
|
||||
|
||||
return func, tuple(args)
|
||||
|
||||
|
||||
|
||||
@@ -37,12 +37,14 @@ def parse_shortcut(sc):
|
||||
parts = sc.split('+')
|
||||
mods = parse_mods(parts[:-1], sc)
|
||||
if mods is None:
|
||||
return None, None
|
||||
return None, None, None
|
||||
key = parts[-1].upper()
|
||||
key = getattr(defines, 'GLFW_KEY_' + named_keys.get(key, key), None)
|
||||
if key is not None:
|
||||
return mods, key
|
||||
return mods, None
|
||||
is_native = False
|
||||
if key is None:
|
||||
key = defines.key_for_native_key_name(parts[-1])
|
||||
is_native = key is not None
|
||||
return mods, is_native, key
|
||||
|
||||
|
||||
KeyAction = namedtuple('KeyAction', 'func args')
|
||||
@@ -140,15 +142,15 @@ sequence_sep = '>'
|
||||
|
||||
class KeyDefinition:
|
||||
|
||||
def __init__(self, is_sequence, action, mods, key, rest=()):
|
||||
def __init__(self, is_sequence, action, mods, is_native, key, rest=()):
|
||||
self.is_sequence = is_sequence
|
||||
self.action = action
|
||||
self.trigger = mods, key
|
||||
self.trigger = mods, is_native, key
|
||||
self.rest = rest
|
||||
|
||||
def resolve(self, kitty_mod):
|
||||
self.trigger = defines.resolve_key_mods(kitty_mod, self.trigger[0]), self.trigger[1]
|
||||
self.rest = tuple((defines.resolve_key_mods(kitty_mod, mods), key) for mods, key in self.rest)
|
||||
self.trigger = defines.resolve_key_mods(kitty_mod, self.trigger[0]), self.trigger[1], self.trigger[2]
|
||||
self.rest = tuple((defines.resolve_key_mods(kitty_mod, mods), is_native, key) for mods, is_native, key in self.rest)
|
||||
|
||||
|
||||
def parse_key(val, key_definitions):
|
||||
@@ -161,18 +163,18 @@ def parse_key(val, key_definitions):
|
||||
trigger = None
|
||||
rest = []
|
||||
for part in sc.split(sequence_sep):
|
||||
mods, key = parse_shortcut(part)
|
||||
mods, is_native, key = parse_shortcut(part)
|
||||
if key is None:
|
||||
if mods is not None:
|
||||
log_error('Shortcut: {} has unknown key, ignoring'.format(sc))
|
||||
return
|
||||
if trigger is None:
|
||||
trigger = mods, key
|
||||
trigger = mods, is_native, key
|
||||
else:
|
||||
rest.append((mods, key))
|
||||
rest.append((mods, is_native, key))
|
||||
rest = tuple(rest)
|
||||
else:
|
||||
mods, key = parse_shortcut(sc)
|
||||
mods, is_native, key = parse_shortcut(sc)
|
||||
if key is None:
|
||||
if mods is not None:
|
||||
log_error('Shortcut: {} has unknown key, ignoring'.format(sc))
|
||||
@@ -186,9 +188,9 @@ def parse_key(val, key_definitions):
|
||||
if paction is not None:
|
||||
all_key_actions.add(paction.func)
|
||||
if is_sequence:
|
||||
key_definitions.append(KeyDefinition(True, paction, trigger[0], trigger[1], rest))
|
||||
key_definitions.append(KeyDefinition(True, paction, trigger[0], trigger[1], trigger[2], rest))
|
||||
else:
|
||||
key_definitions.append(KeyDefinition(False, paction, mods, key))
|
||||
key_definitions.append(KeyDefinition(False, paction, mods, is_native, key))
|
||||
|
||||
|
||||
def parse_symbol_map(val):
|
||||
|
||||
@@ -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.
|
||||
@@ -439,9 +449,20 @@ o('window_border_width', 1.0, option_type=positive_float, long_text=_('''
|
||||
The width (in pts) of window borders. Will be rounded to the nearest number of pixels based on screen resolution.
|
||||
Note that borders are displayed only when more than one window is visible. They are meant to separate multiple windows.'''))
|
||||
|
||||
o('draw_minimal_borders', True, long_text=_('''
|
||||
Draw only the minimum borders needed. This means that only the minimum
|
||||
needed borders for inactive windows are drawn. That is only the borders
|
||||
that separate the inactive window from a neighbor. Note that setting
|
||||
a non-zero window margin overrides this and causes all borders to be drawn.
|
||||
'''))
|
||||
|
||||
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 +637,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:
|
||||
@@ -682,7 +722,10 @@ kitty will stay running, even with no open windows, as is the expected
|
||||
behavior on macOS.
|
||||
'''))
|
||||
|
||||
|
||||
o('macos_window_resizable', True, long_text=_('''
|
||||
Disable this if you want kitty top-level (OS) windows to not be resizable
|
||||
on macOS.
|
||||
'''))
|
||||
# }}}
|
||||
|
||||
g('shortcuts') # {{{
|
||||
|
||||
@@ -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, 3)
|
||||
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']))
|
||||
@@ -42,7 +58,10 @@ def _get_config_dir():
|
||||
|
||||
candidate = os.path.abspath(os.path.expanduser(os.environ.get('XDG_CONFIG_HOME') or '~/.config'))
|
||||
ans = os.path.join(candidate, appname)
|
||||
os.makedirs(ans, exist_ok=True)
|
||||
try:
|
||||
os.makedirs(ans, exist_ok=True)
|
||||
except FileExistsError:
|
||||
raise SystemExit('A file {} already exists. It must be a directory, not a file.'.format(ans))
|
||||
return ans
|
||||
|
||||
|
||||
@@ -86,6 +105,7 @@ def wakeup():
|
||||
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
terminfo_dir = os.path.join(base_dir, 'terminfo')
|
||||
logo_data_file = os.path.join(base_dir, 'logo', 'kitty.rgba')
|
||||
beam_cursor_data_file = os.path.join(base_dir, 'logo', 'beam-cursor.png')
|
||||
try:
|
||||
shell_path = pwd.getpwuid(os.geteuid()).pw_shell or '/bin/sh'
|
||||
except KeyError:
|
||||
|
||||
@@ -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, ""},
|
||||
@@ -203,6 +211,7 @@ extern bool init_shaders(PyObject *module);
|
||||
extern bool init_mouse(PyObject *module);
|
||||
extern bool init_kittens(PyObject *module);
|
||||
extern bool init_logging(PyObject *module);
|
||||
extern bool init_png_reader(PyObject *module);
|
||||
#ifdef __APPLE__
|
||||
extern int init_CoreText(PyObject *);
|
||||
extern bool init_cocoa(PyObject *module);
|
||||
@@ -238,6 +247,7 @@ PyInit_fast_data_types(void) {
|
||||
if (!init_shaders(m)) return NULL;
|
||||
if (!init_mouse(m)) return NULL;
|
||||
if (!init_kittens(m)) return NULL;
|
||||
if (!init_png_reader(m)) return NULL;
|
||||
#ifdef __APPLE__
|
||||
if (!init_CoreText(m)) return NULL;
|
||||
if (!init_cocoa(m)) return NULL;
|
||||
|
||||
@@ -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
2
kitty/emoji.h
generated
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -175,7 +175,7 @@ face_from_descriptor(PyObject *descriptor, FONTS_DATA_HANDLE fg) {
|
||||
if (!missing_ok) { PyErr_SetString(PyExc_KeyError, "font descriptor is missing the key: " #key); return NULL; } \
|
||||
} else key = conv(t); \
|
||||
}
|
||||
char *path = NULL;
|
||||
const char *path = NULL;
|
||||
long index = 0;
|
||||
bool hinting = false;
|
||||
long hint_style = 0;
|
||||
|
||||
5656
kitty/gl-wrapper.c
generated
5656
kitty/gl-wrapper.c
generated
File diff suppressed because it is too large
Load Diff
14
kitty/gl-wrapper.h
generated
14
kitty/gl-wrapper.h
generated
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
OpenGL loader generated by glad 0.1.20a0 on Fri May 4 18:11:55 2018.
|
||||
OpenGL loader generated by glad 0.1.24a0 on Wed Jun 20 12:32:33 2018.
|
||||
Language/Generator: C/C++ Debug
|
||||
Specification: gl
|
||||
APIs: gl=3.3
|
||||
@@ -146,16 +146,8 @@ typedef unsigned int GLhandleARB;
|
||||
typedef unsigned short GLhalfARB;
|
||||
typedef unsigned short GLhalf;
|
||||
typedef GLint GLfixed;
|
||||
#if defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) && (__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ > 1060)
|
||||
typedef long GLintptr;
|
||||
#else
|
||||
typedef ptrdiff_t GLintptr;
|
||||
#endif
|
||||
#if defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) && (__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ > 1060)
|
||||
typedef long GLsizeiptr;
|
||||
#else
|
||||
typedef ptrdiff_t GLsizeiptr;
|
||||
#endif
|
||||
typedef khronos_intptr_t GLintptr;
|
||||
typedef khronos_ssize_t GLsizeiptr;
|
||||
typedef int64_t GLint64;
|
||||
typedef uint64_t GLuint64;
|
||||
#if defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) && (__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ > 1060)
|
||||
|
||||
2
kitty/glfw-wrapper.c
generated
2
kitty/glfw-wrapper.c
generated
@@ -371,6 +371,8 @@ load_glfw(const char* path) {
|
||||
|
||||
*(void **) (&glfwGetX11SelectionString_impl) = dlsym(handle, "glfwGetX11SelectionString");
|
||||
|
||||
*(void **) (&glfwGetXKBScancode_impl) = dlsym(handle, "glfwGetXKBScancode");
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
7
kitty/glfw-wrapper.h
generated
7
kitty/glfw-wrapper.h
generated
@@ -818,6 +818,7 @@ typedef int (* GLFWapplicationshouldhandlereopenfun)(int);
|
||||
/*! @addtogroup init
|
||||
* @{ */
|
||||
#define GLFW_JOYSTICK_HAT_BUTTONS 0x00050001
|
||||
#define GLFW_DEBUG_KEYBOARD 0x00050002
|
||||
|
||||
#define GLFW_COCOA_CHDIR_RESOURCES 0x00051001
|
||||
#define GLFW_COCOA_MENUBAR 0x00051002
|
||||
@@ -1678,7 +1679,7 @@ typedef void (*glfwSetCursorPos_func)(GLFWwindow*, double, double);
|
||||
glfwSetCursorPos_func glfwSetCursorPos_impl;
|
||||
#define glfwSetCursorPos glfwSetCursorPos_impl
|
||||
|
||||
typedef GLFWcursor* (*glfwCreateCursor_func)(const GLFWimage*, int, int);
|
||||
typedef GLFWcursor* (*glfwCreateCursor_func)(const GLFWimage*, int, int, int);
|
||||
glfwCreateCursor_func glfwCreateCursor_impl;
|
||||
#define glfwCreateCursor glfwCreateCursor_impl
|
||||
|
||||
@@ -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);
|
||||
|
||||
60
kitty/glfw.c
60
kitty/glfw.c
@@ -8,7 +8,7 @@
|
||||
#include "fonts.h"
|
||||
#include <structmember.h>
|
||||
#include "glfw-wrapper.h"
|
||||
extern bool cocoa_make_window_resizable(void *w);
|
||||
extern bool cocoa_make_window_resizable(void *w, bool);
|
||||
extern void cocoa_create_global_menu(void);
|
||||
extern void cocoa_set_hide_from_tasks(void);
|
||||
extern void cocoa_set_titlebar_color(void *w, color_type color);
|
||||
@@ -202,6 +202,7 @@ push_focus_history(OSWindow *w) {
|
||||
|
||||
static void
|
||||
window_focus_callback(GLFWwindow *w, int focused) {
|
||||
global_state.active_drag_in_window = 0;
|
||||
if (!set_callback_window(w)) return;
|
||||
global_state.callback_os_window->is_focused = focused ? true : false;
|
||||
if (focused) {
|
||||
@@ -455,8 +456,10 @@ create_os_window(PyObject UNUSED *self, PyObject *args) {
|
||||
if (OPT(macos_hide_from_tasks)) cocoa_set_hide_from_tasks();
|
||||
#endif
|
||||
#define CC(dest, shape) {\
|
||||
dest##_cursor = glfwCreateStandardCursor(GLFW_##shape##_CURSOR); \
|
||||
if (dest##_cursor == NULL) { log_error("Failed to create the %s mouse cursor, using default cursor.", #shape); }}
|
||||
if (!dest##_cursor) { \
|
||||
dest##_cursor = glfwCreateStandardCursor(GLFW_##shape##_CURSOR); \
|
||||
if (dest##_cursor == NULL) { log_error("Failed to create the %s mouse cursor, using default cursor.", #shape); } \
|
||||
}}
|
||||
CC(standard, IBEAM); CC(click, HAND); CC(arrow, ARROW);
|
||||
#undef CC
|
||||
is_first_window = false;
|
||||
@@ -492,10 +495,8 @@ create_os_window(PyObject UNUSED *self, PyObject *args) {
|
||||
glfwSetWindowFocusCallback(glfw_window, window_focus_callback);
|
||||
glfwSetDropCallback(glfw_window, drop_callback);
|
||||
#ifdef __APPLE__
|
||||
if (OPT(macos_hide_titlebar)) {
|
||||
if (glfwGetCocoaWindow) { if (!cocoa_make_window_resizable(glfwGetCocoaWindow(glfw_window))) { PyErr_Print(); } }
|
||||
else log_error("Failed to load glfwGetCocoaWindow");
|
||||
}
|
||||
if (glfwGetCocoaWindow) cocoa_make_window_resizable(glfwGetCocoaWindow(glfw_window), OPT(macos_window_resizable));
|
||||
else log_error("Failed to load glfwGetCocoaWindow");
|
||||
#endif
|
||||
double now = monotonic();
|
||||
w->is_focused = true;
|
||||
@@ -515,6 +516,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 +559,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);
|
||||
@@ -673,6 +679,9 @@ toggle_fullscreen(PYNOARG) {
|
||||
const GLFWvidmode* mode = glfwGetVideoMode(monitor);
|
||||
if (w->before_fullscreen.is_set) glfwSetWindowMonitor(w->handle, NULL, w->before_fullscreen.x, w->before_fullscreen.y, w->before_fullscreen.w, w->before_fullscreen.h, mode->refreshRate);
|
||||
else glfwSetWindowMonitor(w->handle, NULL, 0, 0, 600, 400, mode->refreshRate);
|
||||
#ifdef __APPLE__
|
||||
if (glfwGetCocoaWindow) cocoa_make_window_resizable(glfwGetCocoaWindow(w->handle), OPT(macos_window_resizable));
|
||||
#endif
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
}
|
||||
@@ -835,6 +844,39 @@ set_smallest_allowed_resize(PyObject *self UNUSED, PyObject *args) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
set_custom_cursor(PyObject *self UNUSED, PyObject *args) {
|
||||
int shape;
|
||||
int x=0, y=0;
|
||||
Py_ssize_t sz;
|
||||
PyObject *images;
|
||||
if (!PyArg_ParseTuple(args, "iO!|ii", &shape, &PyTuple_Type, &images, &x, &y)) return NULL;
|
||||
static GLFWimage gimages[16] = {{0}};
|
||||
size_t count = MIN((size_t)PyTuple_GET_SIZE(images), arraysz(gimages));
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
if (!PyArg_ParseTuple(PyTuple_GET_ITEM(images, i), "s#ii", &gimages[i].pixels, &sz, &gimages[i].width, &gimages[i].height)) return NULL;
|
||||
if (gimages[i].width * gimages[i].height * 4 != sz) {
|
||||
PyErr_SetString(PyExc_ValueError, "The image data size does not match its width and height");
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
#define CASE(which, dest) {\
|
||||
case which: \
|
||||
standard_cursor = glfwCreateCursor(gimages, x, y, count); \
|
||||
if (standard_cursor == NULL) { PyErr_SetString(PyExc_ValueError, "Failed to create custom cursor"); return NULL; } \
|
||||
break; \
|
||||
}
|
||||
switch(shape) {
|
||||
CASE(GLFW_IBEAM_CURSOR, standard_cursor);
|
||||
CASE(GLFW_HAND_CURSOR, click_cursor);
|
||||
CASE(GLFW_ARROW_CURSOR, arrow_cursor);
|
||||
default:
|
||||
PyErr_SetString(PyExc_ValueError, "Unknown cursor shape");
|
||||
return NULL;
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
#ifdef __APPLE__
|
||||
void
|
||||
get_cocoa_key_equivalent(int key, int mods, unsigned short *cocoa_key, int *cocoa_mods) {
|
||||
@@ -844,6 +886,7 @@ get_cocoa_key_equivalent(int key, int mods, unsigned short *cocoa_key, int *coco
|
||||
// Boilerplate {{{
|
||||
|
||||
static PyMethodDef module_methods[] = {
|
||||
METHODB(set_custom_cursor, METH_VARARGS),
|
||||
METHODB(set_smallest_allowed_resize, METH_VARARGS),
|
||||
METHODB(create_os_window, METH_VARARGS),
|
||||
METHODB(set_default_window_icon, METH_VARARGS),
|
||||
@@ -887,6 +930,7 @@ init_glfw(PyObject *m) {
|
||||
ADDC(GLFW_PRESS);
|
||||
ADDC(GLFW_REPEAT);
|
||||
ADDC(GLFW_TRUE); ADDC(GLFW_FALSE);
|
||||
ADDC(GLFW_IBEAM_CURSOR); ADDC(GLFW_HAND_CURSOR); ADDC(GLFW_ARROW_CURSOR);
|
||||
|
||||
// --- Keys --------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <zlib.h>
|
||||
#include <png.h>
|
||||
#include <structmember.h>
|
||||
#include "png-reader.h"
|
||||
PyTypeObject GraphicsManager_Type;
|
||||
|
||||
#define STORAGE_LIMIT (320 * (1024 * 1024))
|
||||
@@ -227,87 +227,14 @@ err:
|
||||
return ok;
|
||||
}
|
||||
|
||||
struct fake_file { uint8_t *buf; size_t sz, cur; };
|
||||
|
||||
static void
|
||||
read_png_from_buffer(png_structp png, png_bytep out, png_size_t length) {
|
||||
struct fake_file *f = png_get_io_ptr(png);
|
||||
if (f) {
|
||||
size_t amt = MIN(length, f->sz - f->cur);
|
||||
memcpy(out, f->buf + f->cur, amt);
|
||||
f->cur += amt;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
read_png_error_handler(png_structp png_ptr, png_const_charp msg) {
|
||||
jmp_buf *jb;
|
||||
set_add_response("EBADPNG", msg);
|
||||
jb = png_get_error_ptr(png_ptr);
|
||||
if (jb == NULL) fatal("read_png_error_handler: could not retrieve jmp_buf");
|
||||
longjmp(*jb, 1);
|
||||
}
|
||||
|
||||
static void
|
||||
read_png_warn_handler(png_structp UNUSED png_ptr, png_const_charp UNUSED msg) {
|
||||
// ignore warnings
|
||||
}
|
||||
|
||||
struct png_jmp_data { uint8_t *decompressed; bool ok; png_bytep *row_pointers; int width, height; size_t sz; };
|
||||
|
||||
static void
|
||||
inflate_png_inner(struct png_jmp_data *d, uint8_t *buf, size_t bufsz) {
|
||||
struct fake_file f = {.buf = buf, .sz = bufsz};
|
||||
png_structp png = NULL;
|
||||
png_infop info = NULL;
|
||||
jmp_buf jb;
|
||||
png = png_create_read_struct(PNG_LIBPNG_VER_STRING, &jb, read_png_error_handler, read_png_warn_handler);
|
||||
if (!png) ABRT(ENOMEM, "Failed to create PNG read structure");
|
||||
info = png_create_info_struct(png);
|
||||
if (!info) ABRT(ENOMEM, "Failed to create PNG info structure");
|
||||
|
||||
if (setjmp(jb)) goto err;
|
||||
|
||||
png_set_read_fn(png, &f, read_png_from_buffer);
|
||||
png_read_info(png, info);
|
||||
png_byte color_type, bit_depth;
|
||||
d->width = png_get_image_width(png, info);
|
||||
d->height = png_get_image_height(png, info);
|
||||
color_type = png_get_color_type(png, info);
|
||||
bit_depth = png_get_bit_depth(png, info);
|
||||
|
||||
// Ensure we get RGBA data out of libpng
|
||||
if (bit_depth == 16) png_set_strip_16(png);
|
||||
if (color_type == PNG_COLOR_TYPE_PALETTE) png_set_palette_to_rgb(png);
|
||||
// PNG_COLOR_TYPE_GRAY_ALPHA is always 8 or 16bit depth.
|
||||
if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) png_set_expand_gray_1_2_4_to_8(png);
|
||||
|
||||
if (png_get_valid(png, info, PNG_INFO_tRNS)) png_set_tRNS_to_alpha(png);
|
||||
|
||||
// These color_type don't have an alpha channel then fill it with 0xff.
|
||||
if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_PALETTE) png_set_filler(png, 0xFF, PNG_FILLER_AFTER);
|
||||
|
||||
if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) png_set_gray_to_rgb(png);
|
||||
png_read_update_info(png, info);
|
||||
|
||||
int rowbytes = png_get_rowbytes(png, info);
|
||||
d->sz = rowbytes * d->height * sizeof(png_byte);
|
||||
d->decompressed = malloc(d->sz + 16);
|
||||
if (d->decompressed == NULL) ABRT(ENOMEM, "Out of memory allocating decompression buffer for PNG");
|
||||
d->row_pointers = malloc(d->height * sizeof(png_bytep));
|
||||
if (d->row_pointers == NULL) ABRT(ENOMEM, "Out of memory allocating row_pointers buffer for PNG");
|
||||
for (int i = 0; i < d->height; i++) d->row_pointers[i] = d->decompressed + i * rowbytes;
|
||||
png_read_image(png, d->row_pointers);
|
||||
|
||||
d->ok = true;
|
||||
err:
|
||||
if (png) png_destroy_read_struct(&png, info ? &info : NULL, NULL);
|
||||
return;
|
||||
png_error_handler(const char *code, const char *msg) {
|
||||
set_add_response(code, "%s", msg);
|
||||
}
|
||||
|
||||
static inline bool
|
||||
inflate_png(GraphicsManager UNUSED *self, Image *img, uint8_t *buf, size_t bufsz) {
|
||||
struct png_jmp_data d = {0};
|
||||
png_read_data d = {.err_handler=png_error_handler};
|
||||
inflate_png_inner(&d, buf, bufsz);
|
||||
if (d.ok) {
|
||||
free_load_data(&img->load_data);
|
||||
|
||||
76
kitty/keys.c
76
kitty/keys.c
@@ -26,12 +26,26 @@ key_to_bytes(int glfw_key, bool smkx, bool extended, int mods, int action) {
|
||||
#define SPECIAL_INDEX(key) ((key & 0x7f) | ( (mods & 0xF) << 7))
|
||||
#define IS_ALT_MODS(mods) (mods == GLFW_MOD_ALT || mods == (GLFW_MOD_ALT | GLFW_MOD_SHIFT))
|
||||
|
||||
typedef struct { int mods, scancode; } NativeKey;
|
||||
static NativeKey *native_special_keys = NULL;
|
||||
static size_t native_special_keys_capacity = 0, native_special_keys_count = 0;
|
||||
|
||||
void
|
||||
set_special_key_combo(int glfw_key, int mods) {
|
||||
uint16_t key = key_map[glfw_key];
|
||||
if (key != UINT8_MAX) {
|
||||
key = SPECIAL_INDEX(key);
|
||||
needs_special_handling[key] = true;
|
||||
set_special_key_combo(int glfw_key, int mods, bool is_native) {
|
||||
if (is_native) {
|
||||
if (native_special_keys_count >= native_special_keys_capacity) {
|
||||
native_special_keys_capacity = MAX(128, 2 * native_special_keys_capacity);
|
||||
native_special_keys = realloc(native_special_keys, sizeof(native_special_keys[0]) * native_special_keys_capacity);
|
||||
if (native_special_keys == NULL) fatal("Out of memory");
|
||||
}
|
||||
native_special_keys[native_special_keys_count].mods = mods;
|
||||
native_special_keys[native_special_keys_count++].scancode = glfw_key;
|
||||
} else {
|
||||
uint16_t key = key_map[glfw_key];
|
||||
if (key != UINT8_MAX) {
|
||||
key = SPECIAL_INDEX(key);
|
||||
needs_special_handling[key] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +95,24 @@ is_ascii_control_char(char c) {
|
||||
return c == 0 || (1 <= c && c <= 31) || c == 127;
|
||||
}
|
||||
|
||||
static inline bool
|
||||
check_if_special(int key, int mods, int scancode) {
|
||||
uint16_t qkey = (0 <= key && key < (ssize_t)arraysz(key_map)) ? key_map[key] : UINT8_MAX;
|
||||
bool special = false;
|
||||
if (qkey != UINT8_MAX) {
|
||||
qkey = SPECIAL_INDEX(qkey);
|
||||
special = needs_special_handling[qkey];
|
||||
}
|
||||
#ifdef __APPLE__
|
||||
(void)scancode;
|
||||
#else
|
||||
for (size_t i = 0; !special && i < native_special_keys_count; i++) {
|
||||
if (scancode == native_special_keys[i].scancode && mods == native_special_keys[i].mods) special = true;
|
||||
}
|
||||
#endif
|
||||
return special;
|
||||
}
|
||||
|
||||
void
|
||||
on_key_input(int key, int scancode, int action, int mods, const char* text, int state UNUSED) {
|
||||
Window *w = active_window();
|
||||
@@ -95,14 +127,7 @@ on_key_input(int key, int scancode, int action, int mods, const char* text, int
|
||||
Screen *screen = w->render_data.screen;
|
||||
bool has_text = text && !is_ascii_control_char(text[0]);
|
||||
if (action == GLFW_PRESS || action == GLFW_REPEAT) {
|
||||
uint16_t qkey = (0 <= key && key < (ssize_t)arraysz(key_map)) ? key_map[key] : UINT8_MAX;
|
||||
bool special = false;
|
||||
if (qkey != UINT8_MAX) {
|
||||
qkey = SPECIAL_INDEX(qkey);
|
||||
special = needs_special_handling[qkey];
|
||||
}
|
||||
/* printf("key: %s mods: %d special: %d\n", key_name(lkey), mods, special); */
|
||||
if (special) {
|
||||
if (check_if_special(key, mods, scancode)) {
|
||||
PyObject *ret = PyObject_CallMethod(global_state.boss, "dispatch_special_key", "iiii", key, scancode, action, mods);
|
||||
if (ret == NULL) { PyErr_Print(); }
|
||||
else {
|
||||
@@ -149,13 +174,38 @@ PYWRAP1(key_to_bytes) {
|
||||
return Py_BuildValue("y#", ans + 1, *ans);
|
||||
}
|
||||
|
||||
PYWRAP1(key_for_native_key_name) {
|
||||
const char *name;
|
||||
int case_sensitive = 0;
|
||||
PA("s|p", &name, case_sensitive);
|
||||
#ifdef __APPLE__
|
||||
Py_RETURN_NONE;
|
||||
#else
|
||||
if (glfwGetXKBScancode) { // if this function is called before GLFW is initialized glfwGetXKBScancode will be NULL
|
||||
int scancode = glfwGetXKBScancode(name, case_sensitive);
|
||||
if (scancode) return Py_BuildValue("i", scancode);
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
#endif
|
||||
}
|
||||
|
||||
static PyMethodDef module_methods[] = {
|
||||
M(key_to_bytes, METH_VARARGS),
|
||||
M(key_for_native_key_name, METH_VARARGS),
|
||||
{0}
|
||||
};
|
||||
|
||||
void
|
||||
finalize(void) {
|
||||
free(native_special_keys);
|
||||
}
|
||||
|
||||
bool
|
||||
init_keys(PyObject *module) {
|
||||
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
|
||||
if (Py_AtExit(finalize) != 0) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Failed to register the keys at exit handler");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ from . import fast_data_types as defines
|
||||
from .key_encoding import KEY_MAP
|
||||
from .terminfo import key_as_bytes
|
||||
from .utils import base64_encode
|
||||
from .constants import is_macos
|
||||
|
||||
|
||||
def modify_key_bytes(keybytes, amt):
|
||||
@@ -271,11 +272,17 @@ def interpret_key_event(key, scancode, mods, window, action):
|
||||
|
||||
|
||||
def get_shortcut(keymap, mods, key, scancode):
|
||||
return keymap.get((mods & 0b1111, key))
|
||||
mods &= 0b1111
|
||||
ans = keymap.get((mods, False, key))
|
||||
if ans is None and not is_macos:
|
||||
ans = keymap.get((mods, True, scancode))
|
||||
return ans
|
||||
|
||||
|
||||
def shortcut_matches(s, mods, key, scancode):
|
||||
return s[0] & 0b1111 == mods & 0b1111 and s[1] == key
|
||||
mods &= 0b1111
|
||||
q = scancode if s[1] else key
|
||||
return s[0] & 0b1111 == mods & 0b1111 and s[2] == q
|
||||
|
||||
|
||||
def generate_key_table():
|
||||
|
||||
@@ -7,6 +7,93 @@
|
||||
|
||||
#include "data-types.h"
|
||||
|
||||
#define CMD_BUF_SZ 2048
|
||||
|
||||
|
||||
static inline bool
|
||||
append_buf(char buf[CMD_BUF_SZ], size_t *pos, PyObject *ans) {
|
||||
if (*pos) {
|
||||
PyObject *bytes = PyBytes_FromStringAndSize(buf, *pos);
|
||||
if (!bytes) { PyErr_NoMemory(); return false; }
|
||||
int ret = PyList_Append(ans, bytes);
|
||||
Py_CLEAR(bytes);
|
||||
if (ret != 0) return false;
|
||||
*pos = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline bool
|
||||
add_char(char buf[CMD_BUF_SZ], size_t *pos, char ch, PyObject *ans) {
|
||||
if (*pos >= CMD_BUF_SZ) {
|
||||
if (!append_buf(buf, pos, ans)) return false;
|
||||
}
|
||||
buf[*pos] = ch;
|
||||
*pos += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline bool
|
||||
read_response(int fd, double timeout, PyObject *ans) {
|
||||
static char buf[CMD_BUF_SZ];
|
||||
size_t pos = 0;
|
||||
enum ReadState {START, STARTING_ESC, P, AT, K, I, T, T2, Y, HYPHEN, C, M, BODY, TRAILING_ESC};
|
||||
enum ReadState state = START;
|
||||
char ch;
|
||||
double end_time = monotonic() + timeout;
|
||||
while(monotonic() <= end_time) {
|
||||
ssize_t len = read(fd, &ch, 1);
|
||||
if (len == 0) continue;
|
||||
if (len < 0) {
|
||||
if (errno == EINTR || errno == EAGAIN) continue;
|
||||
PyErr_SetFromErrno(PyExc_OSError);
|
||||
return false;
|
||||
}
|
||||
end_time = monotonic() + timeout;
|
||||
switch(state) {
|
||||
case START:
|
||||
if (ch == 0x1b) state = STARTING_ESC;
|
||||
break;
|
||||
#define CASE(curr, q, next) case curr: state = ch == q ? next : START; break;
|
||||
CASE(STARTING_ESC, 'P', P);
|
||||
CASE(P, '@', AT);
|
||||
CASE(AT, 'k', K);
|
||||
CASE(K, 'i', I);
|
||||
CASE(I, 't', T);
|
||||
CASE(T, 't', T2);
|
||||
CASE(T2, 'y', Y);
|
||||
CASE(Y, '-', HYPHEN);
|
||||
CASE(HYPHEN, 'c', C);
|
||||
CASE(C, 'm', M);
|
||||
CASE(M, 'd', BODY);
|
||||
case BODY:
|
||||
if (ch == 0x1b) { state = TRAILING_ESC; }
|
||||
else {
|
||||
if (!add_char(buf, &pos, ch, ans)) return false;
|
||||
}
|
||||
break;
|
||||
case TRAILING_ESC:
|
||||
if (ch == '\\') return append_buf(buf, &pos, ans);
|
||||
if (!add_char(buf, &pos, 0x1b, ans)) return false;
|
||||
if (!add_char(buf, &pos, ch, ans)) return false;
|
||||
state = BODY;
|
||||
break;
|
||||
}
|
||||
}
|
||||
PyErr_SetString(PyExc_TimeoutError, "Timed out while waiting to read cmd response");
|
||||
return false;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
read_command_response(PyObject *self UNUSED, PyObject *args) {
|
||||
double timeout;
|
||||
int fd;
|
||||
PyObject *ans;
|
||||
if (!PyArg_ParseTuple(args, "idO!", &fd, &timeout, &PyList_Type, &ans)) return NULL;
|
||||
if (!read_response(fd, timeout, ans)) return NULL;
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
parse_input_from_terminal(PyObject *self UNUSED, PyObject *args) {
|
||||
enum State { NORMAL, ESC, CSI, ST, ESC_ST };
|
||||
@@ -105,6 +192,7 @@ parse_input_from_terminal(PyObject *self UNUSED, PyObject *args) {
|
||||
|
||||
static PyMethodDef module_methods[] = {
|
||||
METHODB(parse_input_from_terminal, METH_VARARGS),
|
||||
METHODB(read_command_response, METH_VARARGS),
|
||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
|
||||
188
kitty/layout.py
188
kitty/layout.py
@@ -14,6 +14,9 @@ from .fast_data_types import (
|
||||
# Utils {{{
|
||||
central = Region((0, 0, 199, 199, 200, 200))
|
||||
cell_width = cell_height = 20
|
||||
all_borders = True, True, True, True
|
||||
no_borders = False, False, False, False
|
||||
draw_minimal_borders = False
|
||||
|
||||
|
||||
def idx_for_id(win_id, windows):
|
||||
@@ -22,26 +25,30 @@ def idx_for_id(win_id, windows):
|
||||
return i
|
||||
|
||||
|
||||
def layout_dimension(start_at, length, cell_length, number_of_windows=1, border_length=0, margin_length=0, padding_length=0, left_align=False, bias=None):
|
||||
def set_draw_minimal_borders(opts):
|
||||
global draw_minimal_borders
|
||||
draw_minimal_borders = opts.draw_minimal_borders and opts.window_margin_width == 0
|
||||
|
||||
|
||||
def layout_dimension(start_at, length, cell_length, decoration_pairs, left_align=False, bias=None):
|
||||
number_of_windows = len(decoration_pairs)
|
||||
number_of_cells = length // cell_length
|
||||
border_length += padding_length
|
||||
space_needed_for_border = number_of_windows * 2 * border_length
|
||||
space_needed_for_padding = number_of_windows * 2 * margin_length
|
||||
space_needed = space_needed_for_padding + space_needed_for_border
|
||||
space_needed_for_decorations = sum(map(sum, decoration_pairs))
|
||||
extra = length - number_of_cells * cell_length
|
||||
while extra < space_needed:
|
||||
while extra < space_needed_for_decorations:
|
||||
number_of_cells -= 1
|
||||
extra = length - number_of_cells * cell_length
|
||||
cells_per_window = number_of_cells // number_of_windows
|
||||
extra -= space_needed
|
||||
extra -= space_needed_for_decorations
|
||||
pos = start_at
|
||||
if not left_align:
|
||||
pos += extra // 2
|
||||
pos += border_length + margin_length
|
||||
|
||||
def calc_window_length(cells_in_window):
|
||||
def calc_window_geom(i, cells_in_window):
|
||||
nonlocal pos
|
||||
pos += decoration_pairs[i][0]
|
||||
inner_length = cells_in_window * cell_length
|
||||
return 2 * (border_length + margin_length) + inner_length
|
||||
return inner_length + decoration_pairs[i][1]
|
||||
|
||||
if bias is not None and number_of_windows > 1 and len(bias) == number_of_windows and cells_per_window > 5:
|
||||
cells_map = [int(b * number_of_cells) for b in bias]
|
||||
@@ -51,21 +58,15 @@ def layout_dimension(start_at, length, cell_length, number_of_windows=1, border_
|
||||
break
|
||||
cells_map[mini] += 1
|
||||
cells_map[maxi] -= 1
|
||||
else:
|
||||
cells_map = list(repeat(cells_per_window, number_of_windows))
|
||||
|
||||
extra = number_of_cells - sum(cells_map)
|
||||
if extra:
|
||||
cells_map[-1] += extra
|
||||
for cells_per_window in cells_map:
|
||||
window_length = calc_window_length(cells_per_window)
|
||||
yield pos, cells_per_window
|
||||
pos += window_length
|
||||
return
|
||||
|
||||
window_length = calc_window_length(cells_per_window)
|
||||
extra = number_of_cells - (cells_per_window * number_of_windows)
|
||||
while number_of_windows > 0:
|
||||
number_of_windows -= 1
|
||||
yield pos, cells_per_window + (extra if number_of_windows == 0 else 0)
|
||||
extra = number_of_cells - sum(cells_map)
|
||||
if extra > 0:
|
||||
cells_map[-1] += extra
|
||||
for i, cells_per_window in enumerate(cells_map):
|
||||
window_length = calc_window_geom(i, cells_per_window)
|
||||
yield pos, cells_per_window
|
||||
pos += window_length
|
||||
|
||||
|
||||
@@ -83,9 +84,9 @@ def window_geometry(xstart, xnum, ystart, ynum):
|
||||
return WindowGeometry(left=xstart, top=ystart, xnum=xnum, ynum=ynum, right=xstart + cell_width * xnum, bottom=ystart + cell_height * ynum)
|
||||
|
||||
|
||||
def layout_single_window(margin_length, padding_length):
|
||||
xstart, xnum = next(layout_dimension(central.left, central.width, cell_width, margin_length=margin_length, padding_length=padding_length))
|
||||
ystart, ynum = next(layout_dimension(central.top, central.height, cell_height, margin_length=margin_length, padding_length=padding_length))
|
||||
def layout_single_window(xdecoration_pairs, ydecoration_pairs):
|
||||
xstart, xnum = next(layout_dimension(central.left, central.width, cell_width, xdecoration_pairs))
|
||||
ystart, ynum = next(layout_dimension(central.top, central.height, cell_height, ydecoration_pairs))
|
||||
return window_geometry(xstart, xnum, ystart, ynum)
|
||||
|
||||
|
||||
@@ -158,13 +159,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,19 +338,21 @@ 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
|
||||
decoration_pairs = ((self.padding_width + mw, self.padding_width + mw),)
|
||||
wg = layout_single_window(decoration_pairs, decoration_pairs)
|
||||
w.set_geometry(0, wg)
|
||||
self.blank_rects = blank_rects_for_window(w)
|
||||
|
||||
def xlayout(self, num, bias=None):
|
||||
return layout_dimension(
|
||||
central.left, central.width, cell_width, num, self.border_width,
|
||||
margin_length=self.margin_width, padding_length=self.padding_width, bias=bias)
|
||||
decoration = self.margin_width + self.border_width + self.padding_width
|
||||
decoration_pairs = tuple(repeat((decoration, decoration), num))
|
||||
return layout_dimension(central.left, central.width, cell_width, decoration_pairs, bias=bias)
|
||||
|
||||
def ylayout(self, num, left_align=True, bias=None):
|
||||
return layout_dimension(
|
||||
central.top, central.height, cell_height, num, self.border_width, left_align=left_align,
|
||||
margin_length=self.margin_width, padding_length=self.padding_width, bias=bias)
|
||||
decoration = self.margin_width + self.border_width + self.padding_width
|
||||
decoration_pairs = tuple(repeat((decoration, decoration), num))
|
||||
return layout_dimension(central.top, central.height, cell_height, decoration_pairs, bias=bias)
|
||||
|
||||
def simple_blank_rects(self, first_window, last_window):
|
||||
br = self.blank_rects
|
||||
@@ -363,6 +367,17 @@ class Layout: # {{{
|
||||
|
||||
def do_layout(self, windows, active_window_idx):
|
||||
raise NotImplementedError()
|
||||
|
||||
def resolve_borders(self, windows, active_window):
|
||||
if draw_minimal_borders:
|
||||
needs_borders_map = {w.id: (w is active_window or w.needs_attention) for w in windows}
|
||||
yield from self.minimal_borders(windows, active_window, needs_borders_map)
|
||||
else:
|
||||
yield from Layout.minimal_borders(self, windows, active_window, None)
|
||||
|
||||
def minimal_borders(self, windows, active_window, needs_borders_map):
|
||||
for w in windows:
|
||||
yield all_borders
|
||||
# }}}
|
||||
|
||||
|
||||
@@ -373,7 +388,9 @@ 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
|
||||
decoration_pairs = ((mw + self.padding_width, mw + self.padding_width),)
|
||||
wg = layout_single_window(decoration_pairs, decoration_pairs)
|
||||
for i, w in enumerate(windows):
|
||||
w.set_geometry(i, wg)
|
||||
if w.is_visible_in_layout:
|
||||
@@ -386,6 +403,8 @@ class Tall(Layout): # {{{
|
||||
name = 'tall'
|
||||
vlayout = Layout.ylayout
|
||||
main_is_horizontal = True
|
||||
only_between_border = False, False, False, True
|
||||
only_main_border = False, False, True, False
|
||||
|
||||
def remove_all_biases(self):
|
||||
self.main_bias = list(self.layout_opts['bias'])
|
||||
@@ -449,6 +468,26 @@ class Tall(Layout): # {{{
|
||||
self.between_blank_rect(windows[0], windows[1])
|
||||
# left bottom blank rect
|
||||
self.bottom_blank_rect(windows[0])
|
||||
|
||||
def minimal_borders(self, windows, active_window, needs_borders_map):
|
||||
last_i = len(windows) - 1
|
||||
for i, w in enumerate(windows):
|
||||
if needs_borders_map[w.id]:
|
||||
yield all_borders
|
||||
continue
|
||||
if i == 0:
|
||||
if last_i == 1 and needs_borders_map[windows[1].id]:
|
||||
yield no_borders
|
||||
else:
|
||||
yield self.only_main_border
|
||||
continue
|
||||
if i == last_i:
|
||||
yield no_borders
|
||||
break
|
||||
if needs_borders_map[windows[i+1].id]:
|
||||
yield no_borders
|
||||
else:
|
||||
yield self.only_between_border
|
||||
# }}}
|
||||
|
||||
|
||||
@@ -457,6 +496,8 @@ class Fat(Tall): # {{{
|
||||
name = 'fat'
|
||||
vlayout = Layout.xlayout
|
||||
main_is_horizontal = False
|
||||
only_between_border = False, False, True, False
|
||||
only_main_border = False, False, False, True
|
||||
|
||||
def do_layout(self, windows, active_window_idx):
|
||||
if len(windows) == 1:
|
||||
@@ -573,6 +614,9 @@ class Grid(Layout): # {{{
|
||||
if n == 1:
|
||||
return self.layout_single_window(windows[0])
|
||||
ncols, nrows, special_rows, special_col = self.calc_grid_size(n)
|
||||
layout_data = n, ncols, nrows, special_rows, special_col
|
||||
for w in windows:
|
||||
w.layout_data = layout_data
|
||||
|
||||
win_col_map = []
|
||||
|
||||
@@ -594,12 +638,57 @@ class Grid(Layout): # {{{
|
||||
for i in range(ncols - 1):
|
||||
self.between_blank_rect(win_col_map[i][0], win_col_map[i + 1][0])
|
||||
|
||||
def minimal_borders(self, windows, active_window, needs_borders_map):
|
||||
try:
|
||||
n, ncols, nrows, special_rows, special_col = windows[0].layout_data
|
||||
except Exception:
|
||||
n = -1
|
||||
if n != len(windows):
|
||||
# Something bad happened
|
||||
yield from Layout.minimal_borders(self, windows, active_window, needs_borders_map)
|
||||
return
|
||||
blank_row = [None for i in range(ncols)]
|
||||
matrix = tuple(blank_row[:] for j in range(max(nrows, special_rows)))
|
||||
wi = iter(windows)
|
||||
pos_map = {}
|
||||
col_counts = []
|
||||
for col in range(ncols):
|
||||
rows = special_rows if col == special_col else nrows
|
||||
for row in range(rows):
|
||||
matrix[row][col] = wid = next(wi).id
|
||||
pos_map[wid] = row, col
|
||||
col_counts.append(rows)
|
||||
|
||||
class Vertical(Layout):
|
||||
for w in windows:
|
||||
wid = w.id
|
||||
if needs_borders_map[wid]:
|
||||
yield all_borders
|
||||
continue
|
||||
row, col = pos_map[wid]
|
||||
if col + 1 < ncols:
|
||||
next_col_has_different_count = col_counts[col + 1] != col_counts[col]
|
||||
right_neighbor_id = matrix[row][col+1]
|
||||
else:
|
||||
right_neighbor_id = None
|
||||
next_col_has_different_count = False
|
||||
try:
|
||||
bottom_neighbor_id = matrix[row+1][col]
|
||||
except IndexError:
|
||||
bottom_neighbor_id = None
|
||||
yield (
|
||||
False, False,
|
||||
(right_neighbor_id is not None and not needs_borders_map[right_neighbor_id]) or next_col_has_different_count,
|
||||
bottom_neighbor_id is not None and not needs_borders_map[bottom_neighbor_id]
|
||||
)
|
||||
# }}}
|
||||
|
||||
|
||||
class Vertical(Layout): # {{{
|
||||
|
||||
name = 'vertical'
|
||||
main_is_horizontal = False
|
||||
vlayout = Layout.ylayout
|
||||
only_between_border = False, False, False, True
|
||||
|
||||
def variable_layout(self, num_windows, biased_map):
|
||||
return self.vlayout(num_windows, bias=variable_bias(num_windows, biased_map) if num_windows else None)
|
||||
@@ -638,12 +727,28 @@ class Vertical(Layout):
|
||||
# left, top and right blank rects
|
||||
self.simple_blank_rects(windows[0], windows[-1])
|
||||
|
||||
def minimal_borders(self, windows, active_window, needs_borders_map):
|
||||
last_i = len(windows) - 1
|
||||
for i, w in enumerate(windows):
|
||||
if needs_borders_map[w.id]:
|
||||
yield all_borders
|
||||
continue
|
||||
if i == last_i:
|
||||
yield no_borders
|
||||
break
|
||||
if needs_borders_map[windows[i+1].id]:
|
||||
yield no_borders
|
||||
else:
|
||||
yield self.only_between_border
|
||||
# }}}
|
||||
|
||||
class Horizontal(Vertical):
|
||||
|
||||
class Horizontal(Vertical): # {{{
|
||||
|
||||
name = 'horizontal'
|
||||
main_is_horizontal = True
|
||||
vlayout = Layout.xlayout
|
||||
only_between_border = False, False, True, False
|
||||
|
||||
def do_layout(self, windows, active_window_idx):
|
||||
window_count = len(windows)
|
||||
@@ -671,12 +776,13 @@ class Horizontal(Vertical):
|
||||
all_layouts = {o.name: o for o in globals().values() if isinstance(o, type) and issubclass(o, Layout) and o is not Layout}
|
||||
|
||||
|
||||
def create_layout_object_for(name, os_window_id, tab_id, margin_width, padding_width, border_width, layout_opts=''):
|
||||
key = name, os_window_id, tab_id, margin_width, padding_width, border_width, layout_opts
|
||||
def create_layout_object_for(name, os_window_id, tab_id, margin_width, single_window_margin_width, padding_width, border_width, layout_opts=''):
|
||||
key = name, os_window_id, tab_id, margin_width, single_window_margin_width, padding_width, border_width, layout_opts
|
||||
ans = create_layout_object_for.cache.get(key)
|
||||
if ans is None:
|
||||
name, layout_opts = name.partition(':')[::2]
|
||||
ans = create_layout_object_for.cache[key] = all_layouts[name](os_window_id, tab_id, margin_width, padding_width, border_width, layout_opts)
|
||||
ans = create_layout_object_for.cache[key] = all_layouts[name](
|
||||
os_window_id, tab_id, margin_width, single_window_margin_width, padding_width, border_width, layout_opts)
|
||||
return ans
|
||||
|
||||
|
||||
|
||||
@@ -12,33 +12,84 @@ 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, beam_cursor_data_file, 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_IBEAM_CURSOR, GLFW_MOD_SUPER, create_os_window, free_font_data,
|
||||
glfw_init, glfw_terminate, load_png_data, set_custom_cursor,
|
||||
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 set_custom_ibeam_cursor():
|
||||
with open(beam_cursor_data_file, 'rb') as f:
|
||||
data = f.read()
|
||||
rgba_data, width, height = load_png_data(data)
|
||||
c2x = os.path.splitext(beam_cursor_data_file)
|
||||
with open(c2x[0] + '@2x' + c2x[1], 'rb') as f:
|
||||
data = f.read()
|
||||
rgba_data2, width2, height2 = load_png_data(data)
|
||||
images = (rgba_data, width, height), (rgba_data2, width2, height2)
|
||||
try:
|
||||
set_custom_cursor(GLFW_IBEAM_CURSOR, images, 4, 8)
|
||||
except Exception as e:
|
||||
log_error('Failed to set custom beam cursor with error: {}'.format(e))
|
||||
|
||||
|
||||
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 +108,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
|
||||
@@ -65,6 +116,8 @@ def get_new_os_window_trigger(opts):
|
||||
|
||||
def _run_app(opts, args):
|
||||
new_os_window_trigger = get_new_os_window_trigger(opts)
|
||||
if is_macos:
|
||||
set_custom_ibeam_cursor()
|
||||
with cached_values_for(run_app.cached_values_name) as cached_values:
|
||||
with startup_notification_handler(extra_callback=run_app.first_window_callback) as pre_show_callback:
|
||||
window_id = create_os_window(
|
||||
@@ -86,7 +139,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 +218,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 +236,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 +248,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
|
||||
|
||||
@@ -119,6 +119,16 @@ contains_mouse(Window *w, OSWindow *os_window) {
|
||||
return (w->visible && window_left(w, os_window) <= x && x <= window_right(w, os_window) && window_top(w, os_window) <= y && y <= window_bottom(w, os_window));
|
||||
}
|
||||
|
||||
static inline double
|
||||
distance_to_window(Window *w, OSWindow *os_window) {
|
||||
double x = global_state.callback_os_window->mouse_x, y = global_state.callback_os_window->mouse_y;
|
||||
double cx = (window_left(w, os_window) + window_right(w, os_window)) / 2.0;
|
||||
double cy = (window_top(w, os_window) + window_bottom(w, os_window)) / 2.0;
|
||||
return (x - cx) * (x - cx) + (y - cy) * (y - cy);
|
||||
}
|
||||
|
||||
static bool clamp_to_window = false;
|
||||
|
||||
static inline bool
|
||||
cell_for_pos(Window *w, unsigned int *x, unsigned int *y, OSWindow *os_window) {
|
||||
WindowGeometry *g = &w->geometry;
|
||||
@@ -128,6 +138,10 @@ cell_for_pos(Window *w, unsigned int *x, unsigned int *y, OSWindow *os_window) {
|
||||
double mouse_x = global_state.callback_os_window->mouse_x;
|
||||
double mouse_y = global_state.callback_os_window->mouse_y;
|
||||
double left = window_left(w, os_window), top = window_top(w, os_window), right = window_right(w, os_window), bottom = window_bottom(w, os_window);
|
||||
if (clamp_to_window) {
|
||||
mouse_x = MIN(MAX(mouse_x, left), right);
|
||||
mouse_y = MIN(MAX(mouse_y, top), bottom);
|
||||
}
|
||||
if (mouse_x < left || mouse_y < top || mouse_x > right || mouse_y > bottom) return false;
|
||||
if (mouse_x >= g->right) qx = screen->columns - 1;
|
||||
else if (mouse_x >= g->left) qx = (unsigned int)((double)(mouse_x - g->left) / os_window->fonts_data->cell_width);
|
||||
@@ -147,10 +161,15 @@ update_drag(bool from_button, Window *w, bool is_release, int modifiers) {
|
||||
Screen *screen = w->render_data.screen;
|
||||
if (from_button) {
|
||||
if (is_release) {
|
||||
global_state.active_drag_in_window = 0;
|
||||
w->last_drag_scroll_at = 0;
|
||||
if (screen->selection.in_progress)
|
||||
screen_update_selection(screen, w->mouse_cell_x, w->mouse_cell_y, true);
|
||||
}
|
||||
else screen_start_selection(screen, w->mouse_cell_x, w->mouse_cell_y, modifiers == (int)OPT(rectangle_select_modifiers) || modifiers == ((int)OPT(rectangle_select_modifiers) | GLFW_MOD_SHIFT), EXTEND_CELL);
|
||||
else {
|
||||
global_state.active_drag_in_window = w->id;
|
||||
screen_start_selection(screen, w->mouse_cell_x, w->mouse_cell_y, modifiers == (int)OPT(rectangle_select_modifiers) || modifiers == ((int)OPT(rectangle_select_modifiers) | GLFW_MOD_SHIFT), EXTEND_CELL);
|
||||
}
|
||||
} else if (screen->selection.in_progress) {
|
||||
screen_update_selection(screen, w->mouse_cell_x, w->mouse_cell_y, false);
|
||||
}
|
||||
@@ -340,10 +359,18 @@ HANDLER(handle_button_event) {
|
||||
}
|
||||
}
|
||||
|
||||
static inline int
|
||||
currently_pressed_button() {
|
||||
for (int i = 0; i < GLFW_MOUSE_BUTTON_5; i++) {
|
||||
if (global_state.callback_os_window->mouse_button_pressed[i]) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
HANDLER(handle_event) {
|
||||
switch(button) {
|
||||
case -1:
|
||||
for (int i = 0; i < GLFW_MOUSE_BUTTON_5; i++) { if (global_state.callback_os_window->mouse_button_pressed[i]) { button = i; break; } }
|
||||
button = currently_pressed_button();
|
||||
handle_move_event(w, button, modifiers, window_idx);
|
||||
break;
|
||||
case GLFW_MOUSE_BUTTON_LEFT:
|
||||
@@ -372,6 +399,16 @@ mouse_in_region(Region *r) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline Window*
|
||||
window_for_id(id_type window_id) {
|
||||
Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab;
|
||||
for (unsigned int i = 0; i < t->num_windows; i++) {
|
||||
Window *w = t->windows + i;
|
||||
if (w->id == window_id) return w;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static inline Window*
|
||||
window_for_event(unsigned int *window_idx, bool *in_tab_bar) {
|
||||
Region central, tab_bar;
|
||||
@@ -386,7 +423,21 @@ window_for_event(unsigned int *window_idx, bool *in_tab_bar) {
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static inline Window*
|
||||
closest_window_for_event(unsigned int *window_idx) {
|
||||
Window *ans = NULL;
|
||||
double closest_distance = UINT_MAX;
|
||||
if (global_state.callback_os_window->num_tabs > 0) {
|
||||
Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab;
|
||||
for (unsigned int i = 0; i < t->num_windows; i++) {
|
||||
Window *w = t->windows + i;
|
||||
double d = distance_to_window(w, global_state.callback_os_window);
|
||||
if (d < closest_distance) { ans = w; closest_distance = d; *window_idx = i; }
|
||||
}
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
|
||||
void
|
||||
@@ -405,12 +456,34 @@ mouse_event(int button, int modifiers) {
|
||||
MouseShape old_cursor = mouse_cursor_shape;
|
||||
bool in_tab_bar;
|
||||
unsigned int window_idx = 0;
|
||||
Window *w = window_for_event(&window_idx, &in_tab_bar);
|
||||
Window *w = NULL;
|
||||
if (button == -1 && global_state.active_drag_in_window) { // drag move
|
||||
w = window_for_id(global_state.active_drag_in_window);
|
||||
if (w) {
|
||||
button = currently_pressed_button();
|
||||
if (button == GLFW_MOUSE_BUTTON_LEFT) {
|
||||
clamp_to_window = true;
|
||||
handle_move_event(w, button, modifiers, window_idx);
|
||||
clamp_to_window = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
w = window_for_event(&window_idx, &in_tab_bar);
|
||||
if (in_tab_bar) {
|
||||
mouse_cursor_shape = HAND;
|
||||
handle_tab_bar_mouse(button, modifiers);
|
||||
} else if(w) {
|
||||
handle_event(w, button, modifiers, window_idx);
|
||||
} else if (button == GLFW_MOUSE_BUTTON_LEFT && global_state.callback_os_window->mouse_button_pressed[button]) {
|
||||
// initial click, clamp it to the closest window
|
||||
w = closest_window_for_event(&window_idx);
|
||||
if (w) {
|
||||
clamp_to_window = true;
|
||||
handle_event(w, button, modifiers, window_idx);
|
||||
clamp_to_window = false;
|
||||
}
|
||||
}
|
||||
if (mouse_cursor_shape != old_cursor) {
|
||||
set_mouse_cursor(mouse_cursor_shape);
|
||||
|
||||
@@ -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;
|
||||
|
||||
126
kitty/png-reader.c
Normal file
126
kitty/png-reader.c
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* png-reader.c
|
||||
* Copyright (C) 2018 Kovid Goyal <kovid at kovidgoyal.net>
|
||||
*
|
||||
* Distributed under terms of the GPL3 license.
|
||||
*/
|
||||
|
||||
#include "png-reader.h"
|
||||
|
||||
|
||||
struct fake_file { const uint8_t *buf; size_t sz, cur; };
|
||||
|
||||
static void
|
||||
read_png_from_buffer(png_structp png, png_bytep out, png_size_t length) {
|
||||
struct fake_file *f = png_get_io_ptr(png);
|
||||
if (f) {
|
||||
size_t amt = MIN(length, f->sz - f->cur);
|
||||
memcpy(out, f->buf + f->cur, amt);
|
||||
f->cur += amt;
|
||||
}
|
||||
}
|
||||
|
||||
struct custom_error_handler {
|
||||
jmp_buf jb;
|
||||
png_error_handler_func err_handler;
|
||||
};
|
||||
|
||||
static void
|
||||
read_png_error_handler(png_structp png_ptr, png_const_charp msg) {
|
||||
struct custom_error_handler *eh;
|
||||
eh = png_get_error_ptr(png_ptr);
|
||||
if (eh == NULL) fatal("read_png_error_handler: could not retrieve error handler");
|
||||
if(eh->err_handler) eh->err_handler("EBADPNG", msg);
|
||||
longjmp(eh->jb, 1);
|
||||
}
|
||||
|
||||
static void
|
||||
read_png_warn_handler(png_structp UNUSED png_ptr, png_const_charp UNUSED msg) {
|
||||
// ignore warnings
|
||||
}
|
||||
|
||||
#define ABRT(code, msg) { if(d->err_handler) d->err_handler(#code, msg); goto err; }
|
||||
|
||||
void
|
||||
inflate_png_inner(png_read_data *d, const uint8_t *buf, size_t bufsz) {
|
||||
struct fake_file f = {.buf = buf, .sz = bufsz};
|
||||
png_structp png = NULL;
|
||||
png_infop info = NULL;
|
||||
struct custom_error_handler eh = {.err_handler = d->err_handler};
|
||||
png = png_create_read_struct(PNG_LIBPNG_VER_STRING, &eh, read_png_error_handler, read_png_warn_handler);
|
||||
if (!png) ABRT(ENOMEM, "Failed to create PNG read structure");
|
||||
info = png_create_info_struct(png);
|
||||
if (!info) ABRT(ENOMEM, "Failed to create PNG info structure");
|
||||
|
||||
if (setjmp(eh.jb)) goto err;
|
||||
|
||||
png_set_read_fn(png, &f, read_png_from_buffer);
|
||||
png_read_info(png, info);
|
||||
png_byte color_type, bit_depth;
|
||||
d->width = png_get_image_width(png, info);
|
||||
d->height = png_get_image_height(png, info);
|
||||
color_type = png_get_color_type(png, info);
|
||||
bit_depth = png_get_bit_depth(png, info);
|
||||
|
||||
// Ensure we get RGBA data out of libpng
|
||||
if (bit_depth == 16) png_set_strip_16(png);
|
||||
if (color_type == PNG_COLOR_TYPE_PALETTE) png_set_palette_to_rgb(png);
|
||||
// PNG_COLOR_TYPE_GRAY_ALPHA is always 8 or 16bit depth.
|
||||
if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) png_set_expand_gray_1_2_4_to_8(png);
|
||||
|
||||
if (png_get_valid(png, info, PNG_INFO_tRNS)) png_set_tRNS_to_alpha(png);
|
||||
|
||||
// These color_type don't have an alpha channel then fill it with 0xff.
|
||||
if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_PALETTE) png_set_filler(png, 0xFF, PNG_FILLER_AFTER);
|
||||
|
||||
if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) png_set_gray_to_rgb(png);
|
||||
png_read_update_info(png, info);
|
||||
|
||||
int rowbytes = png_get_rowbytes(png, info);
|
||||
d->sz = rowbytes * d->height * sizeof(png_byte);
|
||||
d->decompressed = malloc(d->sz + 16);
|
||||
if (d->decompressed == NULL) ABRT(ENOMEM, "Out of memory allocating decompression buffer for PNG");
|
||||
d->row_pointers = malloc(d->height * sizeof(png_bytep));
|
||||
if (d->row_pointers == NULL) ABRT(ENOMEM, "Out of memory allocating row_pointers buffer for PNG");
|
||||
for (int i = 0; i < d->height; i++) d->row_pointers[i] = d->decompressed + i * rowbytes;
|
||||
png_read_image(png, d->row_pointers);
|
||||
|
||||
d->ok = true;
|
||||
err:
|
||||
if (png) png_destroy_read_struct(&png, info ? &info : NULL, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
static void
|
||||
png_error_handler(const char *code, const char *msg) {
|
||||
PyErr_Format(PyExc_ValueError, "[%s] %s", code, msg);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
load_png_data(PyObject *self UNUSED, PyObject *args) {
|
||||
Py_ssize_t sz;
|
||||
const char *data;
|
||||
if (!PyArg_ParseTuple(args, "s#", &data, &sz)) return NULL;
|
||||
png_read_data d = {.err_handler=png_error_handler};
|
||||
inflate_png_inner(&d, (const uint8_t*)data, sz);
|
||||
PyObject *ans = NULL;
|
||||
if (d.ok && !PyErr_Occurred()) {
|
||||
ans = Py_BuildValue("y#ii", d.decompressed, (int)d.sz, d.width, d.height);
|
||||
} else {
|
||||
if (!PyErr_Occurred()) PyErr_SetString(PyExc_ValueError, "Unknown error while reading PNG data");
|
||||
}
|
||||
free(d.decompressed);
|
||||
free(d.row_pointers);
|
||||
return ans;
|
||||
}
|
||||
|
||||
static PyMethodDef module_methods[] = {
|
||||
METHODB(load_png_data, METH_VARARGS),
|
||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
bool
|
||||
init_png_reader(PyObject *module) {
|
||||
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
|
||||
return true;
|
||||
}
|
||||
21
kitty/png-reader.h
Normal file
21
kitty/png-reader.h
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Kovid Goyal <kovid at kovidgoyal.net>
|
||||
*
|
||||
* Distributed under terms of the GPL3 license.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "data-types.h"
|
||||
#include <png.h>
|
||||
typedef void(*png_error_handler_func)(const char*, const char*);
|
||||
typedef struct {
|
||||
uint8_t *decompressed;
|
||||
bool ok;
|
||||
png_bytep *row_pointers;
|
||||
int width, height;
|
||||
size_t sz;
|
||||
png_error_handler_func err_handler;
|
||||
} png_read_data;
|
||||
|
||||
void inflate_png_inner(png_read_data *d, const uint8_t *buf, size_t bufsz);
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1753,7 +1764,7 @@ screen_selection_range_for_line(Screen *self, index_type y, index_type *start, i
|
||||
index_type xlimit = line->xnum, xstart = 0;
|
||||
while (xlimit > 0 && CHAR_IS_BLANK(line->cpu_cells[xlimit - 1].ch)) xlimit--;
|
||||
while (xstart < xlimit && CHAR_IS_BLANK(line->cpu_cells[xstart].ch)) xstart++;
|
||||
*start = xstart; *end = xlimit;
|
||||
*start = xstart; *end = xlimit > 0 ? xlimit - 1 : 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -84,8 +84,10 @@ class Completer:
|
||||
return self.matches[state]
|
||||
|
||||
def __enter__(self):
|
||||
if os.path.exists(self.history_path):
|
||||
try:
|
||||
readline.read_history_file(self.history_path)
|
||||
except Exception:
|
||||
pass
|
||||
readline.set_completer(self.complete)
|
||||
delims = readline.get_completer_delims()
|
||||
readline.set_completer_delims(delims.replace('-', ''))
|
||||
|
||||
@@ -325,8 +325,9 @@ set_special_keys(PyObject *dict) {
|
||||
dict_iter(dict) {
|
||||
if (!PyTuple_Check(key)) { PyErr_SetString(PyExc_TypeError, "dict keys for special keys must be tuples"); return; }
|
||||
int mods = PyLong_AsLong(PyTuple_GET_ITEM(key, 0));
|
||||
int glfw_key = PyLong_AsLong(PyTuple_GET_ITEM(key, 1));
|
||||
set_special_key_combo(glfw_key, mods);
|
||||
bool is_native = PyTuple_GET_ITEM(key, 1) == Py_True;
|
||||
int glfw_key = PyLong_AsLong(PyTuple_GET_ITEM(key, 2));
|
||||
set_special_key_combo(glfw_key, mods, is_native);
|
||||
}}
|
||||
}
|
||||
|
||||
@@ -386,6 +387,7 @@ PYWRAP1(set_options) {
|
||||
S(macos_option_as_alt, PyObject_IsTrue);
|
||||
S(macos_hide_titlebar, PyObject_IsTrue);
|
||||
S(macos_quit_when_last_window_closed, PyObject_IsTrue);
|
||||
S(macos_window_resizable, PyObject_IsTrue);
|
||||
S(x11_hide_window_decorations, PyObject_IsTrue);
|
||||
S(macos_hide_from_tasks, PyObject_IsTrue);
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ typedef struct {
|
||||
color_type url_color, background, active_border_color, inactive_border_color, bell_border_color;
|
||||
double repaint_delay, input_delay;
|
||||
bool focus_follows_mouse;
|
||||
bool macos_option_as_alt, macos_hide_titlebar, macos_hide_from_tasks, x11_hide_window_decorations, macos_quit_when_last_window_closed;
|
||||
bool macos_option_as_alt, macos_hide_titlebar, macos_hide_from_tasks, x11_hide_window_decorations, macos_quit_when_last_window_closed, macos_window_resizable;
|
||||
int adjust_line_height_px, adjust_column_width_px;
|
||||
float adjust_line_height_frac, adjust_column_width_frac;
|
||||
float background_opacity, dim_opacity;
|
||||
@@ -141,6 +141,7 @@ typedef struct {
|
||||
bool in_sequence_mode;
|
||||
double font_sz_in_pts;
|
||||
struct { double x, y; } default_dpi;
|
||||
id_type active_drag_in_window;
|
||||
} GlobalState;
|
||||
|
||||
extern GlobalState global_state;
|
||||
|
||||
@@ -153,7 +153,7 @@ class TabBar:
|
||||
s = self.screen
|
||||
s.cursor.x = 0
|
||||
s.erase_in_line(2, False)
|
||||
max_title_length = (self.screen_geometry.xnum // max(1, len(data))) - 1
|
||||
max_title_length = max(1, (self.screen_geometry.xnum // max(1, len(data))) - 1)
|
||||
cr = []
|
||||
last_tab = data[-1] if data else None
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
1464
kitty/wcwidth-std.h
generated
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ import os
|
||||
import sys
|
||||
import weakref
|
||||
from collections import deque
|
||||
from enum import Enum
|
||||
from enum import IntEnum
|
||||
|
||||
from .child import cwd_of_process
|
||||
from .config import build_ansi_color_table
|
||||
@@ -33,7 +33,7 @@ from .utils import (
|
||||
)
|
||||
|
||||
|
||||
class DynamicColor(Enum):
|
||||
class DynamicColor(IntEnum):
|
||||
default_fg, default_bg, cursor_color, highlight_fg, highlight_bg = range(1, 6)
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ class Window:
|
||||
|
||||
def __init__(self, tab, child, opts, args, override_title=None):
|
||||
self.action_on_close = None
|
||||
self.layout_data = None
|
||||
self.needs_attention = False
|
||||
self.override_title = override_title
|
||||
self.overlay_window_id = None
|
||||
@@ -101,12 +102,12 @@ class Window:
|
||||
self.default_title = os.path.basename(child.argv[0] or appname)
|
||||
self.child_title = self.default_title
|
||||
self.id = add_window(tab.os_window_id, tab.id, self.title)
|
||||
self.clipboard_control_buffers = {'p': '', 'c': ''}
|
||||
if not self.id:
|
||||
raise Exception('No tab with id: {} in OS Window: {} was found, or the window counter wrapped'.format(tab.id, tab.os_window_id))
|
||||
self.tab_id = tab.id
|
||||
self.os_window_id = tab.os_window_id
|
||||
self.tabref = weakref.ref(tab)
|
||||
self.clipboard_control_buffers = {'p': '', 'c': ''}
|
||||
self.destroyed = False
|
||||
self.click_queue = deque(maxlen=3)
|
||||
self.geometry = WindowGeometry(0, 0, 0, 0, 0, 0)
|
||||
@@ -133,6 +134,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 +439,7 @@ class Window:
|
||||
if self.screen.in_bracketed_paste_mode:
|
||||
while True:
|
||||
new_text = text.replace(b'\033[201~', b'').replace(b'\x9b201~', b'')
|
||||
if text == new_text:
|
||||
if len(text) == len(new_text):
|
||||
break
|
||||
text = new_text
|
||||
self.screen.paste(text)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 463 KiB |
16
kitty_tests/gr.py
Normal file → Executable file
16
kitty_tests/gr.py
Normal file → Executable file
@@ -49,24 +49,24 @@ def display_png_file(path):
|
||||
|
||||
|
||||
def main():
|
||||
photo = sys.argv[-1]
|
||||
base = os.path.dirname(os.path.abspath(__file__))
|
||||
if not photo.lower().endswith('.png'):
|
||||
raise SystemExit('Must specify a PNG file to display')
|
||||
clear_screen()
|
||||
display(b'\xdd\xdd\xdd\xff', 1, 1, 0, 0, -10, 40, 20)
|
||||
display(open('logo/kitty.rgba', 'rb').read(), 256, 256, 0, 5, -9)
|
||||
display(open(os.path.join(base, '../logo/kitty.rgba'), 'rb').read(), 256, 256, 0, 5, -9)
|
||||
display(b'\0\0\0\xaa', 1, 1, 0, 7, -8, 40, 3)
|
||||
move_cursor(5, 8)
|
||||
print('kitty is \033[3m\033[32mawesome\033[m!')
|
||||
move_cursor(0, 21)
|
||||
print('Lenna...')
|
||||
display_png_file('kitty_tests/Lenna.png')
|
||||
print('Photo...')
|
||||
display_png_file(photo)
|
||||
try:
|
||||
try:
|
||||
raw_input()
|
||||
except NameError:
|
||||
input()
|
||||
input()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
os.chdir(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
|
||||
main()
|
||||
|
||||
@@ -10,7 +10,7 @@ from base64 import standard_b64decode, standard_b64encode
|
||||
from io import BytesIO
|
||||
|
||||
from kitty.fast_data_types import (
|
||||
parse_bytes, set_send_to_gpu, shm_unlink, shm_write
|
||||
load_png_data, parse_bytes, set_send_to_gpu, shm_unlink, shm_write
|
||||
)
|
||||
|
||||
from . import BaseTest
|
||||
@@ -209,8 +209,12 @@ class TestGraphics(BaseTest):
|
||||
def test_load_png_simple(self):
|
||||
# 1x1 transparent PNG
|
||||
png_data = standard_b64decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==')
|
||||
expected = b'\x00\xff\xff\x7f'
|
||||
self.ae(load_png_data(png_data), (expected, 1, 1))
|
||||
s, g, l, sl = load_helpers(self)
|
||||
sl(png_data, f=100, expecting_data=b'\x00\xff\xff\x7f')
|
||||
sl(png_data, f=100, expecting_data=expected)
|
||||
# test error handling for loading bad png data
|
||||
self.assertRaisesRegex(ValueError, '[EBADPNG]', load_png_data, b'dsfsdfsfsfd')
|
||||
|
||||
def test_image_put(self):
|
||||
cw, ch = 10, 20
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user