Compare commits

...

81 Commits

Author SHA1 Message Date
Kovid Goyal
3ce04c3e4b version 0.19.3 2020-12-19 08:19:34 +05:30
Kovid Goyal
3b5951eb2f Add a note about aliasing the SSH kitten 2020-12-18 16:59:42 +05:30
Kovid Goyal
aa8dd4ddf4 ... 2020-12-17 11:30:37 +05:30
Kovid Goyal
4cd14aee42 Add a changelog entry for the last merge
Also some misc code/docs cleanups for it.
2020-12-17 11:24:38 +05:30
Kovid Goyal
0b2726ec33 Cleanup fc_match_postscript_name 2020-12-17 11:16:51 +05:30
Kovid Goyal
bc7f689165 Clean up pattern_as_dict 2020-12-17 11:12:35 +05:30
Kovid Goyal
6681652c21 Merge branch 'master' of https://github.com/Diaoul/kitty 2020-12-17 11:02:26 +05:30
Kovid Goyal
ed4b5f179e Full screen kittens: Fix incorrect cursor position after kitten quits
DECOM should be reset only after entering alternate screen as it also
changes cursor position. Fixes #3176
2020-12-17 08:58:44 +05:30
Kovid Goyal
e48e166584 And another 2020-12-17 08:41:58 +05:30
Kovid Goyal
e7675e8bab Add some more commands to the client 2020-12-17 08:37:55 +05:30
Antoine Bertin
cf7eaea88c Add documentation 2020-12-16 18:01:14 +01:00
Antoine Bertin
3e0855ff45 Properly convert font_features to a list 2020-12-16 17:52:43 +01:00
Antoine Bertin
822c9367c6 Support default font_features from fontconfig 2020-12-16 14:58:38 +01:00
Kovid Goyal
0e46e2a6a7 Forgot to add new delete flags to reference table 2020-12-16 19:07:25 +05:30
Kovid Goyal
ade7ef19e7 Fix #3173 2020-12-16 19:00:31 +05:30
Kovid Goyal
e82a6dedd9 Fix deletion with image numbers 2020-12-16 18:57:46 +05:30
Kovid Goyal
2fd6bf7442 Fix failing test
Error responses must be sent for put commands that refer
non-existent images
2020-12-16 18:41:47 +05:30
Kovid Goyal
d10681f27f Send responses when only a number and no id is specified 2020-12-16 18:31:30 +05:30
Kovid Goyal
e96ff19a7a Graphics protocol: Add support for having the terminal emulator assign image ids
Useful when multiple non co-operating programs want to share the screen.
Fixes #3163
2020-12-16 17:31:15 +05:30
Kovid Goyal
09e75ea329 Add a note about cursor movement after placing an image. 2020-12-15 07:37:14 +05:30
Kovid Goyal
84b03e9f77 ... 2020-12-14 20:06:43 +05:30
Kovid Goyal
55597dcea5 Add the needed import to the example 2020-12-14 19:23:30 +05:30
Kovid Goyal
3290bf3c35 Merge branch 'patch-1' of https://github.com/ThatNerdyPikachu/kitty 2020-12-14 08:37:53 +05:30
Pika
6771429911 Update faq.rst 2020-12-13 19:04:14 -05:00
Kovid Goyal
e428858ad6 Micro-optimization 2020-12-12 17:43:33 +05:30
Kovid Goyal
3e73f860d1 Fix assert in ringbuf_memcpy_from 2020-12-11 21:38:58 +05:30
Kovid Goyal
4cf5bd8d70 Turn on the asserts in ringbuf for debug builds 2020-12-11 21:26:15 +05:30
Kovid Goyal
bffe0f4a6c Use a separate, re-useable ring-buffer implementation
Not only is it re-useable it also allows for easier debugging by
separating the ring buffer specific logic and the pager history logic.

Hopefully it fixes #3049
2020-12-11 21:17:38 +05:30
Kovid Goyal
3b8be26cc7 Workaround for Ubuntu as usual shipping with insanely outdated packages
Fixes #3160
2020-12-11 12:22:28 +05:30
Kovid Goyal
9e95939c47 Add wttr.in to integrations 2020-12-11 07:21:22 +05:30
Kovid Goyal
0af7af95cf Merge branch 'master' of https://github.com/molly-cactus/kitty 2020-12-09 08:17:22 +05:30
Margaret K. Geiger
2b3e2ea6f8 Add tab_switch_strategy option 'right' 2020-12-08 16:55:27 -08:00
Kovid Goyal
19870983ca Allow specifying text formatting in tab_title_template
Fixes #3146
2020-12-08 21:42:40 +05:30
Kovid Goyal
6e83b4c1bc Make it more prominent when you dont need allow_remote_control 2020-12-07 16:24:54 +05:30
Kovid Goyal
14f5e10fd6 Fix mapping of remote_control not working for actions that yield generators
Fixes #3147
2020-12-07 16:15:44 +05:30
Kovid Goyal
ed41e3cd25 Add mentions of glkitty to the docs
Fixes #3111
2020-12-06 08:23:25 +05:30
Kovid Goyal
4cc58e8535 Better fix for filtering out -march=native 2020-12-04 20:59:18 +05:30
Kovid Goyal
e1d9686eb5 Merge branch 'master' of https://github.com/runchard/kitty 2020-12-04 19:35:25 +05:30
Runchard
c5cb4f9e69 do not use clang option -march=native on Apple Silicon
Apple Silicon arm64-apple-darwin clang do not support `-march` option.
2020-12-04 21:07:19 +08:00
Kovid Goyal
4a60413e0b ... 2020-12-03 21:38:42 +05:30
Kovid Goyal
245f5d60b5 Add a test for image put + delete + put 2020-12-03 21:37:32 +05:30
Kovid Goyal
23420adfa6 Graphics protocol: Allow suppressing responses from the terminal to graphics commands 2020-12-03 20:42:03 +05:30
Kovid Goyal
c9828dfece Removed unused parameter 2020-12-03 20:02:16 +05:30
Kovid Goyal
f52c843fa2 Add support for placement id to GraphicsCommand 2020-12-02 05:35:57 +05:30
Kovid Goyal
b5e704a934 Graphics protocol: Add support for giving individual image placements their
Fixes #3133
2020-12-02 05:25:19 +05:30
Kovid Goyal
0173959e64 Make a note that images can be placed and such are known as placements 2020-12-01 21:21:24 +05:30
Kovid Goyal
b00cd5cbc3 Match save/restore cursor behavior of other terms
For the sake of interoperability. This means that doing a DECRC without
a prior DECSC is now undefined. However, one can now issue multiple
DECRC for a single DECSC. Fixes #1264
2020-12-01 17:55:17 +05:30
Kovid Goyal
00aba7c646 Add basic editing support for the broadcast kitten 2020-12-01 17:35:15 +05:30
Kovid Goyal
34db18ea0a Also transmit key events when broadcasting 2020-12-01 14:11:50 +05:30
Kovid Goyal
457c292d3c Fix one ANSI formatting escape code not being removed from the pager history buffer when piping it as plain text
Fixes #3132
2020-12-01 13:17:35 +05:30
Kovid Goyal
f481c17732 The broadcast kitten
Fixes #1569
2020-12-01 13:11:32 +05:30
Kovid Goyal
4a049b14ca Fix infinite loop in send-text when stdin is a tty 2020-12-01 12:29:03 +05:30
Kovid Goyal
ed2dae5884 Fix error when sending empty write buf 2020-12-01 12:25:09 +05:30
Kovid Goyal
df61ee4ac3 ... 2020-12-01 11:39:50 +05:30
Kovid Goyal
850bc12929 DRYer 2020-11-30 22:16:21 +05:30
Kovid Goyal
559e17eb13 Add a couple more useful options to control selected windows for @ send-text 2020-11-30 22:04:46 +05:30
Kovid Goyal
4c4f6983d1 Fix env LC_CTYPE=UTF-8 not working on macOS 2020-11-30 16:23:29 +05:30
Kovid Goyal
2dbeb151f3 Changelog entry for traditional fullscreen on Big Sur 2020-11-29 12:04:52 +05:30
Kovid Goyal
82c137878c Graphics protocol: Dont return filename in the error message when opening file fails, since filenames can contain control characters
Fixes #3128
2020-11-29 12:02:14 +05:30
Kovid Goyal
8845bd6ed3 ... 2020-11-27 15:28:16 +05:30
Kovid Goyal
c89d861c0e Add A FAQ entry about opening new windows with the cwd 2020-11-25 17:47:20 +05:30
Kovid Goyal
3ed536ace1 ... 2020-11-22 12:32:12 +05:30
Kovid Goyal
d13ad4ec4b Merge branch 'nohand' of https://github.com/derekschrock/kitty
Also add a changelog entry and some stylistic fixes
2020-11-22 12:31:28 +05:30
Kovid Goyal
cb5ac1fcf8 DRYer 2020-11-22 12:24:00 +05:30
Kovid Goyal
53797b9ead Wayland: Fix key repeat being stopped by the release of an unrelated key
Fixes #2191
2020-11-22 12:14:52 +05:30
Derek Schrock
cb164e5cac Add new option detect_urls to check for URLs under the mouse cursor
Enabled by default add new option `detect_urls` that will allow you
contorl if kitty should detect URLs under the mouse cursor.  If disabled
the URLs are still clickable with open_url_modifiers.
2020-11-22 00:58:26 -05:00
Kovid Goyal
c46ebebbb3 Override the min-width for body from the alabaster theme
For some reason it sets min width to 450px which is too wide
for some mobile screens. Fixes #3116
2020-11-22 09:14:17 +05:30
Kovid Goyal
ff96036f62 Better window title when choosing tabs/OS windows 2020-11-21 14:58:08 +05:30
Kovid Goyal
6409786f8d Add a new mappable select_tab action to choose a tab to switch to even when the tab bar is hidden
Fixes #3115
2020-11-21 14:52:18 +05:30
Kovid Goyal
05eb07caf5 Add to changelog for last merge
Also remove a spurious return that was disabling a test. Fixes #3113
2020-11-20 20:23:23 +05:30
Kovid Goyal
f5b2a60549 Merge branch 'scroll-region-scrollback' of https://github.com/romkatv/kitty 2020-11-20 20:20:53 +05:30
Roman Perepelitsa
620eaf1ada Change the flow of lines from the scroll region to the scrollback
There are two user-visible changes in here:

1. If a scroll region is set such that there is a bottom margin and no
   top margin, scrolling the region forward used to discard the top
   lines. Now those lines are appended to the scrollback.
   ```shell
   # Assuming a terminal window with 24 lines.
   printf '\033[H\033[J\033[3J\033[0;5r' && seq 100
   ```
   This command used to result in an empty scrollback. Now it contains
   the numbers 1-96.
2. If a scroll region is set such that there is a top margin and no bottom
   margin, scrolling the region forward used to append the top lines to
   the scrollback. Now these lines are discarded.
   ```shell
   # Assuming a terminal window with 24 lines.
   printf '\033[H\033[J\033[3J\033[2;24r' && seq 100
   ```
   This command used to populate scrollback with the numbers 2-78. Now
   the scrollback is empty. The numbers on the screen are the same as
   before: 1 and 79-100.

Related issue: #3113.
2020-11-20 15:41:57 +01:00
Kovid Goyal
9193a20b44 macOS: Fix minimize not working for chromeless windows
Fixes #3112
2020-11-20 07:30:46 +05:30
Kovid Goyal
bdcac9aed3 Partial fix for traditional full screen on Big Turd 2020-11-16 10:23:01 +05:30
Kovid Goyal
e9a28550cc explanatory note 2020-11-15 14:31:40 +05:30
Kovid Goyal
f3cd3e74ae ... 2020-11-15 10:17:32 +05:30
Kovid Goyal
b853fc30c0 Merge branch 'master' of https://github.com/dankamongmen/kitty 2020-11-15 10:13:00 +05:30
nick black
63e32a94c0 Sextants: fix arithmetic mistake that broke 🬓, 🬦, 🬧
The scheme used in draw_sextant works well enough, but the
feed was broken. 21 and 42 need to be skipped, not 20 and
40. Without this change, the following glyphs are broken:

U+1FB13 BLOCK SEXTANT-35 🬓 renders as LEFT HALF
U+1FB26 BLOCK SEXTANT-46 🬦 renders as SEXTANT-146
U+1FB27 BLOCK SEXTANT-146 🬧 renders as RIGHT HALF

With this change, the entirety of the sextant block works
correctly. Found debugging the Notcurses sextant blitter:

https://github.com/dankamongmen/notcurses/issues/1112
2020-11-14 23:30:25 -05:00
Kovid Goyal
ef596f6a8b Distribute extra pixels among all eight-blocks rather than adding them all to the last block
Makes one block stand out less, every block is now at most a pixel in
size different from every other block. Fixes #3097
2020-11-14 11:06:47 +05:30
Kovid Goyal
c830097511 Add a note about using hyperlinks with the hints kitten 2020-11-14 00:03:15 +05:30
Kovid Goyal
d8790a4628 Make the help text for --linenum-action less confusing 2020-11-13 23:53:26 +05:30
66 changed files with 1939 additions and 338 deletions

View File

@@ -21,6 +21,10 @@ div.body p, div.body dd, div.body li, div.body blockquote {
text-align: justify; text-align: justify;
} }
div.body {
min-width: 200px;
}
pre { pre {
white-space: pre-wrap; white-space: pre-wrap;
} }

View File

@@ -30,7 +30,7 @@ Build-time dependencies:
* gcc or clang * gcc or clang
* pkg-config * pkg-config
* For building on Linux in addition to the above dependencies you might also need to install the ``-dev`` packages for: * For building on Linux in addition to the above dependencies you might also need to install the ``-dev`` packages for:
``libdbus-1-dev``, ``libxcursor-dev``, ``libxrandr-dev``, ``libxi-dev``, ``libxinerama-dev``, ``libgl1-mesa-dev``, ``libxkbcommon-x11-dev``, ``libfontconfig-dev``, and ``libpython-dev``, ``libdbus-1-dev``, ``libxcursor-dev``, ``libxrandr-dev``, ``libxi-dev``, ``libxinerama-dev``, ``libgl1-mesa-dev``, ``libxkbcommon-x11-dev``, ``libfontconfig-dev``, and ``libpython3-dev``,
if they are not already installed by your distro. if they are not already installed by your distro.
Install and run from source Install and run from source

View File

@@ -4,6 +4,68 @@ Changelog
|kitty| is a feature full, cross-platform, *fast*, GPU based terminal emulator. |kitty| is a feature full, cross-platform, *fast*, GPU based terminal emulator.
To update |kitty|, :doc:`follow the instructions <binary>`. To update |kitty|, :doc:`follow the instructions <binary>`.
0.19.3 [2020-12-19]
-------------------
- Happy holidays to all kitty users!
- A new :doc:`broadcast <kittens/broadcast>` kitten to type in all kitty windows
simultaneously (:iss:`1569`)
- Add a new mappable `select_tab` action to choose a tab to switch to even
when the tab bar is hidden (:iss:`3115`)
- Allow specifying text formatting in :opt:`tab_title_template` (:iss:`3146`)
- Linux: Read :opt:`font_features` from the FontConfig database as well, so
that they can be configured in a single, central location (:pull:`3174`)
- Graphics protocol: Add support for giving individual image placements their
own ids and for asking the terminal emulator to assign ids for images. Also
allow suppressing responses from the terminal to commands.
These are backwards compatible protocol extensions. (:iss:`3133`,
:iss:`3163`)
- Distribute extra pixels among all eight-blocks rather than adding them
all to the last block (:iss:`3097`)
- Fix drawing of a few sextant characters incorrect (:pull:`3105`)
- macOS: Fix minimize not working for chromeless windows (:iss:`3112`)
- Preserve lines in the scrollback if a scrolling region is defined that
is contiguous with the top of the screen (:iss:`3113`)
- Wayland: Fix key repeat being stopped by the release of an unrelated key
(:iss:`2191`)
- Add an option, :opt:`detect_urls` to control whether kitty will detect URLs
when the mouse moves over them (:pull:`3118`)
- Graphics protocol: Dont return filename in the error message when opening file
fails, since filenames can contain control characters (:iss:`3128`)
- macOS: Partial fix for traditional fullscreen not working on Big Sur
(:iss:`3100`)
- Fix one ANSI formatting escape code not being removed from the pager history
buffer when piping it as plain text (:iss:`3132`)
- Match the save/restore cursor behavior of other terminals, for the sake of
interoperability. This means that doing a DECRC without a prior DECSC is now
undefined (:iss:`1264`)
- Fix mapping ``remote_control send-text`` not working (:iss:`3147`)
- Add a ``right`` option for :opt:`tab_switch_strategy` (:pull:`3155`)
- Fix a regression in 0.19.0 that caused a rare crash when using the optional
:opt:`scrollback_pager_history_size` (:iss:`3049`)
- Full screen kittens: Fix incorrect cursor position after kitten quits
(:iss:`3176`)
0.19.2 [2020-11-13] 0.19.2 [2020-11-13]
------------------- -------------------

View File

@@ -24,7 +24,7 @@ from pygments.lexer import RegexLexer, bygroups # type: ignore
from pygments.token import ( # type: ignore from pygments.token import ( # type: ignore
Comment, Keyword, Literal, Name, Number, String, Whitespace Comment, Keyword, Literal, Name, Number, String, Whitespace
) )
from sphinx import addnodes # type: ignore from sphinx import addnodes, version_info # type: ignore
from sphinx.environment.adapters.toctree import TocTree # type: ignore from sphinx.environment.adapters.toctree import TocTree # type: ignore
from sphinx.util.logging import getLogger # type: ignore from sphinx.util.logging import getLogger # type: ignore
@@ -555,7 +555,7 @@ def process_shortcut_link(env: Any, refnode: Any, has_explicit_title: bool, titl
def write_conf_docs(app: Any, all_kitten_names: Iterable[str]) -> None: def write_conf_docs(app: Any, all_kitten_names: Iterable[str]) -> None:
app.add_lexer('conf', ConfLexer) app.add_lexer('conf', ConfLexer() if version_info[0] < 3 else ConfLexer)
app.add_object_type( app.add_object_type(
'opt', 'opt', 'opt', 'opt',
indextemplate="pair: %s; Config Setting", indextemplate="pair: %s; Config Setting",
@@ -604,7 +604,7 @@ def setup(app: Any) -> None:
write_cli_docs(kn) write_cli_docs(kn)
write_remote_control_protocol_docs() write_remote_control_protocol_docs()
write_conf_docs(app, kn) write_conf_docs(app, kn)
app.add_lexer('session', SessionLexer) app.add_lexer('session', SessionLexer() if version_info[0] < 3 else SessionLexer)
app.add_role('link', link_role) app.add_role('link', link_role)
app.add_role('iss', partial(num_role, 'issues')) app.add_role('iss', partial(num_role, 'issues'))
app.add_role('pull', partial(num_role, 'pull')) app.add_role('pull', partial(num_role, 'pull'))

View File

@@ -47,6 +47,10 @@ terminfo files to the server::
kitty +kitten ssh myserver kitty +kitten ssh myserver
This ssh kitten takes all the same command line arguments
as ssh, you can alias it to ssh in your shell's rc files to avoid having to
type it each time.
If for some reason that does not work (typically because the server is using a If for some reason that does not work (typically because the server is using a
non POSIX compliant shell), you can use the following one-liner instead (it non POSIX compliant shell), you can use the following one-liner instead (it
is slower as it needs to ssh into the server twice, but will work with most is slower as it needs to ssh into the server twice, but will work with most
@@ -182,7 +186,7 @@ following :file:`~/.config/fontconfig/fonts.conf`::
<?xml version="1.0"?> <?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd"> <!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig> <fontconfig>
<match target="font"> <match target="scan">
<test name="family"> <test name="family">
<string>Your Font Family Name</string> <string>Your Font Family Name</string>
</test> </test>
@@ -236,6 +240,18 @@ available. The manual way to figure it out is:
3. Use ``\x(hexval)`` in your ``send_text`` command in kitty. So in this example, ``\x13`` 3. Use ``\x(hexval)`` in your ``send_text`` command in kitty. So in this example, ``\x13``
How do I open a new window or tab with the same working directory as the current window?
--------------------------------------------------------------------------------------------
In :file:`kitty.conf` add the following::
map f1 launch --cwd=current
map f2 launch --cwd=current --type=tab
Pressing :kbd:`F1` will open a new kitty window with the same working directory
as the current window. The :doc:`launch command <launch>` is very powerful,
explore :doc:`its documentation <launch>`.
I am using tmux and have a problem I am using tmux and have a problem
-------------------------------------- --------------------------------------

View File

@@ -38,6 +38,7 @@ Some programs that use the kitty graphics protocol:
* `neofetch <https://github.com/dylanaraps/neofetch>`_ - A command line system * `neofetch <https://github.com/dylanaraps/neofetch>`_ - A command line system
information tool information tool
* `viu <https://github.com/atanunq/viu>`_ - a terminal image viewer * `viu <https://github.com/atanunq/viu>`_ - a terminal image viewer
* `glkitty <https://github.com/michaeljclark/glkitty>`_ - C library to draw OpenGL shaders in the terminal with a glgears demo
.. contents:: .. contents::
@@ -314,7 +315,7 @@ terminal emulators handle input in a FIFO manner, anyway.
So for example, you could send:: So for example, you could send::
<ESC>_Gi=31,s=1,v=1,a=q,t=d,f=24;<NUL><NUL><NUL><ESC>\<ESC>[c <ESC>_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA<ESC>\<ESC>[c
If you get back a response to the graphics query, the terminal emulator supports If you get back a response to the graphics query, the terminal emulator supports
the protocol, if you get back a response to the device attributes query without the protocol, if you get back a response to the device attributes query without
@@ -326,11 +327,12 @@ Display images on screen
Every transmitted image can be displayed an arbitrary number of times on the Every transmitted image can be displayed an arbitrary number of times on the
screen, in different locations, using different parts of the source image, as screen, in different locations, using different parts of the source image, as
needed. You can either simultaneously transmit and display an image using the needed. Each such display of an image is called a *placement*. You can either
action ``a=T``, or first transmit the image with a id, such as ``i=10`` and then display simultaneously transmit and display an image using the action ``a=T``, or first
it with ``a=p,i=10`` which will display the previously transmitted image at the current transmit the image with a id, such as ``i=10`` and then display it with
cursor position. When specifying an image id, the terminal emulator will reply with an ``a=p,i=10`` which will display the previously transmitted image at the current
acknowledgement code, which will be either:: cursor position. When specifying an image id, the terminal emulator will reply
to the placement request with an acknowledgement code, which will be either::
<ESC>_Gi=<id>;OK<ESC>\ <ESC>_Gi=<id>;OK<ESC>\
@@ -343,6 +345,24 @@ scheme described above for querying available transmission media, except that
here we are querying if the image with the specified id is available or needs to here we are querying if the image with the specified id is available or needs to
be re-transmitted. be re-transmitted.
Since there can be many placements per image, you can also give placements an
id. To do so add the ``p`` key with a number between ``1`` and ``4294967295``.
When you specify a placement id, it will be added to the acknowledgement code
above. Every placement is uniquely identified by the pair of the ``image id``
and the ``placement id``. If you specify a placement id for an image that does
not have an id, it will be ignored. An example response::
<ESC>_Gi=<image id>,p=<placement id>;OK<ESC>\
If you send two placements with the same ``image id`` and ``placement id`` the
second one will replace the first. This can be used to resize or move
placements around the screen, without flicker.
.. versionadded:: 0.19.3
Support for specifying placement ids (see :doc:`kittens/query_terminal` to query kitty version)
Controlling displayed image layout Controlling displayed image layout
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -369,6 +389,13 @@ allows rendering of text on top of images. Negative z-index values below
INT32_MIN/2 (-1,073,741,824) will be drawn under cells with non-default background INT32_MIN/2 (-1,073,741,824) will be drawn under cells with non-default background
colors. colors.
.. note:: After placing an image on the screen the cursor must be moved to the
right by the number of cols in the image placement rectangle and down by the
number of rows in the image placement rectangle. If either of these cause
the cursor to leave either the screen or the scroll area, the exact
positioning of the cursor is undefined, and up to implementations.
Deleting images Deleting images
--------------------- ---------------------
@@ -385,24 +412,77 @@ scrollback buffer. The values of the ``x`` and ``y`` keys are the same as cursor
================= ============ ================= ============
Value of ``d`` Meaning Value of ``d`` Meaning
================= ============ ================= ============
``a`` or ``A`` Delete all images visible on screen ``a`` or ``A`` Delete all placements visible on screen
``i`` or ``I`` Delete all images with the specified id, specified using the ``i`` key. ``i`` or ``I`` Delete all images with the specified id, specified using the ``i`` key. If you specify a ``p`` key for the placement id as well, then only the placement with the specified image id and placement id will be deleted.
``c`` or ``C`` Delete all images that intersect with the current cursor position. ``n`` or ``N`` Delete newest image with the specified number, specified using the ``I`` key. If you specify a ``p`` key for the
``p`` or ``P`` Delete all images that intersect a specific cell, the cell is specified using the ``x`` and ``y`` keys placement id as well, then only the placement with the specified number and placement id will be deleted.
``q`` or ``Q`` Delete all images that intersect a specific cell having a specific z-index. The cell and z-index is specified using the ``x``, ``y`` and ``z`` keys. ``c`` or ``C`` Delete all placements that intersect with the current cursor position.
``x`` or ``X`` Delete all images that intersect the specified column, specified using the ``x`` key. ``c`` or ``C`` Delete all placements that intersect with the current cursor position.
``y`` or ``Y`` Delete all images that intersect the specified row, specified using the ``y`` key. ``p`` or ``P`` Delete all placements that intersect a specific cell, the cell is specified using the ``x`` and ``y`` keys
``z`` or ``Z`` Delete all images that have the specified z-index, specified using the ``z`` key. ``q`` or ``Q`` Delete all placements that intersect a specific cell having a specific z-index. The cell and z-index is specified using the ``x``, ``y`` and ``z`` keys.
``x`` or ``X`` Delete all placements that intersect the specified column, specified using the ``x`` key.
``y`` or ``Y`` Delete all placements that intersect the specified row, specified using the ``y`` key.
``z`` or ``Z`` Delete all placements that have the specified z-index, specified using the ``z`` key.
================= ============ ================= ============
Note when all placements for an image have been deleted, the image is also
deleted, if the capital letter form above is specified. Also, when the terminal
is running out of quota space for image, images without placements will be
preferentially deleted.
Some examples:: Some examples::
<ESC>_Ga=d<ESC>\ # delete all visible images <ESC>_Ga=d<ESC>\ # delete all visible placements
<ESC>_Ga=d,d=i,i=10<ESC>\ # delete the image with id=10, without freeing data <ESC>_Ga=d,d=i,i=10<ESC>\ # delete the image with id=10, without freeing data
<ESC>_Ga=d,d=Z,z=-1<ESC>\ # delete the images with z-index -1, also freeing up image data <ESC>_Ga=d,d=i,i=10,p=7<ESC>\ # delete the image with id=10 and placement id=7, without freeing data
<ESC>_Ga=d,d=p,x=3,y=4<ESC>\ # delete all images that intersect the cell at (3, 4), without freeing data <ESC>_Ga=d,d=Z,z=-1<ESC>\ # delete the placements with z-index -1, also freeing up image data
<ESC>_Ga=d,d=p,x=3,y=4<ESC>\ # delete all placements that intersect the cell at (3, 4), without freeing data
Suppressing responses from the terminal
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you are using the graphics protocol from a limited client, such as a shell
script, it might be useful to avoid having to process responses from the
terminal. For this, you can use the ``q`` key. Set it to ``1`` to suppress
``OK`` responses and to ``2`` to suppress failure responses.
.. versionadded:: 0.19.3
The ability to suppress responses (see :doc:`kittens/query_terminal` to query kitty version)
Requesting image ids from the terminal
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you are writing a program that is going to share the screen with other
programs and you still want to use image ids, it is not possible to know
what image ids are free to use. In this case, instead of using the ``i``
key to specify and image id use the ``I`` key to specify and image number
instead. These numbers are not unique.
When creating a new image, even if an existing image has the same number a new
one is created. And the terminal will reply with the id of the newly created
image. For example, when creating an image with ``I=13``, the terminal will
send the response::
<ESC>_Gi=99,I=13;OK<ESC>\
Here, the value of ``i`` is the id for the newly created image and the value of
``I`` is the same as was sent in the creation command.
All future commands that refer to images using the image number, such as
creating placements or deleting images, will act on only the newest image with
that number. This allows the client program to send a bunch of commands dealing
with an image by image number without waiting for a response from the terminal
with the image id. Once such a response is received, the client program should
use the ``i`` key with the image id for all future communication.
.. note:: Specifying both ``i`` and ``I`` keys in any command is an error. The
terminal must reply with an EINVAL error message, unless silenced.
.. versionadded:: 0.19.3
The ability to use image numbers (see :doc:`kittens/query_terminal` to query kitty version)
Image persistence and storage quotas Image persistence and storage quotas
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -425,6 +505,9 @@ Key Value Default Description
======= ==================== ========= ================= ======= ==================== ========= =================
``a`` Single character. ``t`` The overall action this graphics command is performing. ``a`` Single character. ``t`` The overall action this graphics command is performing.
``(t, T, q, p, d)`` ``(t, T, q, p, d)``
``q`` ``0, 1, 2`` ``0`` Suppress responses from the terminal to this graphics command.
**Keys for image transmission** **Keys for image transmission**
----------------------------------------------------------- -----------------------------------------------------------
``f`` Positive integer. ``32`` The format in which the image data is sent. ``f`` Positive integer. ``32`` The format in which the image data is sent.
@@ -437,6 +520,8 @@ Key Value Default Description
``O`` Positive integer. ``0`` The offset from which to read data from a file. ``O`` Positive integer. ``0`` The offset from which to read data from a file.
``i`` Positive integer. ``i`` Positive integer.
``(0 - 4294967295)`` ``0`` The image id ``(0 - 4294967295)`` ``0`` The image id
``p`` Positive integer.
``(0 - 4294967295)`` ``0`` The placement id
``o`` Single character. ``null`` The type of data compression. ``o`` Single character. ``null`` The type of data compression.
``only z`` ``only z``
``m`` zero or one ``0`` Whether there is more chunked data available. ``m`` zero or one ``0`` Whether there is more chunked data available.
@@ -454,9 +539,9 @@ Key Value Default Description
**Keys for deleting images** **Keys for deleting images**
----------------------------------------------------------- -----------------------------------------------------------
``d`` Single character. ``a`` What to delete. ``d`` Single character. ``a`` What to delete.
``(a, A, c, C, i, ``(a, A, c, C, n, N,
I, p, P, q, Q, x, X, i, I, p, P, q, Q, x,
y, Y, z, Z)``. X, y, Y, z, Z)``.
======= ==================== ========= ================= ======= ==================== ========= =================

View File

@@ -70,6 +70,9 @@ System and data visualisation tools
`onefetch <https://github.com/o2sh/onefetch>`_ `onefetch <https://github.com/o2sh/onefetch>`_
a tool to fetch information about your git repositories a tool to fetch information about your git repositories
`wttr <https://github.com/chubin/wttr.in>`_
a tool to display weather information in your terminal with curl
Editor integration Editor integration
----------------------- -----------------------
@@ -123,3 +126,6 @@ Miscellaneous
`weechat-hints <https://github.com/GermainZ/kitty-weechat-hints>`_ `weechat-hints <https://github.com/GermainZ/kitty-weechat-hints>`_
URL hints kitten for WeeChat that works without having to use WeeChat's URL hints kitten for WeeChat that works without having to use WeeChat's
raw-mode. raw-mode.
`glkitty <https://github.com/michaeljclark/glkitty>`_
C library to draw OpenGL shaders in the terminal with a glgears demo

View File

@@ -0,0 +1,23 @@
broadcast - type text in all kitty windows
==================================================
The ``broadcast`` kitten can be used to type text simultaneously in
all kitty windows (or a subset as desired).
To use it, simply create a mapping in :file:`kitty.conf` such as::
map F1 launch --allow-remote-control kitty +kitten broadcast
Then press the :kbd:`F1` key and whatever you type in the newly created widow
will be sent to all kitty windows.
You can use the options described below to control which windows
are selected.
.. program:: kitty +kitten broadcast
Command Line Interface
--------------------------
.. include:: /generated/cli-kitten-broadcast.rst

View File

@@ -20,6 +20,9 @@ your machine).
.. code-block:: python .. code-block:: python
from typing import List
from kitty.boss import Boss
def main(args: List[str]) -> str: def main(args: List[str]) -> str:
# this is the main entry point of the kitten, it will be executed in # this is the main entry point of the kitten, it will be executed in
# the overlay window when the kitten is launched # the overlay window when the kitten is launched
@@ -28,7 +31,7 @@ your machine).
# handle_result() function # handle_result() function
return answer return answer
def handle_result(args: List[str], answer: str, target_window_id: int, boss: kitty.boss.Boss) -> None: def handle_result(args: List[str], answer: str, target_window_id: int, boss: Boss) -> None:
# get the kitty window into which to paste answer # get the kitty window into which to paste answer
w = boss.window_id_map.get(target_window_id) w = boss.window_id_map.get(target_window_id)
if w is not None: if w is not None:

View File

@@ -28,11 +28,17 @@ to be used can be modified using options passed to the kitten. For example::
will open the selected file in a new tab inside neovim when you press will open the selected file in a new tab inside neovim when you press
:kbd:`ctrl+g`. :kbd:`ctrl+g`.
Pressing :sc:`open_selected_hyperlink` will open hyperlinks, i.e. a URL
that has been marked as such by the program running in the terminal,
for example, by ``ls --hyperlink=auto``. You can also :doc:`customize what actions are
taken for different types of URLs <../open_actions>`.
The hints kitten is very powerful to see more detailed help on its various The hints kitten is very powerful to see more detailed help on its various
options and modes of operation, see below. You can use these options to options and modes of operation, see below. You can use these options to
create mappings in :file:`kitty.conf` to select various different text create mappings in :file:`kitty.conf` to select various different text
snippets. See :sc:`insert_selected_path` for examples. snippets. See :sc:`insert_selected_path` for examples.
Completely customizing the matching and actions of the kitten Completely customizing the matching and actions of the kitten
--------------------------------------------------------------- ---------------------------------------------------------------

View File

@@ -115,8 +115,10 @@ shell. Run ``kitty @`` with no arguments and you will be dropped into the |kitty
shell with completion for |kitty| command names and options. shell with completion for |kitty| command names and options.
You can even open the |kitty| shell inside a running |kitty| using a simple You can even open the |kitty| shell inside a running |kitty| using a simple
keyboard shortcut (:sc:`kitty_shell` by default). This has the added keyboard shortcut (:sc:`kitty_shell` by default).
advantage that you don't need to use ``allow_remote_control`` to make it work.
.. note:: This has the added advantage that you don't need to use
``allow_remote_control`` to make it work.
Allowing only some windows to control kitty Allowing only some windows to control kitty
@@ -134,6 +136,12 @@ still write to the pipes of any other program on the same computer and
therefore can control |kitty|. It can, however, be useful to block programs therefore can control |kitty|. It can, however, be useful to block programs
running on other computers (for example, over ssh) or as other users. running on other computers (for example, over ssh) or as other users.
.. note:: You dont need ``allow_remote_control`` to make this work as it is
limited to only programs running in that specific window. Be careful with
what programs you run in such windows, since they can effectively control
kitty, as if you were running with ``allow_remote_control`` turned on.
Mapping key presses to remote control commands Mapping key presses to remote control commands
-------------------------------------------------- --------------------------------------------------
@@ -144,8 +152,24 @@ you can map it in :file:`kitty.conf`. For example::
Then pressing the :kbd:`F1` key will set the active window margins to 30. Then pressing the :kbd:`F1` key will set the active window margins to 30.
The syntax for what follows :code:`remote_control` is exactly the same The syntax for what follows :code:`remote_control` is exactly the same
as the syntax for what follows :code:`kitty @` above. You do not need as the syntax for what follows :code:`kitty @` above.
to enable remote control to use these mappings.
.. note:: You do not need ``allow_remote_control`` to use these mappings,
as they are not actual remote programs, but are simply a way to resuse
the remote control infrastructure via keybings.
Broadcasting what you type to all kitty windows
--------------------------------------------------
As a simple illustration of the power of remote control, lets
have what we type sent to all open kitty windows. To do that define the
following mapping in :file:`kitty.conf`::
map F1 launch --allow-remote-control kitty +kitten broadcast
Now press, F1 and start typing, what you type will be sent to all windows,
live, as you type it.
Documentation for the remote control protocol Documentation for the remote control protocol

View File

@@ -251,12 +251,15 @@ def graphics_parser() -> None:
flag = frozenset flag = frozenset
keymap: KeymapType = { keymap: KeymapType = {
'a': ('action', flag('tTqpd')), 'a': ('action', flag('tTqpd')),
'd': ('delete_action', flag('aAiIcCpPqQxXyYzZ')), 'd': ('delete_action', flag('aAiIcCnNpPqQxXyYzZ')),
't': ('transmission_type', flag('dfts')), 't': ('transmission_type', flag('dfts')),
'o': ('compressed', flag('z')), 'o': ('compressed', flag('z')),
'f': ('format', 'uint'), 'f': ('format', 'uint'),
'm': ('more', 'uint'), 'm': ('more', 'uint'),
'i': ('id', 'uint'), 'i': ('id', 'uint'),
'I': ('image_number', 'uint'),
'p': ('placement_id', 'uint'),
'q': ('quiet', 'uint'),
'w': ('width', 'uint'), 'w': ('width', 'uint'),
'h': ('height', 'uint'), 'h': ('height', 'uint'),
'x': ('x_offset', 'uint'), 'x': ('x_offset', 'uint'),

View File

@@ -128,6 +128,9 @@ typedef struct _GLFWwindowNS
bool maximized; bool maximized;
bool retina; bool retina;
bool in_traditional_fullscreen;
bool titlebar_hidden;
unsigned long pre_full_screen_style_mask;
// Cached window properties to filter out duplicate events // Cached window properties to filter out duplicate events
int width, height; int width, height;

View File

@@ -1314,6 +1314,16 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, int which, int a, int b, int c,
glfw_window = NULL; glfw_window = NULL;
} }
- (BOOL)validateMenuItem:(NSMenuItem *)item {
if (item.action == @selector(performMiniaturize:)) return YES;
return [super validateMenuItem:item];
}
- (void)performMiniaturize:(id)sender
{
if (glfw_window && (!glfw_window->decorated || glfw_window->ns.titlebar_hidden)) [self miniaturize:self];
else [super performMiniaturize:sender];
}
- (BOOL)canBecomeKeyWindow - (BOOL)canBecomeKeyWindow
{ {
@@ -2062,18 +2072,38 @@ bool _glfwPlatformToggleFullscreen(_GLFWwindow* w, unsigned int flags) {
bool made_fullscreen = true; bool made_fullscreen = true;
bool traditional = !(flags & 1); bool traditional = !(flags & 1);
NSWindowStyleMask sm = [window styleMask]; NSWindowStyleMask sm = [window styleMask];
bool in_fullscreen = sm & NSWindowStyleMaskFullScreen;
if (traditional) { if (traditional) {
if (!(in_fullscreen)) { if (@available(macOS 10.16, *)) {
sm |= NSWindowStyleMaskBorderless | NSWindowStyleMaskFullScreen; // As of Big Turd NSWindowStyleMaskFullScreen is no longer useable
[[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationAutoHideDock]; if (!w->ns.in_traditional_fullscreen) {
w->ns.pre_full_screen_style_mask = sm;
[window setStyleMask: NSWindowStyleMaskBorderless];
[[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationAutoHideDock];
[window setFrame:[window.screen frame] display:YES];
w->ns.in_traditional_fullscreen = true;
} else {
made_fullscreen = false;
[window setStyleMask: w->ns.pre_full_screen_style_mask];
[[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationDefault];
w->ns.in_traditional_fullscreen = false;
}
// for some reason despite calling this selector, the window doesnt actually get keyboard focus till you click on it.
// presumably a bug with NSWindowStyleMaskBorderless windows
[window performSelector:@selector(makeKeyAndOrderFront:) withObject:nil afterDelay:0];
} else { } else {
made_fullscreen = false; bool in_fullscreen = sm & NSWindowStyleMaskFullScreen;
sm &= ~(NSWindowStyleMaskBorderless | NSWindowStyleMaskFullScreen); if (!(in_fullscreen)) {
[[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationDefault]; sm |= NSWindowStyleMaskBorderless | NSWindowStyleMaskFullScreen;
[[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationAutoHideDock];
} else {
made_fullscreen = false;
sm &= ~(NSWindowStyleMaskBorderless | NSWindowStyleMaskFullScreen);
[[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationDefault];
}
[window setStyleMask: sm];
} }
[window setStyleMask: sm];
} else { } else {
bool in_fullscreen = sm & NSWindowStyleMaskFullScreen;
if (in_fullscreen) made_fullscreen = false; if (in_fullscreen) made_fullscreen = false;
[window toggleFullScreen: nil]; [window toggleFullScreen: nil];
} }
@@ -2273,6 +2303,29 @@ GLFWAPI id glfwGetCocoaWindow(GLFWwindow* handle)
return window->ns.object; return window->ns.object;
} }
GLFWAPI void glfwHideCocoaTitlebar(GLFWwindow* handle, bool yes) {
@autoreleasepool {
_GLFWwindow* w = (_GLFWwindow*) handle;
NSWindow *window = w->ns.object;
w->ns.titlebar_hidden = yes;
NSButton *button;
button = [window standardWindowButton: NSWindowCloseButton];
if (button) button.hidden = yes;
button = [window standardWindowButton: NSWindowMiniaturizeButton];
if (button) button.hidden = yes;
button = [window standardWindowButton: NSWindowZoomButton];
[window setTitlebarAppearsTransparent:yes];
if (button) button.hidden = yes;
if (yes) {
[window setTitleVisibility:NSWindowTitleHidden];
[window setStyleMask: [window styleMask] | NSWindowStyleMaskFullSizeContentView];
} else {
[window setTitleVisibility:NSWindowTitleVisible];
[window setStyleMask: [window styleMask] & ~NSWindowStyleMaskFullSizeContentView];
}
} // autoreleasepool
}
GLFWAPI GLFWcocoatextinputfilterfun glfwSetCocoaTextInputFilter(GLFWwindow *handle, GLFWcocoatextinputfilterfun callback) { GLFWAPI GLFWcocoatextinputfilterfun glfwSetCocoaTextInputFilter(GLFWwindow *handle, GLFWcocoatextinputfilterfun callback) {
_GLFWwindow* window = (_GLFWwindow*) handle; _GLFWwindow* window = (_GLFWwindow*) handle;
_GLFW_REQUIRE_INIT_OR_RETURN(nil); _GLFW_REQUIRE_INIT_OR_RETURN(nil);

View File

@@ -199,6 +199,7 @@ def generate_wrappers(glfw_header: str) -> None:
functions.append(Function(decl)) functions.append(Function(decl))
for line in '''\ for line in '''\
void* glfwGetCocoaWindow(GLFWwindow* window) void* glfwGetCocoaWindow(GLFWwindow* window)
void glfwHideCocoaTitlebar(GLFWwindow* window, bool yes)
void* glfwGetNSGLContext(GLFWwindow *window) void* glfwGetNSGLContext(GLFWwindow *window)
uint32_t glfwGetCocoaMonitor(GLFWmonitor* monitor) uint32_t glfwGetCocoaMonitor(GLFWmonitor* monitor)
GLFWcocoatextinputfilterfun glfwSetCocoaTextInputFilter(GLFWwindow* window, GLFWcocoatextinputfilterfun callback) GLFWcocoatextinputfilterfun glfwSetCocoaTextInputFilter(GLFWwindow* window, GLFWcocoatextinputfilterfun callback)

10
glfw/wl_init.c vendored
View File

@@ -445,19 +445,17 @@ static void keyboardHandleKey(void* data UNUSED,
_glfw.wl.serial = serial; _glfw.wl.serial = serial;
glfw_xkb_handle_key_event(window, &_glfw.wl.xkb, key, action); glfw_xkb_handle_key_event(window, &_glfw.wl.xkb, key, action);
bool repeatable = false;
_glfw.wl.keyRepeatInfo.key = 0;
if (action == GLFW_PRESS && _glfw.wl.keyboardRepeatRate > 0 && glfw_xkb_should_repeat(&_glfw.wl.xkb, key)) if (action == GLFW_PRESS && _glfw.wl.keyboardRepeatRate > 0 && glfw_xkb_should_repeat(&_glfw.wl.xkb, key))
{ {
_glfw.wl.keyRepeatInfo.key = key; _glfw.wl.keyRepeatInfo.key = key;
repeatable = true;
_glfw.wl.keyRepeatInfo.keyboardFocus = window; _glfw.wl.keyRepeatInfo.keyboardFocus = window;
}
if (repeatable) {
changeTimerInterval(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, _glfw.wl.keyboardRepeatDelay); changeTimerInterval(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, _glfw.wl.keyboardRepeatDelay);
toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, 1);
} else if (action == GLFW_RELEASE && key == _glfw.wl.keyRepeatInfo.key) {
_glfw.wl.keyRepeatInfo.key = 0;
toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, 0);
} }
toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, repeatable ? 1 : 0);
} }
static void keyboardHandleModifiers(void* data UNUSED, static void keyboardHandleModifiers(void* data UNUSED,

View File

114
kittens/broadcast/main.py Normal file
View File

@@ -0,0 +1,114 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
import sys
from base64 import standard_b64encode
from gettext import gettext as _
from typing import Any, Dict, List, Optional, Tuple
from kitty.cli import parse_args
from kitty.cli_stub import BroadcastCLIOptions
from kitty.key_encoding import RELEASE, encode_key_event, key_defs as K
from kitty.rc.base import MATCH_TAB_OPTION, MATCH_WINDOW_OPTION
from kitty.remote_control import create_basic_command, encode_send
from kitty.typing import KeyEventType, ScreenSize
from ..tui.handler import Handler
from ..tui.line_edit import LineEdit
from ..tui.loop import Loop
from ..tui.operations import RESTORE_CURSOR, SAVE_CURSOR, styled
class Broadcast(Handler):
def __init__(self, opts: BroadcastCLIOptions, initial_strings: List[str]) -> None:
self.opts = opts
self.initial_strings = initial_strings
self.payload = {'exclude_active': True, 'data': '', 'match': opts.match_tab, 'match_tab': opts.match_tab}
self.line_edit = LineEdit()
if not opts.match and not opts.match_tab:
self.payload['all'] = True
def initialize(self) -> None:
self.print('Type the text to broadcast below, press', styled('Ctrl+c', fg='yellow'), 'to quit:')
for x in self.initial_strings:
self.write_broadcast_text(x)
self.write(SAVE_CURSOR)
def commit_line(self) -> None:
self.write(RESTORE_CURSOR + SAVE_CURSOR)
self.cmd.clear_to_end_of_screen()
self.line_edit.write(self.write, screen_cols=self.screen_size.cols)
def on_resize(self, screen_size: ScreenSize) -> None:
super().on_resize(screen_size)
self.commit_line()
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
self.write_broadcast_text(text)
self.line_edit.on_text(text, in_bracketed_paste)
self.commit_line()
def on_interrupt(self) -> None:
self.quit_loop(0)
def on_eot(self) -> None:
self.write_broadcast_text('\x04')
def on_key(self, key_event: KeyEventType) -> None:
if self.line_edit.on_key(key_event):
self.commit_line()
if key_event.type is not RELEASE and not key_event.mods:
if key_event.key is K['ENTER']:
self.write_broadcast_text('\r')
self.print('')
self.line_edit.clear()
self.write(SAVE_CURSOR)
return
ek = encode_key_event(key_event)
self.write_broadcast_data('kitty-key:' + ek)
def write_broadcast_text(self, text: str) -> None:
self.write_broadcast_data('base64:' + standard_b64encode(text.encode('utf-8')).decode('ascii'))
def write_broadcast_data(self, data: str) -> None:
payload = self.payload.copy()
payload['data'] = data
send = create_basic_command('send-text', payload, no_response=True)
self.write(encode_send(send))
OPTIONS = (MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t')).format
help_text = 'Broadcast typed text to all kitty windows. By default text is sent to all windows, unless one of the matching options is specified'
usage = '[initial text to send ...]'
def parse_broadcast_args(args: List[str]) -> Tuple[BroadcastCLIOptions, List[str]]:
return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten broadcast', result_class=BroadcastCLIOptions)
def main(args: List[str]) -> Optional[Dict[str, Any]]:
try:
opts, items = parse_broadcast_args(args[1:])
except SystemExit as e:
if e.code != 0:
print(e.args[0], file=sys.stderr)
input(_('Press Enter to quit'))
return None
print('Type text to be broadcast below, Ctrl-C to quit:', end='\r\n')
sys.stdout.flush()
loop = Loop()
handler = Broadcast(opts, items)
loop.loop(handler)
if __name__ == '__main__':
main(sys.argv)
elif __name__ == '__doc__':
cd = sys.cli_docs # type: ignore
cd['usage'] = usage
cd['options'] = OPTIONS
cd['help_text'] = help_text

View File

@@ -338,8 +338,8 @@ class DiffHandler(Handler):
if candidate.image.image_id == image_id: if candidate.image.image_id == image_id:
q = self.xpos_for_image(row, candidate, is_left) q = self.xpos_for_image(row, candidate, is_left)
if q is not None: if q is not None:
pl['x'] = q[0] pl.x = q[0]
pl['y'] = row pl.y = row
return True return True
return False return False

View File

@@ -120,7 +120,7 @@ class Hints(Handler):
self.all_marks = all_marks self.all_marks = all_marks
self.ignore_mark_indices: Set[int] = set() self.ignore_mark_indices: Set[int] = set()
self.args = args self.args = args
self.window_title = _('Choose URL') if args.type == 'url' else _('Choose text') self.window_title = args.window_title or (_('Choose URL') if args.type == 'url' else _('Choose text'))
self.multiple = args.multiple self.multiple = args.multiple
self.match_suffix = self.get_match_suffix(args) self.match_suffix = self.get_match_suffix(args)
self.chosen: List[Mark] = [] self.chosen: List[Mark] = []
@@ -519,7 +519,7 @@ The type of text to search for. A value of :code:`linenum` is special, it looks
for error messages using the pattern specified with :option:`--regex`, which for error messages using the pattern specified with :option:`--regex`, which
must have the named groups, :code:`path` and :code:`line`. If not specified, must have the named groups, :code:`path` and :code:`line`. If not specified,
will look for :code:`path:line`. The :option:`--linenum-action` option will look for :code:`path:line`. The :option:`--linenum-action` option
controls what to do with the selected error message, other options are ignored. controls where to display the selected error message, other options are ignored.
--regex --regex
@@ -539,13 +539,13 @@ the form key=value.
default=self default=self
type=choice type=choice
choices=self,window,tab,os_window,background choices=self,window,tab,os_window,background
Where to perform the action on matched errors. :code:`self` means the current
window, :code:`window` a new kitty window, :code:`tab` a new tab,
:code:`os_window` a new OS window and :code:`background` run in the background.
The action to perform on the matched errors. The actual action is whatever The action to perform on the matched errors. The actual action is whatever
arguments are provided to the kitten, for example: arguments are provided to the kitten, for example: :code:`kitty + kitten hints
:code:`kitty + kitten hints --type=linenum vim +{line} {path}` --type=linenum --linenum-action=tab vim +{line} {path}` will open the matched
will open the matched path at the matched line number in vim. This option path at the matched line number in vim in a new kitty tab.
controls where the action is executed: :code:`self` means the current window,
:code:`window` a new kitty window, :code:`tab` a new tab, :code:`os_window`
a new OS window and :code:`background` run in the background.
--url-prefixes --url-prefixes
@@ -634,6 +634,9 @@ on selected matches. See https://sw.kovidgoyal.net/kitty/kittens/hints.html
for details. You can also specify absolute paths to load the script from elsewhere. for details. You can also specify absolute paths to load the script from elsewhere.
--window-title
The window title for the hints window, default title is selected based on
the type of text being hinted.
'''.format( '''.format(
default_regex=DEFAULT_REGEX, default_regex=DEFAULT_REGEX,
line='{{line}}', path='{{path}}' line='{{line}}', path='{{path}}'

View File

@@ -15,8 +15,7 @@ from typing import (
) )
from kitty.typing import ( from kitty.typing import (
CompletedProcess, GRT_a, GRT_d, GRT_f, GRT_m, GRT_o, GRT_t, HandlerType, CompletedProcess, GRT_a, GRT_d, GRT_f, GRT_m, GRT_o, GRT_t, HandlerType
TypedDict
) )
from kitty.utils import ScreenSize, fit_image from kitty.utils import ScreenSize, fit_image
@@ -136,6 +135,7 @@ class GraphicsCommand:
S: int = 0 # size of data to read from file S: int = 0 # size of data to read from file
O: int = 0 # offset of data to read from file O: int = 0 # offset of data to read from file
i: int = 0 # image id i: int = 0 # image id
p: int = 0 # placement id
o: Optional[GRT_o] = None # type of compression o: Optional[GRT_o] = None # type of compression
m: GRT_m = 0 # 0 or 1 whether there is more chunked data m: GRT_m = 0 # 0 or 1 whether there is more chunked data
x: int = 0 # left edge of image area to display x: int = 0 # left edge of image area to display
@@ -173,10 +173,15 @@ class GraphicsCommand:
setattr(self, k, defval) setattr(self, k, defval)
class Placement(TypedDict): class Placement:
cmd: GraphicsCommand cmd: GraphicsCommand
x: int x: int = 0
y: int y: int = 0
def __init__(self, cmd: GraphicsCommand, x: int = 0, y: int = 0):
self.cmd = cmd
self.x = x
self.y = y
class ImageManager: class ImageManager:
@@ -263,8 +268,8 @@ class ImageManager:
skey = self.image_id_to_converted_data[image_id] skey = self.image_id_to_converted_data[image_id]
self.transmit_image(image_data, image_id, *skey) self.transmit_image(image_data, image_id, *skey)
with cursor(self.handler.write): with cursor(self.handler.write):
self.handler.cmd.set_cursor_position(pl['x'], pl['y']) self.handler.cmd.set_cursor_position(pl.x, pl.y)
self.handler.cmd.gr_command(pl['cmd']) self.handler.cmd.gr_command(pl.cmd)
def send_image(self, path: str, max_cols: Optional[int] = None, max_rows: Optional[int] = None, scale_up: bool = False) -> SentImageKey: def send_image(self, path: str, max_cols: Optional[int] = None, max_rows: Optional[int] = None, scale_up: bool = False) -> SentImageKey:
path = os.path.abspath(path) path = os.path.abspath(path)
@@ -316,7 +321,7 @@ class ImageManager:
gc.i = image_id gc.i = image_id
if src_rect is not None: if src_rect is not None:
gc.x, gc.y, gc.w, gc.h = map(int, src_rect) gc.x, gc.y, gc.w, gc.h = map(int, src_rect)
self.placements_in_flight[image_id].append({'cmd': gc, 'x': x, 'y': y}) self.placements_in_flight[image_id].append(Placement(gc, x, y))
with cursor(self.handler.write): with cursor(self.handler.write):
self.handler.cmd.set_cursor_position(x, y) self.handler.cmd.set_cursor_position(x, y)
self.handler.cmd.gr_command(gc) self.handler.cmd.gr_command(gc)

View File

@@ -7,6 +7,8 @@ from typing import Callable, Tuple
from kitty.fast_data_types import truncate_point_for_length, wcswidth from kitty.fast_data_types import truncate_point_for_length, wcswidth
from kitty.key_encoding import RELEASE, KeyEvent, key_defs as K from kitty.key_encoding import RELEASE, KeyEvent, key_defs as K
from .operations import RESTORE_CURSOR, SAVE_CURSOR, move_cursor_by
HOME = K['HOME'] HOME = K['HOME']
END = K['END'] END = K['END']
BACKSPACE = K['BACKSPACE'] BACKSPACE = K['BACKSPACE']
@@ -31,13 +33,28 @@ class LineEdit:
before, after = self.current_input[:x], self.current_input[x:] before, after = self.current_input[:x], self.current_input[x:]
return before, after return before, after
def write(self, write: Callable[[str], None], prompt: str = '') -> None: def write(self, write: Callable[[str], None], prompt: str = '', screen_cols: int = 0) -> None:
if self.pending_bell: if self.pending_bell:
write('\a') write('\a')
self.pending_bell = False self.pending_bell = False
write(prompt) text = prompt + self.current_input
write(self.current_input) cursor_pos = self.cursor_pos + wcswidth(prompt)
write('\r\x1b[{}C'.format(self.cursor_pos + wcswidth(prompt))) if screen_cols:
write(SAVE_CURSOR + text + RESTORE_CURSOR)
used_lines, last_line_cursor_pos = divmod(cursor_pos, screen_cols)
if used_lines == 0:
if last_line_cursor_pos:
write(move_cursor_by(last_line_cursor_pos, 'right'))
else:
if used_lines:
write(move_cursor_by(used_lines, 'down'))
if last_line_cursor_pos:
write(move_cursor_by(last_line_cursor_pos, 'right'))
else:
write(text)
write('\r')
if cursor_pos:
write(move_cursor_by(cursor_pos, 'right'))
def add_text(self, text: str) -> None: def add_text(self, text: str) -> None:
if self.current_input: if self.current_input:

View File

@@ -181,11 +181,14 @@ class SignalManager:
signal.SIGWINCH, signal.SIGINT, signal.SIGTERM))) signal.SIGWINCH, signal.SIGINT, signal.SIGTERM)))
sanitize_bracketed_paste: str = '[\x03\x04\x0e\x0f\r\x07\x7f\x8d\x8e\x8f\x90\x9b\x9d\x9e\x9f]'
class Loop: class Loop:
def __init__( def __init__(
self, self,
sanitize_bracketed_paste: str = '[\x03\x04\x0e\x0f\r\x07\x7f\x8d\x8e\x8f\x90\x9b\x9d\x9e\x9f]', sanitize_bracketed_paste: str = sanitize_bracketed_paste,
optional_actions: int = termios.TCSADRAIN optional_actions: int = termios.TCSADRAIN
): ):
if is_macos: if is_macos:
@@ -299,7 +302,7 @@ class Loop:
def _on_apc(self, apc: str) -> None: def _on_apc(self, apc: str) -> None:
if apc.startswith('K'): if apc.startswith('K'):
try: try:
k = decode_key_event(apc) k = decode_key_event(apc[1:])
except Exception: except Exception:
pass pass
else: else:
@@ -321,13 +324,17 @@ class Loop:
self.write_buf[self.iov_limit - 1] = b''.join(self.write_buf[self.iov_limit - 1:]) self.write_buf[self.iov_limit - 1] = b''.join(self.write_buf[self.iov_limit - 1:])
del self.write_buf[self.iov_limit:] del self.write_buf[self.iov_limit:]
sizes = tuple(map(len, self.write_buf)) sizes = tuple(map(len, self.write_buf))
try: total_size = sum(sizes)
written = os.writev(fd, self.write_buf) if total_size:
except BlockingIOError: try:
return written = os.writev(fd, self.write_buf)
if not written: except BlockingIOError:
raise EOFError('The output stream is closed') return
if written >= sum(sizes): if not written:
raise EOFError('The output stream is closed')
else:
written = 0
if written >= total_size:
self.write_buf: List[bytes] = [] self.write_buf: List[bytes] = []
self.asycio_loop.remove_writer(fd) self.asycio_loop.remove_writer(fd)
self.waiting_for_writes = False self.waiting_for_writes = False

View File

@@ -68,6 +68,11 @@ def clear_screen() -> str:
return '\033[H\033[2J' return '\033[H\033[2J'
@cmd
def clear_to_end_of_screen() -> str:
return '\033[J'
@cmd @cmd
def clear_to_eol() -> str: def clear_to_eol() -> str:
return '\033[K' return '\033[K'
@@ -108,6 +113,12 @@ def set_cursor_position(x: int, y: int) -> str: # (0, 0) is top left
return '\033[{};{}H'.format(y + 1, x + 1) return '\033[{};{}H'.format(y + 1, x + 1)
@cmd
def move_cursor_by(amt: int, direction: str) -> str:
suffix = {'up': 'A', 'down': 'B', 'right': 'C', 'left': 'D'}[direction]
return f'\033[{amt}{suffix}'
@cmd @cmd
def set_cursor_shape(shape: str = 'block', blink: bool = True) -> str: def set_cursor_shape(shape: str = 'block', blink: bool = True) -> str:
val = {'block': 1, 'underline': 3, 'bar': 5}.get(shape, 1) val = {'block': 1, 'underline': 3, 'bar': 5}.get(shape, 1)
@@ -253,7 +264,7 @@ def init_state(alternate_screen: bool = True) -> str:
ans = ( ans = (
S7C1T + SAVE_CURSOR + SAVE_PRIVATE_MODE_VALUES + reset_mode('LNM') + S7C1T + SAVE_CURSOR + SAVE_PRIVATE_MODE_VALUES + reset_mode('LNM') +
reset_mode('IRM') + reset_mode('DECKM') + reset_mode('DECSCNM') + reset_mode('IRM') + reset_mode('DECKM') + reset_mode('DECSCNM') +
set_mode('DECARM') + reset_mode('DECOM') + set_mode('DECAWM') + set_mode('DECARM') + set_mode('DECAWM') +
set_mode('DECTCEM') + reset_mode('MOUSE_BUTTON_TRACKING') + set_mode('DECTCEM') + reset_mode('MOUSE_BUTTON_TRACKING') +
reset_mode('MOUSE_MOTION_TRACKING') + reset_mode('MOUSE_MOVE_TRACKING') + reset_mode('MOUSE_MOTION_TRACKING') + reset_mode('MOUSE_MOVE_TRACKING') +
reset_mode('FOCUS_TRACKING') + reset_mode('MOUSE_UTF8_MODE') + reset_mode('FOCUS_TRACKING') + reset_mode('MOUSE_UTF8_MODE') +
@@ -263,7 +274,7 @@ def init_state(alternate_screen: bool = True) -> str:
'\033[*x' # reset DECSACE to default region select '\033[*x' # reset DECSACE to default region select
) )
if alternate_screen: if alternate_screen:
ans += set_mode('ALTERNATE_SCREEN') ans += set_mode('ALTERNATE_SCREEN') + reset_mode('DECOM')
ans += clear_screen() ans += clear_screen()
return ans return ans

View File

@@ -10,7 +10,8 @@ from contextlib import suppress
from functools import partial from functools import partial
from gettext import gettext as _ from gettext import gettext as _
from typing import ( from typing import (
Any, Callable, Dict, Generator, Iterable, List, Optional, Tuple, Union Any, Callable, Dict, Generator, Iterable, List, Optional, Tuple, Union,
cast
) )
from weakref import WeakValueDictionary from weakref import WeakValueDictionary
@@ -363,9 +364,17 @@ class Boss:
c = command_for_name(cmd) c = command_for_name(cmd)
opts, items = parse_subcommand_cli(c, items) opts, items = parse_subcommand_cli(c, items)
payload = c.message_to_kitty(global_opts, opts, items) payload = c.message_to_kitty(global_opts, opts, items)
c.response_from_kitty(self, self.active_window, PayloadGetter(c, payload if isinstance(payload, dict) else {})) import types
except (Exception, SystemExit) as e: if isinstance(cast(types.GeneratorType, payload), types.GeneratorType):
self.show_error(_('remote_control mapping failed'), str(e)) payloads = cast(types.GeneratorType, payload)
for x in payloads:
c.response_from_kitty(self, self.active_window, PayloadGetter(c, x if isinstance(x, dict) else {}))
else:
c.response_from_kitty(self, self.active_window, PayloadGetter(c, payload if isinstance(payload, dict) else {}))
except (Exception, SystemExit):
import traceback
tb = traceback.format_exc()
self.show_error(_('remote_control mapping failed'), tb)
def peer_message_received(self, msg_bytes: bytes) -> Optional[bytes]: def peer_message_received(self, msg_bytes: bytes) -> Optional[bytes]:
cmd_prefix = b'\x1bP@kitty-cmd' cmd_prefix = b'\x1bP@kitty-cmd'
@@ -1459,15 +1468,45 @@ class Boss:
self._cleanup_tab_after_window_removal(tab) self._cleanup_tab_after_window_removal(tab)
target_tab.make_active() target_tab.make_active()
def select_tab(self) -> None:
title = 'Choose a tab to switch to'
lines = [title, '']
fmt = ': {1}'
tab_id_map: Dict[int, Optional[Union[str, int]]] = {}
current_tab = self.active_tab
done_tab_id: Optional[Union[str, int]] = None
for i, tab in enumerate(self.all_tabs):
if tab is not current_tab:
tab_id_map[len(tab_id_map)] = tab.id
lines.append(fmt.format(i + 1, tab.title))
def done(data: Dict[str, Any], target_window_id: int, self: Boss) -> None:
nonlocal done_tab_id
done_tab_id = tab_id_map[int(data['groupdicts'][0]['index'])]
def done2(target_window_id: int, self: Boss) -> None:
tab_id = done_tab_id
if tab_id is not None:
for i, tab in enumerate(self.all_tabs):
if tab.id == tab_id:
self.set_active_tab(tab)
break
self._run_kitten(
'hints', args=(
'--ascending', '--customize-processing=::import::kitty.choose_entry',
r'--regex=(?m)^:\s+.+$', '--window-title', title,
), input_data='\r\n'.join(lines).encode('utf-8'), custom_callback=done, action_on_removal=done2
)
def detach_window(self, *args: str) -> None: def detach_window(self, *args: str) -> None:
if not args or args[0] == 'new': if not args or args[0] == 'new':
return self._move_window_to(target_os_window_id='new') return self._move_window_to(target_os_window_id='new')
if args[0] == 'new-tab': if args[0] == 'new-tab':
return self._move_window_to(target_tab_id='new') return self._move_window_to(target_tab_id='new')
lines = [ title = 'Choose a tab to move the window to'
'Choose a tab to move the window to', lines = [title, '']
''
]
fmt = ': {1}' fmt = ': {1}'
tab_id_map: Dict[int, Optional[Union[str, int]]] = {} tab_id_map: Dict[int, Optional[Union[str, int]]] = {}
current_tab = self.active_tab current_tab = self.active_tab
@@ -1503,7 +1542,7 @@ class Boss:
self._run_kitten( self._run_kitten(
'hints', args=( 'hints', args=(
'--ascending', '--customize-processing=::import::kitty.choose_entry', '--ascending', '--customize-processing=::import::kitty.choose_entry',
r'--regex=(?m)^:\s+.+$', r'--regex=(?m)^:\s+.+$', '--window-title', title,
), input_data='\r\n'.join(lines).encode('utf-8'), custom_callback=done, action_on_removal=done2 ), input_data='\r\n'.join(lines).encode('utf-8'), custom_callback=done, action_on_removal=done2
) )
@@ -1511,10 +1550,8 @@ class Boss:
if not args or args[0] == 'new': if not args or args[0] == 'new':
return self._move_tab_to() return self._move_tab_to()
lines = [ title = 'Choose an OS window to move the tab to'
'Choose an OS window to move the tab to', lines = [title, '']
''
]
fmt = ': {1}' fmt = ': {1}'
os_window_id_map: Dict[int, Optional[int]] = {} os_window_id_map: Dict[int, Optional[int]] = {}
current_os_window = getattr(self.active_tab, 'os_window_id', 0) current_os_window = getattr(self.active_tab, 'os_window_id', 0)
@@ -1551,7 +1588,7 @@ class Boss:
self._run_kitten( self._run_kitten(
'hints', args=( 'hints', args=(
'--ascending', '--customize-processing=::import::kitty.choose_entry', '--ascending', '--customize-processing=::import::kitty.choose_entry',
r'--regex=(?m)^:\s+.+$', r'--regex=(?m)^:\s+.+$', '--window-title', title,
), input_data='\r\n'.join(lines).encode('utf-8'), custom_callback=done, action_on_removal=done2 ), input_data='\r\n'.join(lines).encode('utf-8'), custom_callback=done, action_on_removal=done2
) )

View File

@@ -156,9 +156,12 @@ def default_env() -> Dict[str, str]:
def set_default_env(val: Optional[Dict[str, str]] = None) -> None: def set_default_env(val: Optional[Dict[str, str]] = None) -> None:
env = process_env().copy() env = process_env().copy()
has_lctype = False
if val: if val:
has_lctype = 'LC_CTYPE' in val
env.update(val) env.update(val)
setattr(default_env, 'env', env) setattr(default_env, 'env', env)
setattr(default_env, 'lc_ctype_set_by_user', has_lctype)
def openpty() -> Tuple[int, int]: def openpty() -> Tuple[int, int]:
@@ -210,7 +213,8 @@ class Child:
env: Optional[Dict[str, str]] = getattr(self, '_final_env', None) env: Optional[Dict[str, str]] = getattr(self, '_final_env', None)
if env is None: if env is None:
env = self._final_env = default_env().copy() env = self._final_env = default_env().copy()
if is_macos and env.get('LC_CTYPE') == 'UTF-8' and not sys._xoptions.get('lc_ctype_before_python'): if is_macos and env.get('LC_CTYPE') == 'UTF-8' and not sys._xoptions.get(
'lc_ctype_before_python') and not getattr(default_env, 'lc_ctype_set_by_user', False):
del env['LC_CTYPE'] del env['LC_CTYPE']
env.update(self.env) env.update(self.env)
env['TERM'] = self.opts.term env['TERM'] = self.opts.term

View File

@@ -13,7 +13,7 @@ class CLIOptions:
LaunchCLIOptions = AskCLIOptions = ClipboardCLIOptions = DiffCLIOptions = CLIOptions LaunchCLIOptions = AskCLIOptions = ClipboardCLIOptions = DiffCLIOptions = CLIOptions
HintsCLIOptions = IcatCLIOptions = PanelCLIOptions = ResizeCLIOptions = CLIOptions HintsCLIOptions = IcatCLIOptions = PanelCLIOptions = ResizeCLIOptions = CLIOptions
ErrorCLIOptions = UnicodeCLIOptions = RCOptions = RemoteFileCLIOptions = CLIOptions ErrorCLIOptions = UnicodeCLIOptions = RCOptions = RemoteFileCLIOptions = CLIOptions
QueryTerminalCLIOptions = CLIOptions QueryTerminalCLIOptions = BroadcastCLIOptions = CLIOptions
def generate_stub() -> None: def generate_stub() -> None:
@@ -48,6 +48,9 @@ def generate_stub() -> None:
from kittens.hints.main import OPTIONS from kittens.hints.main import OPTIONS
do(OPTIONS(), 'HintsCLIOptions') do(OPTIONS(), 'HintsCLIOptions')
from kittens.broadcast.main import OPTIONS
do(OPTIONS(), 'BroadcastCLIOptions')
from kittens.icat.main import options_spec from kittens.icat.main import options_spec
do(options_spec(), 'IcatCLIOptions') do(options_spec(), 'IcatCLIOptions')

View File

@@ -11,6 +11,7 @@
import sys import sys
from contextlib import suppress from contextlib import suppress
from typing import Any from typing import Any
from functools import partial
CSI = '\033[' CSI = '\033['
@@ -50,10 +51,26 @@ def screen_cursor_forward(amt: int) -> None:
write(CSI + '%sC' % amt) write(CSI + '%sC' % amt)
def screen_save_cursor() -> None:
write('\x1b7')
def screen_restore_cursor() -> None:
write('\x1b8')
def screen_cursor_back1(amt: int) -> None: def screen_cursor_back1(amt: int) -> None:
write(CSI + '%sD' % amt) write(CSI + '%sD' % amt)
def screen_save_modes() -> None:
write(CSI + '?s')
def screen_restore_modes() -> None:
write(CSI + '?r')
def screen_designate_charset(which: int, to: int) -> None: def screen_designate_charset(which: int, to: int) -> None:
w = '()'[int(which)] w = '()'[int(which)]
t = chr(int(to)) t = chr(int(to))
@@ -161,6 +178,14 @@ def report_device_attributes(mode: int, char: int) -> None:
write(CSI + x + 'c') write(CSI + x + 'c')
def screen_decsace(mode: int) -> None:
write(CSI + str(mode) + '*x')
def screen_set_8bit_controls(mode: int) -> None:
write('\x1b ' + ('G' if mode else 'F'))
def write_osc(code: int, string: str = '') -> None: def write_osc(code: int, string: str = '') -> None:
if string: if string:
string = ';' + string string = ';' + string
@@ -168,6 +193,8 @@ def write_osc(code: int, string: str = '') -> None:
set_dynamic_color = set_color_table_color = write_osc set_dynamic_color = set_color_table_color = write_osc
screen_push_dynamic_colors = partial(write_osc, 30001)
screen_pop_dynamic_colors = partial(write_osc, 30101)
def replay(raw: str) -> None: def replay(raw: str) -> None:

View File

@@ -615,30 +615,6 @@ cocoa_hide_window_title(void *w)
} // autoreleasepool } // autoreleasepool
} }
void
cocoa_hide_titlebar(void *w)
{
@autoreleasepool {
cocoa_hide_window_title(w);
NSWindow *window = (NSWindow*)w;
NSButton *button;
button = [window standardWindowButton: NSWindowCloseButton];
if (button) button.hidden = true;
button = [window standardWindowButton: NSWindowMiniaturizeButton];
if (button) button.hidden = true;
button = [window standardWindowButton: NSWindowZoomButton];
if (button) button.hidden = true;
[window setTitlebarAppearsTransparent:YES];
[window setStyleMask:
[window styleMask] | NSWindowStyleMaskFullSizeContentView];
} // autoreleasepool
}
void void
cocoa_system_beep(void) { cocoa_system_beep(void) {
NSBeep(); NSBeep();

View File

@@ -21,6 +21,7 @@ from .conf.utils import (
) )
from .config_data import all_options, parse_mods, type_convert from .config_data import all_options, parse_mods, type_convert
from .constants import cache_dir, defconf, is_macos from .constants import cache_dir, defconf, is_macos
from .fonts import FontFeature
from .key_names import get_key_name_lookup, key_name_aliases from .key_names import get_key_name_lookup, key_name_aliases
from .options_stub import Options as OptionsStub from .options_stub import Options as OptionsStub
from .typing import TypedDict from .typing import TypedDict
@@ -524,14 +525,6 @@ def handle_symbol_map(key: str, val: str, ans: Dict[str, Any]) -> None:
ans['symbol_map'].update(parse_symbol_map(val)) ans['symbol_map'].update(parse_symbol_map(val))
class FontFeature(str):
def __new__(cls, name: str, parsed: bytes) -> 'FontFeature':
ans: FontFeature = str.__new__(cls, name) # type: ignore
ans.parsed = parsed # type: ignore
return ans
@special_handler @special_handler
def handle_font_features(key: str, val: str, ans: Dict[str, Any]) -> None: def handle_font_features(key: str, val: str, ans: Dict[str, Any]) -> None:
if val != 'none': if val != 'none':

View File

@@ -325,6 +325,9 @@ Note that this code is indexed by PostScript name, and not the font
family. This allows you to define very precise feature settings; e.g. you can family. This allows you to define very precise feature settings; e.g. you can
disable a feature in the italic font but not in the regular font. disable a feature in the italic font but not in the regular font.
On Linux, these are read from the FontConfig database first and then this,
setting is applied, so they can be configured in a single, central place.
To get the PostScript name for a font, use :code:`kitty + list-fonts --psnames`: To get the PostScript name for a font, use :code:`kitty + list-fonts --psnames`:
.. code-block:: sh .. code-block:: sh
@@ -520,6 +523,11 @@ def url_prefixes(x: str) -> Tuple[str, ...]:
o('url_prefixes', 'http https file ftp', option_type=url_prefixes, long_text=_(''' o('url_prefixes', 'http https file ftp', option_type=url_prefixes, long_text=_('''
The set of URL prefixes to look for when detecting a URL under the mouse cursor.''')) The set of URL prefixes to look for when detecting a URL under the mouse cursor.'''))
o('detect_urls', True, long_text=_('''
Detect URLs under the mouse. Detected URLs are highlighted
with an underline and the mouse cursor becomes a hand over them.
Even if this option is disabled, URLs are still clickable.'''))
def copy_on_select(raw: str) -> str: def copy_on_select(raw: str) -> str:
q = raw.lower() q = raw.lower()
@@ -862,6 +870,8 @@ o('tab_bar_style', 'fade', option_type=choices('fade', 'separator', 'powerline',
The tab bar style, can be one of: :code:`fade`, :code:`separator`, :code:`powerline`, or :code:`hidden`. The tab bar style, can be one of: :code:`fade`, :code:`separator`, :code:`powerline`, or :code:`hidden`.
In the fade style, each tab's edges fade into the background color, in the separator style, tabs are In the fade style, each tab's edges fade into the background color, in the separator style, tabs are
separated by a configurable separator, and the powerline shows the tabs as a continuous line. separated by a configurable separator, and the powerline shows the tabs as a continuous line.
If you use the hidden style, you might want to create a mapping for the :code:`select_tab` action which
presents you with a list of tabs and allows for easy switching to a tab.
''')) '''))
@@ -873,11 +883,12 @@ o('tab_bar_min_tabs', 2, option_type=tab_bar_min_tabs, long_text=_('''
The minimum number of tabs that must exist before the tab bar is shown The minimum number of tabs that must exist before the tab bar is shown
''')) '''))
o('tab_switch_strategy', 'previous', option_type=choices('previous', 'left', 'last'), long_text=_(''' o('tab_switch_strategy', 'previous', option_type=choices('previous', 'left', 'right', 'last'), long_text=_('''
The algorithm to use when switching to a tab when the current tab is closed. The algorithm to use when switching to a tab when the current tab is closed.
The default of :code:`previous` will switch to the last used tab. A value of The default of :code:`previous` will switch to the last used tab. A value of
:code:`left` will switch to the tab to the left of the closed tab. A value :code:`left` will switch to the tab to the left of the closed tab. A value
of :code:`last` will switch to the right-most tab. of :code:`right` will switch to the tab to the right of the closed tab.
A value of :code:`last` will switch to the right-most tab.
''')) '''))
@@ -932,6 +943,10 @@ layout name and :code:`{num_windows}` for the number of windows
in the tab. Note that formatting is done by Python's string formatting in the tab. Note that formatting is done by Python's string formatting
machinery, so you can use, for instance, :code:`{layout_name[:2].upper()}` to machinery, so you can use, for instance, :code:`{layout_name[:2].upper()}` to
show only the first two letters of the layout name, upper-cased. show only the first two letters of the layout name, upper-cased.
If you want to style the text, you can use styling directives, for example:
:code:`{fmt.fg.red}red{fmt.fg.default}normal{fmt.bg._00FF00}green bg{fmt.bg.normal}`.
Similarly, for bold and italic:
:code:`{fmt.bold}bold{fmt.nobold}normal{fmt.italic}italic{fmt.noitalic}`.
''')) '''))
o('active_tab_title_template', 'none', option_type=active_tab_title_template, long_text=_(''' o('active_tab_title_template', 'none', option_type=active_tab_title_template, long_text=_('''
Template to use for active tabs, if not specified falls back Template to use for active tabs, if not specified falls back

View File

@@ -20,7 +20,7 @@ class Version(NamedTuple):
appname: str = 'kitty' appname: str = 'kitty'
version: Version = Version(0, 19, 2) version: Version = Version(0, 19, 3)
str_version: str = '.'.join(map(str, version)) str_version: str = '.'.join(map(str, version))
_plat = sys.platform.lower() _plat = sys.platform.lower()
is_macos: bool = 'darwin' in _plat is_macos: bool = 'darwin' in _plat

View File

@@ -193,9 +193,8 @@ typedef struct {
} HistoryBufSegment; } HistoryBufSegment;
typedef struct { typedef struct {
uint8_t *buffer; void *ringbuf;
size_t buffer_size, max_sz; size_t maximum_size;
size_t start, length;
bool rewrap_needed; bool rewrap_needed;
} PagerHistoryBuf; } PagerHistoryBuf;

View File

@@ -410,6 +410,7 @@ class FontConfigPattern(TypedDict):
postscript_name: str postscript_name: str
style: str style: str
spacing: str spacing: str
fontfeatures: List[str]
weight: int weight: int
width: int width: int
slant: int slant: int
@@ -441,6 +442,12 @@ def fc_match(
pass pass
def fc_match_postscript_name(
postscript_name: str
) -> FontConfigPattern:
pass
class CoreTextFont(TypedDict): class CoreTextFont(TypedDict):
path: str path: str
postscript_name: str postscript_name: str
@@ -1002,6 +1009,9 @@ class Screen:
def draw(self, text: str) -> None: def draw(self, text: str) -> None:
pass pass
def apply_sgr(self, text: str) -> None:
pass
def copy_colors_from(self, other: 'Screen') -> None: def copy_colors_from(self, other: 'Screen') -> None:
pass pass

View File

@@ -27,17 +27,33 @@ pyspacing(int val) {
static inline PyObject* static inline PyObject*
pattern_as_dict(FcPattern *pat) { pattern_as_dict(FcPattern *pat) {
PyObject *ans = PyDict_New(); PyObject *ans = PyDict_New(), *p = NULL, *list = NULL;
if (ans == NULL) return NULL; if (ans == NULL) return NULL;
#define PS(x) PyUnicode_Decode((const char*)x, strlen((const char*)x), "UTF-8", "replace") #define PS(x) PyUnicode_Decode((const char*)x, strlen((const char*)x), "UTF-8", "replace")
#define G(type, get, which, conv, name) { \ #define G(type, get, which, conv, name) { \
type out; PyObject *p; \ type out; \
if (get(pat, which, 0, &out) == FcResultMatch) { \ if (get(pat, which, 0, &out) == FcResultMatch) { \
p = conv(out); if (p == NULL) { Py_CLEAR(ans); return NULL; } \ p = conv(out); if (p == NULL) goto exit; \
if (PyDict_SetItemString(ans, #name, p) != 0) { Py_CLEAR(p); Py_CLEAR(ans); return NULL; } \ if (PyDict_SetItemString(ans, #name, p) != 0) goto exit; \
Py_CLEAR(p); \ Py_CLEAR(p); \
}} }}
#define L(type, get, which, conv, name) { \
type out; int n = 0; \
list = PyList_New(0); \
if (!list) goto exit; \
while (get(pat, which, n++, &out) == FcResultMatch) { \
p = conv(out); if (p == NULL) goto exit; \
if (PyList_Append(list, p) != 0) goto exit; \
Py_CLEAR(p); \
} \
if (PyDict_SetItemString(ans, #name, list) != 0) goto exit; \
Py_CLEAR(list); \
}
#define S(which, key) G(FcChar8*, FcPatternGetString, which, PS, key) #define S(which, key) G(FcChar8*, FcPatternGetString, which, PS, key)
#define LS(which, key) L(FcChar8*, FcPatternGetString, which, PS, key)
#define I(which, key) G(int, FcPatternGetInteger, which, PyLong_FromLong, key) #define I(which, key) G(int, FcPatternGetInteger, which, PyLong_FromLong, key)
#define B(which, key) G(int, FcPatternGetBool, which, pybool, key) #define B(which, key) G(int, FcPatternGetBool, which, pybool, key)
#define E(which, key, conv) G(int, FcPatternGetInteger, which, conv, key) #define E(which, key, conv) G(int, FcPatternGetInteger, which, conv, key)
@@ -46,6 +62,7 @@ pattern_as_dict(FcPattern *pat) {
S(FC_STYLE, style); S(FC_STYLE, style);
S(FC_FULLNAME, full_name); S(FC_FULLNAME, full_name);
S(FC_POSTSCRIPT_NAME, postscript_name); S(FC_POSTSCRIPT_NAME, postscript_name);
LS(FC_FONT_FEATURES, fontfeatures);
I(FC_WEIGHT, weight); I(FC_WEIGHT, weight);
I(FC_WIDTH, width) I(FC_WIDTH, width)
I(FC_SLANT, slant); I(FC_SLANT, slant);
@@ -58,6 +75,10 @@ pattern_as_dict(FcPattern *pat) {
B(FC_OUTLINE, outline); B(FC_OUTLINE, outline);
B(FC_COLOR, color); B(FC_COLOR, color);
E(FC_SPACING, spacing, pyspacing); E(FC_SPACING, spacing, pyspacing);
exit:
if (PyErr_Occurred()) Py_CLEAR(ans);
Py_CLEAR(p);
Py_CLEAR(list);
return ans; return ans;
#undef PS #undef PS
@@ -66,6 +87,8 @@ pattern_as_dict(FcPattern *pat) {
#undef B #undef B
#undef E #undef E
#undef G #undef G
#undef L
#undef LS
} }
static inline PyObject* static inline PyObject*
@@ -179,6 +202,27 @@ end:
return ans; return ans;
} }
static PyObject*
fc_match_postscript_name(PyObject UNUSED *self, PyObject *args) {
const char *postscript_name = NULL;
FcPattern *pat = NULL;
PyObject *ans = NULL;
if (!PyArg_ParseTuple(args, "s", &postscript_name)) return NULL;
if (!postscript_name || !postscript_name[0]) { PyErr_SetString(PyExc_KeyError, "postscript_name must not be empty"); return NULL; }
pat = FcPatternCreate();
if (pat == NULL) return PyErr_NoMemory();
AP(FcPatternAddString, FC_POSTSCRIPT_NAME, (const FcChar8*)postscript_name, "postscript_name");
ans = _fc_match(pat);
end:
if (pat != NULL) FcPatternDestroy(pat);
return ans;
}
PyObject* PyObject*
specialize_font_descriptor(PyObject *base_descriptor, FONTS_DATA_HANDLE fg) { specialize_font_descriptor(PyObject *base_descriptor, FONTS_DATA_HANDLE fg) {
PyObject *p = PyDict_GetItemString(base_descriptor, "path"), *ans = NULL; PyObject *p = PyDict_GetItemString(base_descriptor, "path"), *ans = NULL;
@@ -224,6 +268,7 @@ end:
static PyMethodDef module_methods[] = { static PyMethodDef module_methods[] = {
METHODB(fc_list, METH_VARARGS), METHODB(fc_list, METH_VARARGS),
METHODB(fc_match, METH_VARARGS), METHODB(fc_match, METH_VARARGS),
METHODB(fc_match_postscript_name, METH_VARARGS),
{NULL, NULL, 0, NULL} /* Sentinel */ {NULL, NULL, 0, NULL} /* Sentinel */
}; };

View File

@@ -9,3 +9,11 @@ class ListedFont(TypedDict):
full_name: str full_name: str
postscript_name: str postscript_name: str
is_monospace: bool is_monospace: bool
class FontFeature(str):
def __new__(cls, name: str, parsed: bytes) -> 'FontFeature':
ans: FontFeature = str.__new__(cls, name) # type: ignore
ans.parsed = parsed # type: ignore
return ans

View File

@@ -663,17 +663,32 @@ def smooth_mosaic(
buf[offset + x] = 255 buf[offset + x] = 255
def eight_range(size: int, which: int) -> range:
thickness = max(1, size // 8)
block = thickness * 8
if block == size:
return range(thickness * which, thickness * (which + 1))
if block > size:
start = min(which * thickness, size - thickness)
return range(start, start + thickness)
extra = size - block
thicknesses = list(repeat(thickness, 8))
for i in (3, 4, 2, 5, 6, 1, 7, 0): # ensures the thickness of first and last are least likely to be changed
if not extra:
break
extra -= 1
thicknesses[i] += 1
pos = sum(thicknesses[:which])
return range(pos, pos + thicknesses[which])
def eight_bar(buf: BufType, width: int, height: int, level: int = 1, which: int = 0, horizontal: bool = False) -> None: def eight_bar(buf: BufType, width: int, height: int, level: int = 1, which: int = 0, horizontal: bool = False) -> None:
if horizontal: if horizontal:
x_range = range(0, width) x_range = range(0, width)
thickness = max(1, height // 8) y_range = eight_range(height, which)
y_start = min(which * thickness, height - 2)
y_range = range(y_start, height if which == 7 else min(y_start + thickness, height))
else: else:
y_range = range(0, height) y_range = range(0, height)
thickness = max(1, width // 8) x_range = eight_range(width, which)
x_start = min(which * thickness, width - 2)
x_range = range(x_start, width if which == 7 else min(x_start + thickness, width))
for y in y_range: for y in y_range:
offset = y * width offset = y * width
for x in x_range: for x in x_range:
@@ -940,10 +955,9 @@ for i in range(256):
c = 0x1fb00 c = 0x1fb00
for i in range(1, 63): for i in range(1, 63):
if i in (20, 40): if i not in (21, 42):
continue box_chars[chr(c)] = [p(sextant, which=i)]
box_chars[chr(c)] = [p(sextant, which=i)] c += 1
c += 1
for i in range(1, 7): for i in range(1, 7):
box_chars[chr(0x1fb6f + i)] = [p(eight_bar, which=i)] box_chars[chr(0x1fb6f + i)] = [p(eight_bar, which=i)]

View File

@@ -50,6 +50,11 @@ def list_fonts() -> Generator[ListedFont, None, None]:
yield {'family': f, 'full_name': fn, 'postscript_name': fd['postscript_name'] or '', 'is_monospace': is_mono} yield {'family': f, 'full_name': fn, 'postscript_name': fd['postscript_name'] or '', 'is_monospace': is_mono}
def find_font_features(postscript_name: str) -> Tuple[str, ...]:
"""Not Implemented"""
return ()
def find_best_match(family: str, bold: bool = False, italic: bool = False) -> CoreTextFont: def find_best_match(family: str, bold: bool = False, italic: bool = False) -> CoreTextFont:
q = re.sub(r'\s+', ' ', family.lower()) q = re.sub(r'\s+', ' ', family.lower())
font_map = all_fonts_map() font_map = all_fonts_map()

View File

@@ -8,12 +8,14 @@ from typing import Dict, Generator, List, Optional, Tuple, cast
from kitty.fast_data_types import ( from kitty.fast_data_types import (
FC_DUAL, FC_MONO, FC_SLANT_ITALIC, FC_SLANT_ROMAN, FC_WEIGHT_BOLD, FC_DUAL, FC_MONO, FC_SLANT_ITALIC, FC_SLANT_ROMAN, FC_WEIGHT_BOLD,
FC_WEIGHT_REGULAR, FC_WIDTH_NORMAL, fc_list, fc_match as fc_match_impl FC_WEIGHT_REGULAR, FC_WIDTH_NORMAL, fc_list, fc_match as fc_match_impl,
fc_match_postscript_name, parse_font_feature
) )
from kitty.options_stub import Options from kitty.options_stub import Options
from kitty.typing import FontConfigPattern from kitty.typing import FontConfigPattern
from kitty.utils import log_error
from . import ListedFont from . import ListedFont, FontFeature
attr_map = {(False, False): 'font_family', attr_map = {(False, False): 'font_family',
(True, False): 'bold_font', (True, False): 'bold_font',
@@ -69,6 +71,24 @@ def fc_match(family: str, bold: bool, italic: bool, spacing: int = FC_MONO) -> F
return fc_match_impl(family, bold, italic, spacing) return fc_match_impl(family, bold, italic, spacing)
def find_font_features(postscript_name: str) -> Tuple[str, ...]:
pat = fc_match_postscript_name(postscript_name)
if pat.get('postscript_name') != postscript_name or 'fontfeatures' not in pat:
return ()
features = []
for feat in pat['fontfeatures']:
try:
parsed = parse_font_feature(feat)
except ValueError:
log_error('Ignoring invalid font feature: {}'.format(feat))
else:
features.append(FontFeature(feat, parsed))
return tuple(features)
def find_best_match(family: str, bold: bool = False, italic: bool = False, monospaced: bool = True) -> FontConfigPattern: def find_best_match(family: str, bold: bool = False, italic: bool = False, monospaced: bool = True) -> FontConfigPattern:
q = family_name_to_key(family) q = family_name_to_key(family)
font_map = all_fonts_map(monospaced) font_map = all_fonts_map(monospaced)

View File

@@ -25,9 +25,9 @@ from kitty.typing import CoreTextFont, FontConfigPattern
from kitty.utils import log_error from kitty.utils import log_error
if is_macos: if is_macos:
from .core_text import get_font_files as get_font_files_coretext, font_for_family as font_for_family_macos from .core_text import get_font_files as get_font_files_coretext, font_for_family as font_for_family_macos, find_font_features
else: else:
from .fontconfig import get_font_files as get_font_files_fontconfig, font_for_family as font_for_family_fontconfig from .fontconfig import get_font_files as get_font_files_fontconfig, font_for_family as font_for_family_fontconfig, find_font_features
FontObject = Union[CoreTextFont, FontConfigPattern] FontObject = Union[CoreTextFont, FontConfigPattern]
current_faces: List[Tuple[FontObject, bool, bool]] = [] current_faces: List[Tuple[FontObject, bool, bool]] = []
@@ -189,12 +189,16 @@ def set_font_family(opts: Optional[OptionsStub] = None, override_font_size: Opti
before = len(current_faces) before = len(current_faces)
sm = create_symbol_map(opts) sm = create_symbol_map(opts)
num_symbol_fonts = len(current_faces) - before num_symbol_fonts = len(current_faces) - before
font_features = {}
for face, _, _ in current_faces:
font_features[face['postscript_name']] = find_font_features(face['postscript_name'])
font_features.update(opts.font_features)
if debug_font_matching: if debug_font_matching:
dump_faces(ftypes, indices) dump_faces(ftypes, indices)
set_font_data( set_font_data(
render_box_drawing, prerender_function, descriptor_for_idx, render_box_drawing, prerender_function, descriptor_for_idx,
indices['bold'], indices['italic'], indices['bi'], num_symbol_fonts, indices['bold'], indices['italic'], indices['bi'], num_symbol_fonts,
sm, sz, opts.font_features sm, sz, font_features
) )

2
kitty/glfw-wrapper.c generated
View File

@@ -380,6 +380,8 @@ load_glfw(const char* path) {
*(void **) (&glfwGetCocoaWindow_impl) = dlsym(handle, "glfwGetCocoaWindow"); *(void **) (&glfwGetCocoaWindow_impl) = dlsym(handle, "glfwGetCocoaWindow");
*(void **) (&glfwHideCocoaTitlebar_impl) = dlsym(handle, "glfwHideCocoaTitlebar");
*(void **) (&glfwGetNSGLContext_impl) = dlsym(handle, "glfwGetNSGLContext"); *(void **) (&glfwGetNSGLContext_impl) = dlsym(handle, "glfwGetNSGLContext");
*(void **) (&glfwGetCocoaMonitor_impl) = dlsym(handle, "glfwGetCocoaMonitor"); *(void **) (&glfwGetCocoaMonitor_impl) = dlsym(handle, "glfwGetCocoaMonitor");

4
kitty/glfw-wrapper.h generated
View File

@@ -2147,6 +2147,10 @@ typedef void* (*glfwGetCocoaWindow_func)(GLFWwindow*);
GFW_EXTERN glfwGetCocoaWindow_func glfwGetCocoaWindow_impl; GFW_EXTERN glfwGetCocoaWindow_func glfwGetCocoaWindow_impl;
#define glfwGetCocoaWindow glfwGetCocoaWindow_impl #define glfwGetCocoaWindow glfwGetCocoaWindow_impl
typedef void (*glfwHideCocoaTitlebar_func)(GLFWwindow*, bool);
GFW_EXTERN glfwHideCocoaTitlebar_func glfwHideCocoaTitlebar_impl;
#define glfwHideCocoaTitlebar glfwHideCocoaTitlebar_impl
typedef void* (*glfwGetNSGLContext_func)(GLFWwindow*); typedef void* (*glfwGetNSGLContext_func)(GLFWwindow*);
GFW_EXTERN glfwGetNSGLContext_func glfwGetNSGLContext_impl; GFW_EXTERN glfwGetNSGLContext_func glfwGetNSGLContext_impl;
#define glfwGetNSGLContext glfwGetNSGLContext_impl #define glfwGetNSGLContext glfwGetNSGLContext_impl

View File

@@ -14,7 +14,6 @@ extern void cocoa_focus_window(void *w);
extern long cocoa_window_number(void *w); extern long cocoa_window_number(void *w);
extern void cocoa_create_global_menu(void); extern void cocoa_create_global_menu(void);
extern void cocoa_hide_window_title(void *w); extern void cocoa_hide_window_title(void *w);
extern void cocoa_hide_titlebar(void *w);
extern void cocoa_system_beep(void); extern void cocoa_system_beep(void);
extern void cocoa_set_activation_policy(bool); extern void cocoa_set_activation_policy(bool);
extern void cocoa_set_titlebar_color(void *w, color_type color); extern void cocoa_set_titlebar_color(void *w, color_type color);
@@ -695,7 +694,7 @@ create_os_window(PyObject UNUSED *self, PyObject *args) {
#ifdef __APPLE__ #ifdef __APPLE__
if (glfwGetCocoaWindow) { if (glfwGetCocoaWindow) {
if (OPT(hide_window_decorations) & 2) { if (OPT(hide_window_decorations) & 2) {
cocoa_hide_titlebar(glfwGetCocoaWindow(glfw_window)); glfwHideCocoaTitlebar(glfw_window, true);
} else if (!(OPT(macos_show_window_title_in) & WINDOW)) { } else if (!(OPT(macos_show_window_title_in) & WINDOW)) {
cocoa_hide_window_title(glfwGetCocoaWindow(glfw_window)); cocoa_hide_window_title(glfwGetCocoaWindow(glfw_window));
} }

View File

@@ -92,6 +92,16 @@ img_by_client_id(GraphicsManager *self, uint32_t id) {
return NULL; return NULL;
} }
static inline Image*
img_by_client_number(GraphicsManager *self, uint32_t number) {
// get the newest image with the specified number
for (size_t i = self->image_count; i-- > 0; ) {
if (self->images[i].client_number == number) return self->images + i;
}
return NULL;
}
static inline void static inline void
remove_image(GraphicsManager *self, size_t idx) { remove_image(GraphicsManager *self, size_t idx) {
free_image(self, self->images + idx); free_image(self, self->images + idx);
@@ -137,22 +147,20 @@ apply_storage_quota(GraphicsManager *self, size_t storage_limit, id_type current
if (!self->image_count) self->used_storage = 0; // sanity check if (!self->image_count) self->used_storage = 0; // sanity check
} }
static char add_response[512] = {0}; static char command_response[512] = {0};
static bool has_add_respose = false;
static inline void static inline void
set_add_response(const char *code, const char *fmt, ...) { set_command_failed_response(const char *code, const char *fmt, ...) {
va_list args; va_list args;
va_start(args, fmt); va_start(args, fmt);
size_t sz = sizeof(add_response)/sizeof(add_response[0]); const size_t sz = sizeof(command_response)/sizeof(command_response[0]);
int num = snprintf(add_response, sz, "%s:", code); const int num = snprintf(command_response, sz, "%s:", code);
vsnprintf(add_response + num, sz - num, fmt, args); vsnprintf(command_response + num, sz - num, fmt, args);
va_end(args); va_end(args);
has_add_respose = true;
} }
// Decode formats {{{ // Decode formats {{{
#define ABRT(code, ...) { set_add_response(#code, __VA_ARGS__); goto err; } #define ABRT(code, ...) { set_command_failed_response(#code, __VA_ARGS__); goto err; }
static inline bool static inline bool
mmap_img_file(GraphicsManager UNUSED *self, Image *img, int fd, size_t sz, off_t offset) { mmap_img_file(GraphicsManager UNUSED *self, Image *img, int fd, size_t sz, off_t offset) {
@@ -220,7 +228,7 @@ err:
static void static void
png_error_handler(const char *code, const char *msg) { png_error_handler(const char *code, const char *msg) {
set_add_response(code, "%s", msg); set_command_failed_response(code, "%s", msg);
} }
static inline bool static inline bool
@@ -308,12 +316,38 @@ find_or_create_image(GraphicsManager *self, uint32_t id, bool *existing) {
return ans; return ans;
} }
static int
cmp_client_ids(const void* a, const void* b) {
const uint32_t *x = a, *y = b;
return *x - *y;
}
static inline uint32_t
get_free_client_id(const GraphicsManager *self) {
if (!self->image_count) return 1;
uint32_t *client_ids = malloc(sizeof(uint32_t) * self->image_count);
size_t count = 0;
for (size_t i = 0; i < self->image_count; i++) {
Image *q = self->images + i;
if (q->client_id) client_ids[count++] = q->client_id;
}
if (!count) { free(client_ids); return 1; }
qsort(client_ids, count, sizeof(uint32_t), cmp_client_ids);
uint32_t prev_id = 0, ans = 1;
for (size_t i = 0; i < count; i++) {
if (client_ids[i] == prev_id) continue;
prev_id = client_ids[i];
if (client_ids[i] != ans) break;
ans = client_ids[i] + 1;
}
free(client_ids);
return ans;
}
static Image* static Image*
handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_t *payload, bool *is_dirty, uint32_t iid) { handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_t *payload, bool *is_dirty, uint32_t iid) {
#define ABRT(code, ...) { set_add_response(#code, __VA_ARGS__); self->loading_image = 0; if (img) img->data_loaded = false; return NULL; } #define ABRT(code, ...) { set_command_failed_response(#code, __VA_ARGS__); self->loading_image = 0; if (img) img->data_loaded = false; return NULL; }
#define MAX_DATA_SZ (4u * 100000000u) #define MAX_DATA_SZ (4u * 100000000u)
has_add_respose = false;
bool existing, init_img = true; bool existing, init_img = true;
Image *img = NULL; Image *img = NULL;
unsigned char tt = g->transmission_type ? g->transmission_type : 'd'; unsigned char tt = g->transmission_type ? g->transmission_type : 'd';
@@ -336,6 +370,11 @@ handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_
} else { } else {
img->internal_id = internal_id_counter++; img->internal_id = internal_id_counter++;
img->client_id = iid; img->client_id = iid;
img->client_number = g->image_number;
if (!img->client_id && img->client_number) {
img->client_id = get_free_client_id(self);
self->last_init_graphics_command.id = img->client_id;
}
} }
img->atime = monotonic(); img->used_storage = 0; img->atime = monotonic(); img->used_storage = 0;
img->width = g->data_width; img->height = g->data_height; img->width = g->data_width; img->height = g->data_height;
@@ -402,7 +441,7 @@ handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_
snprintf(fname, sizeof(fname)/sizeof(fname[0]), "%.*s", (int)g->payload_sz, payload); snprintf(fname, sizeof(fname)/sizeof(fname[0]), "%.*s", (int)g->payload_sz, payload);
if (tt == 's') fd = shm_open(fname, O_RDONLY, 0); if (tt == 's') fd = shm_open(fname, O_RDONLY, 0);
else fd = open(fname, O_CLOEXEC | O_RDONLY); else fd = open(fname, O_CLOEXEC | O_RDONLY);
if (fd == -1) ABRT(EBADF, "Failed to open file %s for graphics transmission with error: [%d] %s", fname, errno, strerror(errno)); if (fd == -1) ABRT(EBADF, "Failed to open file for graphics transmission with error: [%d] %s", errno, strerror(errno));
img->data_loaded = mmap_img_file(self, img, fd, g->data_sz, g->data_offset); img->data_loaded = mmap_img_file(self, img, fd, g->data_sz, g->data_offset);
safe_close(fd, __FILE__, __LINE__); safe_close(fd, __FILE__, __LINE__);
if (tt == 't') { if (tt == 't') {
@@ -475,15 +514,26 @@ handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_
} }
static inline const char* static inline const char*
create_add_response(GraphicsManager UNUSED *self, bool data_loaded, uint32_t iid) { finish_command_response(const GraphicsCommand *g, bool data_loaded, uint32_t iid, uint32_t placement_id, uint32_t image_number) {
static char rbuf[sizeof(add_response)/sizeof(add_response[0]) + 64]; static char rbuf[sizeof(command_response)/sizeof(command_response[0]) + 128];
if (iid) { bool is_ok_response = !command_response[0];
if (!has_add_respose) { if (g->quiet) {
if (is_ok_response || g->quiet > 1) return NULL;
}
if (iid || image_number) {
if (is_ok_response) {
if (!data_loaded) return NULL; if (!data_loaded) return NULL;
snprintf(add_response, 10, "OK"); snprintf(command_response, 10, "OK");
} }
snprintf(rbuf, sizeof(rbuf)/sizeof(rbuf[0]) - 1, "Gi=%u;%s", iid, add_response); size_t pos = 0;
rbuf[pos++] = 'G';
#define print(fmt, ...) pos += snprintf(rbuf + pos, arraysz(rbuf) - 1 - pos, fmt, __VA_ARGS__)
if (iid) print("i=%u", iid);
if (image_number) print(",I=%u", image_number);
if (placement_id) print(",p=%u", placement_id);
print(";%s", command_response);
return rbuf; return rbuf;
#undef print
} }
return NULL; return NULL;
} }
@@ -519,20 +569,24 @@ update_dest_rect(ImageRef *ref, uint32_t num_cols, uint32_t num_rows, CellPixelS
} }
static void static uint32_t
handle_put_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c, bool *is_dirty, Image *img, CellPixelSize cell) { handle_put_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c, bool *is_dirty, Image *img, CellPixelSize cell) {
has_add_respose = false; if (img == NULL) {
if (img == NULL) img = img_by_client_id(self, g->id); if (g->id) img = img_by_client_id(self, g->id);
if (img == NULL) { set_add_response("ENOENT", "Put command refers to non-existent image with id: %u", g->id); return; } else if (g->image_number) img = img_by_client_number(self, g->image_number);
if (!img->data_loaded) { set_add_response("ENOENT", "Put command refers to image with id: %u that could not load its data", g->id); return; } if (img == NULL) { set_command_failed_response("ENOENT", "Put command refers to non-existent image with id: %u and number: %u", g->id, g->image_number); return g->id; }
}
if (!img->data_loaded) { set_command_failed_response("ENOENT", "Put command refers to image with id: %u that could not load its data", g->id); return img->client_id; }
ensure_space_for(img, refs, ImageRef, img->refcnt + 1, refcap, 16, true); ensure_space_for(img, refs, ImageRef, img->refcnt + 1, refcap, 16, true);
*is_dirty = true; *is_dirty = true;
self->layers_dirty = true; self->layers_dirty = true;
ImageRef *ref = NULL; ImageRef *ref = NULL;
for (size_t i=0; i < img->refcnt; i++) { if (g->placement_id && img->client_id) {
if ((unsigned)img->refs[i].start_row == c->x && (unsigned)img->refs[i].start_column == c->y) { for (size_t i=0; i < img->refcnt; i++) {
ref = img->refs + i; if (img->refs[i].client_id == g->placement_id) {
break; ref = img->refs + i;
break;
}
} }
} }
if (ref == NULL) { if (ref == NULL) {
@@ -548,10 +602,12 @@ handle_put_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c, b
ref->cell_x_offset = MIN(g->cell_x_offset, cell.width - 1); ref->cell_x_offset = MIN(g->cell_x_offset, cell.width - 1);
ref->cell_y_offset = MIN(g->cell_y_offset, cell.height - 1); ref->cell_y_offset = MIN(g->cell_y_offset, cell.height - 1);
ref->num_cols = g->num_cells; ref->num_rows = g->num_lines; ref->num_cols = g->num_cells; ref->num_rows = g->num_lines;
if (img->client_id) ref->client_id = g->placement_id;
update_src_rect(ref, img); update_src_rect(ref, img);
update_dest_rect(ref, g->num_cells, g->num_lines, cell); update_dest_rect(ref, g->num_cells, g->num_lines, cell);
// Move the cursor, the screen will take care of ensuring it is in bounds // Move the cursor, the screen will take care of ensuring it is in bounds
c->x += ref->effective_num_cols; c->y += ref->effective_num_rows - 1; c->x += ref->effective_num_cols; c->y += ref->effective_num_rows - 1;
return img->client_id;
} }
static int static int
@@ -645,7 +701,8 @@ grman_update_layers(GraphicsManager *self, unsigned int scrolled_by, float scree
// Image lifetime/scrolling {{{ // Image lifetime/scrolling {{{
static inline void static inline void
filter_refs(GraphicsManager *self, const void* data, bool free_images, bool (*filter_func)(const ImageRef*, Image*, const void*, CellPixelSize), CellPixelSize cell) { filter_refs(GraphicsManager *self, const void* data, bool free_images, bool (*filter_func)(const ImageRef*, Image*, const void*, CellPixelSize), CellPixelSize cell, bool only_first_image) {
bool matched = false;
for (size_t i = self->image_count; i-- > 0;) { for (size_t i = self->image_count; i-- > 0;) {
Image *img = self->images + i; Image *img = self->images + i;
for (size_t j = img->refcnt; j-- > 0;) { for (size_t j = img->refcnt; j-- > 0;) {
@@ -653,12 +710,15 @@ filter_refs(GraphicsManager *self, const void* data, bool free_images, bool (*fi
if (filter_func(ref, img, data, cell)) { if (filter_func(ref, img, data, cell)) {
remove_i_from_array(img->refs, j, img->refcnt); remove_i_from_array(img->refs, j, img->refcnt);
self->layers_dirty = true; self->layers_dirty = true;
matched = true;
} }
} }
if (img->refcnt == 0 && (free_images || img->client_id == 0)) remove_image(self, i); if (img->refcnt == 0 && (free_images || img->client_id == 0)) remove_image(self, i);
if (only_first_image && matched) break;
} }
} }
static inline void static inline void
modify_refs(GraphicsManager *self, const void* data, bool free_images, bool (*filter_func)(ImageRef*, Image*, const void*, CellPixelSize), CellPixelSize cell) { modify_refs(GraphicsManager *self, const void* data, bool free_images, bool (*filter_func)(ImageRef*, Image*, const void*, CellPixelSize), CellPixelSize cell) {
for (size_t i = self->image_count; i-- > 0;) { for (size_t i = self->image_count; i-- > 0;) {
@@ -739,15 +799,24 @@ clear_all_filter_func(const ImageRef *ref UNUSED, Image UNUSED *img, const void
void void
grman_clear(GraphicsManager *self, bool all, CellPixelSize cell) { grman_clear(GraphicsManager *self, bool all, CellPixelSize cell) {
filter_refs(self, NULL, true, all ? clear_all_filter_func : clear_filter_func, cell); filter_refs(self, NULL, true, all ? clear_all_filter_func : clear_filter_func, cell, false);
} }
static inline bool static inline bool
id_filter_func(const ImageRef UNUSED *ref, Image *img, const void *data, CellPixelSize cell UNUSED) { id_filter_func(const ImageRef *ref, Image *img, const void *data, CellPixelSize cell UNUSED) {
uint32_t iid = *(uint32_t*)data; const GraphicsCommand *g = data;
return img->client_id == iid; if (g->id && img->client_id == g->id) return !g->placement_id || ref->client_id == g->placement_id;
return false;
} }
static inline bool
number_filter_func(const ImageRef *ref, Image *img, const void *data, CellPixelSize cell UNUSED) {
const GraphicsCommand *g = data;
if (g->image_number && img->client_number == g->image_number) return !g->placement_id || ref->client_id == g->placement_id;
return false;
}
static inline bool static inline bool
x_filter_func(const ImageRef *ref, Image UNUSED *img, const void *data, CellPixelSize cell UNUSED) { x_filter_func(const ImageRef *ref, Image UNUSED *img, const void *data, CellPixelSize cell UNUSED) {
const GraphicsCommand *g = data; const GraphicsCommand *g = data;
@@ -780,14 +849,15 @@ point3d_filter_func(const ImageRef *ref, Image *img, const void *data, CellPixel
static void static void
handle_delete_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c, bool *is_dirty, CellPixelSize cell) { handle_delete_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c, bool *is_dirty, CellPixelSize cell) {
static GraphicsCommand d; GraphicsCommand d;
bool only_first_image = false;
switch (g->delete_action) { switch (g->delete_action) {
#define I(u, data, func) filter_refs(self, data, g->delete_action == u, func, cell); *is_dirty = true; break #define I(u, data, func) filter_refs(self, data, g->delete_action == u, func, cell, only_first_image); *is_dirty = true; break
#define D(l, u, data, func) case l: case u: I(u, data, func) #define D(l, u, data, func) case l: case u: I(u, data, func)
#define G(l, u, func) D(l, u, g, func) #define G(l, u, func) D(l, u, g, func)
case 0: case 0:
D('a', 'A', NULL, clear_filter_func); D('a', 'A', NULL, clear_filter_func);
D('i', 'I', &g->id, id_filter_func); G('i', 'I', id_filter_func);
G('p', 'P', point_filter_func); G('p', 'P', point_filter_func);
G('q', 'Q', point3d_filter_func); G('q', 'Q', point3d_filter_func);
G('x', 'X', x_filter_func); G('x', 'X', x_filter_func);
@@ -797,6 +867,10 @@ handle_delete_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c
case 'C': case 'C':
d.x_offset = c->x + 1; d.y_offset = c->y + 1; d.x_offset = c->x + 1; d.y_offset = c->y + 1;
I('C', &d, point_filter_func); I('C', &d, point_filter_func);
case 'n':
case 'N':
only_first_image = true;
I('N', g, number_filter_func);
default: default:
REPORT_ERROR("Unknown graphics command delete action: %c", g->delete_action); REPORT_ERROR("Unknown graphics command delete action: %c", g->delete_action);
break; break;
@@ -832,6 +906,12 @@ grman_rescale(GraphicsManager *self, CellPixelSize cell) {
const char* const char*
grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_t *payload, Cursor *c, bool *is_dirty, CellPixelSize cell) { grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_t *payload, Cursor *c, bool *is_dirty, CellPixelSize cell) {
const char *ret = NULL; const char *ret = NULL;
command_response[0] = 0;
if (g->id && g->image_number) {
set_command_failed_response("EINVAL", "Must not specify both image id and image number");
return finish_command_response(g, false, g->id, g->placement_id, g->image_number);
}
switch(g->action) { switch(g->action) {
case 0: case 0:
@@ -839,9 +919,11 @@ grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint
case 'T': case 'T':
case 'q': { case 'q': {
uint32_t iid = g->id, q_iid = iid; uint32_t iid = g->id, q_iid = iid;
if (g->action == 'q') { iid = 0; if (!q_iid) { REPORT_ERROR("Query graphics command without image id"); break; } } bool is_query = g->action == 'q';
if (is_query) { iid = 0; if (!q_iid) { REPORT_ERROR("Query graphics command without image id"); break; } }
Image *image = handle_add_command(self, g, payload, is_dirty, iid); Image *image = handle_add_command(self, g, payload, is_dirty, iid);
ret = create_add_response(self, image != NULL, g->action == 'q' ? q_iid: self->last_init_graphics_command.id); if (is_query) ret = finish_command_response(g, image != NULL, q_iid, 0, 0);
else ret = finish_command_response(g, image != NULL, self->last_init_graphics_command.id, self->last_init_graphics_command.placement_id, self->last_init_graphics_command.image_number);
if (self->last_init_graphics_command.action == 'T' && image && image->data_loaded) handle_put_command(self, &self->last_init_graphics_command, c, is_dirty, image, cell); if (self->last_init_graphics_command.action == 'T' && image && image->data_loaded) handle_put_command(self, &self->last_init_graphics_command, c, is_dirty, image, cell);
id_type added_image_id = image ? image->internal_id : 0; id_type added_image_id = image ? image->internal_id : 0;
if (g->action == 'q') remove_images(self, add_trim_predicate, 0); if (g->action == 'q') remove_images(self, add_trim_predicate, 0);
@@ -849,12 +931,12 @@ grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint
break; break;
} }
case 'p': case 'p':
if (!g->id) { if (!g->id && !g->image_number) {
REPORT_ERROR("Put graphics command without image id"); REPORT_ERROR("Put graphics command without image id or number");
break; break;
} }
handle_put_command(self, g, c, is_dirty, NULL, cell); uint32_t image_id = handle_put_command(self, g, c, is_dirty, NULL, cell);
ret = create_add_response(self, true, g->id); ret = finish_command_response(g, true, image_id, g->placement_id, g->image_number);
break; break;
case 'd': case 'd':
handle_delete_command(self, g, c, is_dirty, cell); handle_delete_command(self, g, c, is_dirty, cell);
@@ -878,8 +960,8 @@ new(PyTypeObject UNUSED *type, PyObject UNUSED *args, PyObject UNUSED *kwds) {
static inline PyObject* static inline PyObject*
image_as_dict(Image *img) { image_as_dict(Image *img) {
#define U(x) #x, img->x #define U(x) #x, img->x
return Py_BuildValue("{sI sI sI sI sK sI sO sO sN}", return Py_BuildValue("{sI sI sI sI sK sI sI sO sO sN}",
U(texture_id), U(client_id), U(width), U(height), U(internal_id), U(refcnt), U(texture_id), U(client_id), U(width), U(height), U(internal_id), U(refcnt), U(client_number),
"data_loaded", img->data_loaded ? Py_True : Py_False, "data_loaded", img->data_loaded ? Py_True : Py_False,
"is_4byte_aligned", img->load_data.is_4byte_aligned ? Py_True : Py_False, "is_4byte_aligned", img->load_data.is_4byte_aligned ? Py_True : Py_False,
"data", Py_BuildValue("y#", img->load_data.data, img->load_data.data_sz) "data", Py_BuildValue("y#", img->load_data.data, img->load_data.data_sz)
@@ -899,6 +981,13 @@ W(image_for_client_id) {
return image_as_dict(img); return image_as_dict(img);
} }
W(image_for_client_number) {
unsigned long num = PyLong_AsUnsignedLong(args);
Image *img = img_by_client_number(self, num);
if (!img) Py_RETURN_NONE;
return image_as_dict(img);
}
W(shm_write) { W(shm_write) {
const char *name, *data; const char *name, *data;
Py_ssize_t sz; Py_ssize_t sz;
@@ -949,6 +1038,7 @@ W(update_layers) {
static PyMethodDef methods[] = { static PyMethodDef methods[] = {
M(image_for_client_id, METH_O), M(image_for_client_id, METH_O),
M(image_for_client_number, METH_O),
M(update_layers, METH_VARARGS), M(update_layers, METH_VARARGS),
{NULL} /* Sentinel */ {NULL} /* Sentinel */
}; };

View File

@@ -10,7 +10,7 @@
typedef struct { typedef struct {
unsigned char action, transmission_type, compressed, delete_action; unsigned char action, transmission_type, compressed, delete_action;
uint32_t format, more, id, data_sz, data_offset; uint32_t format, more, id, image_number, data_sz, data_offset, placement_id, quiet;
uint32_t width, height, x_offset, y_offset, data_height, data_width, num_cells, num_lines, cell_x_offset, cell_y_offset; uint32_t width, height, x_offset, y_offset, data_height, data_width, num_cells, num_lines, cell_x_offset, cell_y_offset;
int32_t z_index; int32_t z_index;
size_t payload_sz; size_t payload_sz;
@@ -38,12 +38,13 @@ typedef struct {
uint32_t cell_x_offset, cell_y_offset, num_cols, num_rows, effective_num_rows, effective_num_cols; uint32_t cell_x_offset, cell_y_offset, num_cols, num_rows, effective_num_rows, effective_num_cols;
int32_t z_index; int32_t z_index;
int32_t start_row, start_column; int32_t start_row, start_column;
uint32_t client_id;
ImageRect src_rect; ImageRect src_rect;
} ImageRef; } ImageRef;
typedef struct { typedef struct {
uint32_t texture_id, client_id, width, height; uint32_t texture_id, client_id, client_number, width, height;
id_type internal_id; id_type internal_id;
bool data_loaded; bool data_loaded;

View File

@@ -9,6 +9,7 @@
#include "lineops.h" #include "lineops.h"
#include "charsets.h" #include "charsets.h"
#include <structmember.h> #include <structmember.h>
#include "ringbuf.h"
extern PyTypeObject Line_Type; extern PyTypeObject Line_Type;
#define SEGMENT_SIZE 2048 #define SEGMENT_SIZE 2048
@@ -61,42 +62,37 @@ alloc_pagerhist(size_t pagerhist_sz) {
if (!pagerhist_sz) return NULL; if (!pagerhist_sz) return NULL;
ph = PyMem_Calloc(1, sizeof(PagerHistoryBuf)); ph = PyMem_Calloc(1, sizeof(PagerHistoryBuf));
if (!ph) return NULL; if (!ph) return NULL;
ph->max_sz = pagerhist_sz; size_t sz = MIN(1024u * 1024u, pagerhist_sz);
ph->buffer_size = MIN(1024u*1024u, ph->max_sz); ph->ringbuf = ringbuf_new(sz);
ph->buffer = PyMem_Malloc(ph->buffer_size); if (!ph->ringbuf) { PyMem_Free(ph); return NULL; }
if (!ph->buffer) { PyMem_Free(ph); return NULL; } ph->maximum_size = pagerhist_sz;
return ph; return ph;
} }
static inline void static inline void
free_pagerhist(HistoryBuf *self) { free_pagerhist(HistoryBuf *self) {
if (self->pagerhist) PyMem_Free(self->pagerhist->buffer); if (self->pagerhist && self->pagerhist->ringbuf) ringbuf_free((ringbuf_t*)&self->pagerhist->ringbuf);
PyMem_Free(self->pagerhist); PyMem_Free(self->pagerhist);
self->pagerhist = NULL; self->pagerhist = NULL;
} }
static inline bool static inline bool
pagerhist_extend(PagerHistoryBuf *ph, size_t minsz) { pagerhist_extend(PagerHistoryBuf *ph, size_t minsz) {
if (ph->buffer_size >= ph->max_sz) return false; size_t buffer_size = ringbuf_capacity(ph->ringbuf);
size_t newsz = MIN(ph->max_sz, ph->buffer_size + MAX(1024u * 1024u, minsz)); if (buffer_size >= ph->maximum_size) return false;
uint8_t *newbuf = PyMem_Malloc(newsz); size_t newsz = MIN(ph->maximum_size, buffer_size + MAX(1024u * 1024u, minsz));
ringbuf_t newbuf = ringbuf_new(newsz);
if (!newbuf) return false; if (!newbuf) return false;
size_t copied = MIN(ph->length, ph->buffer_size - ph->start); size_t count = ringbuf_bytes_used(ph->ringbuf);
if (copied) memcpy(newbuf, ph->buffer + ph->start, copied); if (count) ringbuf_copy(newbuf, ph->ringbuf, count);
if (copied < ph->length) memcpy(newbuf + copied, ph->buffer, (ph->length - copied)); ringbuf_free((ringbuf_t*)&ph->ringbuf);
PyMem_Free(ph->buffer); ph->ringbuf = newbuf;
ph->start = 0;
ph->buffer = newbuf;
ph->buffer_size = newsz;
return true; return true;
} }
static inline void static inline void
pagerhist_clear(HistoryBuf *self) { pagerhist_clear(HistoryBuf *self) {
if (!self->pagerhist || !self->pagerhist->max_sz) return; if (self->pagerhist && self->pagerhist->ringbuf) ringbuf_reset(self->pagerhist->ringbuf);
index_type pagerhist_sz = self->pagerhist->max_sz;
free_pagerhist(self);
self->pagerhist = alloc_pagerhist(pagerhist_sz);
} }
static HistoryBuf* static HistoryBuf*
@@ -188,36 +184,28 @@ historybuf_clear(HistoryBuf *self) {
static inline bool static inline bool
pagerhist_write_bytes(PagerHistoryBuf *ph, const uint8_t *buf, size_t sz) { pagerhist_write_bytes(PagerHistoryBuf *ph, const uint8_t *buf, size_t sz) {
if (sz > ph->max_sz) return false; if (sz > ph->maximum_size) return false;
if (!sz) return true; if (!sz) return true;
if (sz > ph->buffer_size - ph->length) pagerhist_extend(ph, sz); size_t space_in_ringbuf = ringbuf_bytes_free(ph->ringbuf);
if (sz > ph->buffer_size) return false; if (sz > space_in_ringbuf) pagerhist_extend(ph, sz);
size_t start_writing_at = (ph->start + ph->length) % ph->buffer_size; ringbuf_memcpy_into(ph->ringbuf, buf, sz);
size_t available_space = ph->buffer_size - ph->length;
size_t overlap = available_space < sz ? sz - available_space : 0;
size_t copied = MIN(sz, ph->buffer_size - start_writing_at);
ph->length += sz - overlap;
ph->start = (ph->start + overlap) % ph->buffer_size;
if (copied) memcpy(ph->buffer + start_writing_at, buf, copied);
if (copied < sz) memcpy(ph->buffer, buf + copied, (sz - copied));
return true; return true;
} }
static inline bool static inline bool
pagerhist_ensure_start_is_valid_utf8(PagerHistoryBuf *ph) { pagerhist_ensure_start_is_valid_utf8(PagerHistoryBuf *ph) {
uint8_t scratch[8];
size_t num = ringbuf_memcpy_from(scratch, ph->ringbuf, arraysz(scratch));
uint32_t state = UTF8_ACCEPT, codep; uint32_t state = UTF8_ACCEPT, codep;
size_t pos = ph->start, count = 0; size_t count = 0;
size_t last_reject_at = 0; size_t last_reject_at = 0;
while (count < ph->length) { while (count < num) {
decode_utf8(&state, &codep, ph->buffer[pos]); decode_utf8(&state, &codep, scratch[count++]);
count++;
if (state == UTF8_ACCEPT) break; if (state == UTF8_ACCEPT) break;
if (state == UTF8_REJECT) { state = UTF8_ACCEPT; last_reject_at = count; } if (state == UTF8_REJECT) { state = UTF8_ACCEPT; last_reject_at = count; }
pos = pos == ph->buffer_size - 1 ? 0: pos + 1;
} }
if (last_reject_at) { if (last_reject_at) {
ph->start = (ph->start + last_reject_at) % ph->buffer_size; ringbuf_memmove_from(scratch, ph->ringbuf, last_reject_at);
ph->length -= last_reject_at;
return true; return true;
} }
return false; return false;
@@ -241,7 +229,7 @@ pagerhist_push(HistoryBuf *self, ANSIBuf *as_ansi_buf) {
Line l = {.xnum=self->xnum}; Line l = {.xnum=self->xnum};
init_line(self, self->start_of_data, &l); init_line(self, self->start_of_data, &l);
line_as_ansi(&l, as_ansi_buf, &prev_cell); line_as_ansi(&l, as_ansi_buf, &prev_cell);
if (ph->length != 0 && !l.continued) pagerhist_write_bytes(ph, (const uint8_t*)"\n", 1); if (ringbuf_bytes_used(ph->ringbuf) && !l.continued) pagerhist_write_bytes(ph, (const uint8_t*)"\n", 1);
pagerhist_write_bytes(ph, (const uint8_t*)"\x1b[m", 3); pagerhist_write_bytes(ph, (const uint8_t*)"\x1b[m", 3);
if (pagerhist_write_ucs4(ph, as_ansi_buf->buf, as_ansi_buf->len)) pagerhist_write_bytes(ph, (const uint8_t*)"\r", 1); if (pagerhist_write_ucs4(ph, as_ansi_buf->buf, as_ansi_buf->len)) pagerhist_write_bytes(ph, (const uint8_t*)"\r", 1);
} }
@@ -335,15 +323,16 @@ static inline Line*
get_line(HistoryBuf *self, index_type y, Line *l) { init_line(self, index_of(self, self->count - y - 1), l); return l; } get_line(HistoryBuf *self, index_type y, Line *l) { init_line(self, index_of(self, self->count - y - 1), l); return l; }
static inline char_type static inline char_type
pagerhist_read_char(PagerHistoryBuf *ph, size_t pos, unsigned *count, uint8_t record[8]) { pagerhist_remove_char(PagerHistoryBuf *ph, unsigned *count, uint8_t record[8]) {
uint32_t codep, state = UTF8_ACCEPT; uint32_t codep, state = UTF8_ACCEPT;
*count = 0; *count = 0;
while (true) { size_t num = ringbuf_bytes_used(ph->ringbuf);
decode_utf8(&state, &codep, ph->buffer[pos]); while (num--) {
record[(*count)++] = ph->buffer[pos]; record[*count] = ringbuf_move_char(ph->ringbuf);
decode_utf8(&state, &codep, record[*count]);
*count += 1;
if (state == UTF8_REJECT) { codep = 0; break; } if (state == UTF8_REJECT) { codep = 0; break; }
if (state == UTF8_ACCEPT) break; if (state == UTF8_ACCEPT) break;
pos = pos == ph->buffer_size - 1 ? 0 : (pos + 1);
} }
return codep; return codep;
} }
@@ -351,14 +340,12 @@ pagerhist_read_char(PagerHistoryBuf *ph, size_t pos, unsigned *count, uint8_t re
static void static void
pagerhist_rewrap_to(HistoryBuf *self, index_type cells_in_line) { pagerhist_rewrap_to(HistoryBuf *self, index_type cells_in_line) {
PagerHistoryBuf *ph = self->pagerhist; PagerHistoryBuf *ph = self->pagerhist;
if (!ph->length) return; if (!ph->ringbuf || !ringbuf_bytes_used(ph->ringbuf)) return;
PagerHistoryBuf *nph = PyMem_Calloc(sizeof(PagerHistoryBuf), 1); PagerHistoryBuf *nph = PyMem_Calloc(sizeof(PagerHistoryBuf), 1);
if (!nph) return; if (!nph) return;
nph->buffer_size = ph->buffer_size; nph->maximum_size = ph->maximum_size;
nph->max_sz = ph->max_sz; nph->ringbuf = ringbuf_new(MIN(ph->maximum_size, ringbuf_capacity(ph->ringbuf) + 4096));
nph->buffer = PyMem_Malloc(nph->buffer_size); if (!nph->ringbuf) { PyMem_Free(nph); return ; }
if (!nph->buffer) { PyMem_Free(nph); return ; }
size_t i = 0, pos;
ssize_t ch_width = 0; ssize_t ch_width = 0;
unsigned count; unsigned count;
uint8_t record[8]; uint8_t record[8];
@@ -367,11 +354,6 @@ pagerhist_rewrap_to(HistoryBuf *self, index_type cells_in_line) {
WCSState wcs_state; WCSState wcs_state;
initialize_wcs_state(&wcs_state); initialize_wcs_state(&wcs_state);
#define READ_CHAR(ch) { \
ch = pagerhist_read_char(ph, pos, &count, record); \
i += count; pos += count; \
if (pos >= ph->buffer_size) pos = pos - ph->buffer_size; \
}
#define WRITE_CHAR() { \ #define WRITE_CHAR() { \
if (num_in_current_line + ch_width > cells_in_line) { \ if (num_in_current_line + ch_width > cells_in_line) { \
pagerhist_write_bytes(nph, (const uint8_t*)"\r", 1); \ pagerhist_write_bytes(nph, (const uint8_t*)"\r", 1); \
@@ -381,10 +363,8 @@ pagerhist_rewrap_to(HistoryBuf *self, index_type cells_in_line) {
pagerhist_write_bytes(nph, record, count); \ pagerhist_write_bytes(nph, record, count); \
} }
for (i = 0; i < ph->length;) { while (ringbuf_bytes_used(ph->ringbuf)) {
pos = ph->start + i; ch = pagerhist_remove_char(ph, &count, record);
if (pos >= ph->buffer_size) pos = pos - ph->buffer_size;
READ_CHAR(ch);
if (ch == '\n') { if (ch == '\n') {
initialize_wcs_state(&wcs_state); initialize_wcs_state(&wcs_state);
ch_width = 1; ch_width = 1;
@@ -397,12 +377,12 @@ pagerhist_rewrap_to(HistoryBuf *self, index_type cells_in_line) {
} }
free_pagerhist(self); free_pagerhist(self);
self->pagerhist = nph; self->pagerhist = nph;
#undef READ_CHAR #undef WRITE_CHAR
} }
static PyObject* static PyObject*
pagerhist_write(HistoryBuf *self, PyObject *what) { pagerhist_write(HistoryBuf *self, PyObject *what) {
if (self->pagerhist && self->pagerhist->max_sz) { if (self->pagerhist && self->pagerhist->maximum_size) {
if (PyBytes_Check(what)) pagerhist_write_bytes(self->pagerhist, (const uint8_t*)PyBytes_AS_STRING(what), PyBytes_GET_SIZE(what)); if (PyBytes_Check(what)) pagerhist_write_bytes(self->pagerhist, (const uint8_t*)PyBytes_AS_STRING(what), PyBytes_GET_SIZE(what));
else if (PyUnicode_Check(what) && PyUnicode_READY(what) == 0) { else if (PyUnicode_Check(what) && PyUnicode_READY(what) == 0) {
Py_UCS4 *buf = PyUnicode_AsUCS4Copy(what); Py_UCS4 *buf = PyUnicode_AsUCS4Copy(what);
@@ -418,19 +398,17 @@ pagerhist_write(HistoryBuf *self, PyObject *what) {
static PyObject* static PyObject*
pagerhist_as_bytes(HistoryBuf *self, PyObject *args UNUSED) { pagerhist_as_bytes(HistoryBuf *self, PyObject *args UNUSED) {
PagerHistoryBuf *ph = self->pagerhist; PagerHistoryBuf *ph = self->pagerhist;
if (!ph || !ph->length) return PyBytes_FromStringAndSize("", 0); if (!ph || !ringbuf_bytes_used(ph->ringbuf)) return PyBytes_FromStringAndSize("", 0);
pagerhist_ensure_start_is_valid_utf8(ph); pagerhist_ensure_start_is_valid_utf8(ph);
if (ph->rewrap_needed) pagerhist_rewrap_to(self, self->xnum); if (ph->rewrap_needed) pagerhist_rewrap_to(self, self->xnum);
Line l = {.xnum=self->xnum}; get_line(self, 0, &l); Line l = {.xnum=self->xnum}; get_line(self, 0, &l);
size_t sz = ph->length; size_t sz = ringbuf_bytes_used(ph->ringbuf);
if (!l.continued) sz += 1; if (!l.continued) sz += 1;
PyObject *ans = PyBytes_FromStringAndSize(NULL, sz); PyObject *ans = PyBytes_FromStringAndSize(NULL, sz);
if (!ans) return NULL; if (!ans) return NULL;
uint8_t *buf = (uint8_t*)PyBytes_AS_STRING(ans); uint8_t *buf = (uint8_t*)PyBytes_AS_STRING(ans);
size_t copied = MIN(ph->length, ph->buffer_size - ph->start); ringbuf_memcpy_from(buf, ph->ringbuf, sz);
if (copied) memcpy(buf, ph->buffer + ph->start, copied);
if (copied < ph->length) memcpy(buf + copied, ph->buffer, (ph->length - copied));
if (!l.continued) buf[sz-1] = '\n'; if (!l.continued) buf[sz-1] = '\n';
return ans; return ans;
} }
@@ -561,7 +539,7 @@ void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other, ANSIBuf *as_ansi_buf
other->count = self->count; other->start_of_data = self->start_of_data; other->count = self->count; other->start_of_data = self->start_of_data;
return; return;
} }
if (other->pagerhist && other->xnum != self->xnum && other->pagerhist->length) if (other->pagerhist && other->xnum != self->xnum && ringbuf_bytes_used(other->pagerhist->ringbuf))
other->pagerhist->rewrap_needed = true; other->pagerhist->rewrap_needed = true;
other->count = 0; other->start_of_data = 0; other->count = 0; other->start_of_data = 0;
index_type x = 0, y = 0; index_type x = 0, y = 0;

39
kitty/key_encoding.py generated
View File

@@ -417,6 +417,10 @@ def symbolic_name(glfw_name: str) -> str:
return glfw_name[9:].replace('_', ' ') return glfw_name[9:].replace('_', ' ')
def glfw_key_name(symbolic_name: str) -> str:
return 'GLFW_KEY_' + symbolic_name.replace(' ', '_')
def update_encoding() -> None: def update_encoding() -> None:
import re import re
import subprocess import subprocess
@@ -492,9 +496,9 @@ del key_name, enc
def decode_key_event(text: str) -> KeyEvent: def decode_key_event(text: str) -> KeyEvent:
typ = type_map[text[1]] typ = type_map[text[0]]
mods = mod_map[text[2]] mods = mod_map[text[1]]
key = key_rmap[text[3:5]] key = key_rmap[text[2:4]]
return KeyEvent(typ, mods, key) return KeyEvent(typ, mods, key)
@@ -503,3 +507,32 @@ def encode_key_event(key_event: KeyEvent) -> str:
mods = rmod_map[key_event.mods] mods = rmod_map[key_event.mods]
key = ENCODING[key_event.key.replace('_', ' ')] key = ENCODING[key_event.key.replace('_', ' ')]
return typ + mods + key return typ + mods + key
class WindowSystemKeyEvent(NamedTuple):
code: int
mods: int
action: int
def decode_key_event_as_window_system_key(text: str) -> Optional[WindowSystemKeyEvent]:
k = decode_key_event(text)
glfw_name = glfw_key_name(k.key)
glfw_code = getattr(defines, glfw_name, None)
if glfw_code is None:
return None
action = defines.GLFW_PRESS
if k.type is RELEASE:
action = defines.GLFW_RELEASE
elif k.type is REPEAT:
action = defines.GLFW_REPEAT
mods = 0
if k.mods & CTRL:
mods |= defines.GLFW_MOD_CONTROL
if k.mods & ALT:
mods |= defines.GLFW_MOD_ALT
if k.mods & SUPER:
mods |= defines.GLFW_MOD_SUPER
if k.mods & SHIFT:
mods |= defines.GLFW_MOD_SHIFT
return WindowSystemKeyEvent(glfw_code, mods, action)

View File

@@ -249,7 +249,6 @@ def expand_listen_on(listen_on: str, from_config_file: bool) -> str:
def setup_environment(opts: OptionsStub, cli_opts: CLIOptions) -> None: def setup_environment(opts: OptionsStub, cli_opts: CLIOptions) -> None:
extra_env = opts.env.copy()
if opts.editor == '.': if opts.editor == '.':
editor = get_editor_from_env(os.environ) editor = get_editor_from_env(os.environ)
if not editor: if not editor:
@@ -266,7 +265,7 @@ def setup_environment(opts: OptionsStub, cli_opts: CLIOptions) -> None:
if cli_opts.listen_on and opts.allow_remote_control != 'n': if cli_opts.listen_on and opts.allow_remote_control != 'n':
cli_opts.listen_on = expand_listen_on(cli_opts.listen_on, from_config_file) cli_opts.listen_on = expand_listen_on(cli_opts.listen_on, from_config_file)
os.environ['KITTY_LISTEN_ON'] = cli_opts.listen_on os.environ['KITTY_LISTEN_ON'] = cli_opts.listen_on
set_default_env(extra_env) set_default_env(opts.env.copy())
def set_locale() -> None: def set_locale() -> None:

View File

@@ -368,7 +368,7 @@ HANDLER(handle_move_event) {
bool in_left_half_of_cell = false; bool in_left_half_of_cell = false;
if (!cell_for_pos(w, &x, &y, &in_left_half_of_cell, global_state.callback_os_window)) return; if (!cell_for_pos(w, &x, &y, &in_left_half_of_cell, global_state.callback_os_window)) return;
Screen *screen = w->render_data.screen; Screen *screen = w->render_data.screen;
detect_url(screen, x, y); if(OPT(detect_urls)) detect_url(screen, x, y);
bool mouse_cell_changed = x != w->mouse_pos.cell_x || y != w->mouse_pos.cell_y; bool mouse_cell_changed = x != w->mouse_pos.cell_x || y != w->mouse_pos.cell_y;
bool cell_half_changed = in_left_half_of_cell != w->mouse_pos.in_left_half_of_cell; bool cell_half_changed = in_left_half_of_cell != w->mouse_pos.in_left_half_of_cell;
w->mouse_pos.cell_x = x; w->mouse_pos.cell_y = y; w->mouse_pos.cell_x = x; w->mouse_pos.cell_y = y;

View File

@@ -1,4 +1,5 @@
// This file is generated by ./gen-apc-parsers.py do not edit! // This file is generated by /home/kovid/work/kitty/./gen-apc-parsers.py do not
// edit!
#pragma once #pragma once
@@ -23,6 +24,9 @@ static inline void parse_graphics_code(Screen *screen,
format = 'f', format = 'f',
more = 'm', more = 'm',
id = 'i', id = 'i',
image_number = 'I',
placement_id = 'p',
quiet = 'q',
width = 'w', width = 'w',
height = 'h', height = 'h',
x_offset = 'x', x_offset = 'x',
@@ -69,6 +73,15 @@ static inline void parse_graphics_code(Screen *screen,
case id: case id:
value_state = UINT; value_state = UINT;
break; break;
case image_number:
value_state = UINT;
break;
case placement_id:
value_state = UINT;
break;
case quiet:
value_state = UINT;
break;
case width: case width:
value_state = UINT; value_state = UINT;
break; break;
@@ -131,8 +144,8 @@ static inline void parse_graphics_code(Screen *screen,
case action: { case action: {
g.action = screen->parser_buf[pos++] & 0xff; g.action = screen->parser_buf[pos++] & 0xff;
if (g.action != 't' && g.action != 'd' && g.action != 'p' && if (g.action != 'T' && g.action != 'q' && g.action != 'd' &&
g.action != 'q' && g.action != 'T') { g.action != 'p' && g.action != 't') {
REPORT_ERROR("Malformed GraphicsCommand control block, unknown flag " REPORT_ERROR("Malformed GraphicsCommand control block, unknown flag "
"value for action: 0x%x", "value for action: 0x%x",
g.action); g.action);
@@ -142,14 +155,15 @@ static inline void parse_graphics_code(Screen *screen,
case delete_action: { case delete_action: {
g.delete_action = screen->parser_buf[pos++] & 0xff; g.delete_action = screen->parser_buf[pos++] & 0xff;
if (g.delete_action != 'X' && g.delete_action != 'y' && if (g.delete_action != 'C' && g.delete_action != 'z' &&
g.delete_action != 'i' && g.delete_action != 'I' && g.delete_action != 'y' && g.delete_action != 'X' &&
g.delete_action != 'A' && g.delete_action != 'p' &&
g.delete_action != 'Y' && g.delete_action != 'z' &&
g.delete_action != 'a' && g.delete_action != 'P' && g.delete_action != 'a' && g.delete_action != 'P' &&
g.delete_action != 'x' && g.delete_action != 'q' && g.delete_action != 'i' && g.delete_action != 'c' &&
g.delete_action != 'Z' && g.delete_action != 'Q' && g.delete_action != 'N' && g.delete_action != 'I' &&
g.delete_action != 'c' && g.delete_action != 'C') { g.delete_action != 'p' && g.delete_action != 'q' &&
g.delete_action != 'Q' && g.delete_action != 'Z' &&
g.delete_action != 'A' && g.delete_action != 'n' &&
g.delete_action != 'x' && g.delete_action != 'Y') {
REPORT_ERROR("Malformed GraphicsCommand control block, unknown flag " REPORT_ERROR("Malformed GraphicsCommand control block, unknown flag "
"value for delete_action: 0x%x", "value for delete_action: 0x%x",
g.delete_action); g.delete_action);
@@ -159,7 +173,7 @@ static inline void parse_graphics_code(Screen *screen,
case transmission_type: { case transmission_type: {
g.transmission_type = screen->parser_buf[pos++] & 0xff; g.transmission_type = screen->parser_buf[pos++] & 0xff;
if (g.transmission_type != 'f' && g.transmission_type != 'd' && if (g.transmission_type != 'd' && g.transmission_type != 'f' &&
g.transmission_type != 's' && g.transmission_type != 't') { g.transmission_type != 's' && g.transmission_type != 't') {
REPORT_ERROR("Malformed GraphicsCommand control block, unknown flag " REPORT_ERROR("Malformed GraphicsCommand control block, unknown flag "
"value for transmission_type: 0x%x", "value for transmission_type: 0x%x",
@@ -233,6 +247,9 @@ static inline void parse_graphics_code(Screen *screen,
U(format); U(format);
U(more); U(more);
U(id); U(id);
U(image_number);
U(placement_id);
U(quiet);
U(width); U(width);
U(height); U(height);
U(x_offset); U(x_offset);
@@ -303,11 +320,14 @@ static inline void parse_graphics_code(Screen *screen,
} }
REPORT_VA_COMMAND( REPORT_VA_COMMAND(
"s {sc sc sc sc sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI si sI} y#", "s {sc sc sc sc sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI si "
"sI} y#",
"graphics_command", "action", g.action, "delete_action", g.delete_action, "graphics_command", "action", g.action, "delete_action", g.delete_action,
"transmission_type", g.transmission_type, "compressed", g.compressed, "transmission_type", g.transmission_type, "compressed", g.compressed,
"format", (unsigned int)g.format, "more", (unsigned int)g.more, "id", "format", (unsigned int)g.format, "more", (unsigned int)g.more, "id",
(unsigned int)g.id, "width", (unsigned int)g.width, "height", (unsigned int)g.id, "image_number", (unsigned int)g.image_number,
"placement_id", (unsigned int)g.placement_id, "quiet",
(unsigned int)g.quiet, "width", (unsigned int)g.width, "height",
(unsigned int)g.height, "x_offset", (unsigned int)g.x_offset, "y_offset", (unsigned int)g.height, "x_offset", (unsigned int)g.x_offset, "y_offset",
(unsigned int)g.y_offset, "data_height", (unsigned int)g.data_height, (unsigned int)g.y_offset, "data_height", (unsigned int)g.data_height,
"data_width", (unsigned int)g.data_width, "data_sz", "data_width", (unsigned int)g.data_width, "data_sz",

View File

@@ -52,7 +52,6 @@ utf8(char_type codepoint) {
// }}} // }}}
// Macros {{{ // Macros {{{
#define MAX_PARAMS 256
#define IS_DIGIT \ #define IS_DIGIT \
case '0': \ case '0': \
case '1': \ case '1': \
@@ -465,7 +464,10 @@ repr_csi_params(unsigned int *params, unsigned int num_params) {
return buf; return buf;
} }
static inline void #ifdef DUMP_COMMANDS
static
#endif
void
parse_sgr(Screen *screen, uint32_t *buf, unsigned int num, unsigned int *params, PyObject DUMP_UNUSED *dump_callback, const char *report_name DUMP_UNUSED, Region *region) { parse_sgr(Screen *screen, uint32_t *buf, unsigned int num, unsigned int *params, PyObject DUMP_UNUSED *dump_callback, const char *report_name DUMP_UNUSED, Region *region) {
enum State { START, NORMAL, MULTIPLE, COLOR, COLOR1, COLOR3 }; enum State { START, NORMAL, MULTIPLE, COLOR, COLOR1, COLOR3 };
enum State state = START; enum State state = START;

View File

@@ -5,9 +5,13 @@
import base64 import base64
import os import os
import sys import sys
from typing import TYPE_CHECKING, Dict, Generator, Optional from typing import TYPE_CHECKING, Dict, Generator, List, Optional
from kitty.config import parse_send_text_bytes from kitty.config import parse_send_text_bytes
from kitty.key_encoding import (
WindowSystemKeyEvent, decode_key_event_as_window_system_key
)
from kitty.keys import interpret_key_event
from .base import ( from .base import (
MATCH_TAB_OPTION, MATCH_WINDOW_OPTION, ArgsType, Boss, MatchError, MATCH_TAB_OPTION, MATCH_WINDOW_OPTION, ArgsType, Boss, MatchError,
@@ -24,6 +28,8 @@ class SendText(RemoteCommand):
data+: The data being sent. Can be either: text: followed by text or base64: followed by standard base64 encoded bytes data+: The data being sent. Can be either: text: followed by text or base64: followed by standard base64 encoded bytes
match: A string indicating the window to send text to match: A string indicating the window to send text to
match_tab: A string indicating the tab to send text to match_tab: A string indicating the tab to send text to
all: A boolean indicating all windows should be matched.
exclude_active: A boolean that prevents sending text to the active window
''' '''
short_desc = 'Send arbitrary text to specified windows' short_desc = 'Send arbitrary text to specified windows'
desc = ( desc = (
@@ -34,6 +40,11 @@ class SendText(RemoteCommand):
' only the currently active window.' ' only the currently active window.'
) )
options_spec = MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t') + '''\n options_spec = MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t') + '''\n
--all
type=bool-set
Match all windows.
--stdin --stdin
type=bool-set type=bool-set
Read the text to be sent from :italic:`stdin`. Note that in this case the text is sent as is, Read the text to be sent from :italic:`stdin`. Note that in this case the text is sent as is,
@@ -43,16 +54,22 @@ not interpreted for escapes. If stdin is a terminal, you can press Ctrl-D to end
--from-file --from-file
Path to a file whose contents you wish to send. Note that in this case the file contents Path to a file whose contents you wish to send. Note that in this case the file contents
are sent as is, not interpreted for escapes. are sent as is, not interpreted for escapes.
--exclude-active
type=bool-set
Do not send text to the active window, even if it is one of the matched windows.
''' '''
no_response = True no_response = True
argspec = '[TEXT TO SEND]' argspec = '[TEXT TO SEND]'
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType: def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
limit = 1024 limit = 1024
ret = {'match': opts.match, 'data': '', 'match_tab': opts.match_tab} ret = {'match': opts.match, 'data': '', 'match_tab': opts.match_tab, 'all': opts.all, 'exclude_active': opts.exclude_active}
def pipe() -> Generator[Dict, None, None]: def pipe() -> Generator[Dict, None, None]:
if sys.stdin.isatty(): if sys.stdin.isatty():
ret['exclude_active'] = True
import select import select
fd = sys.stdin.fileno() fd = sys.stdin.fileno()
keep_going = True keep_going = True
@@ -109,28 +126,40 @@ are sent as is, not interpreted for escapes.
return chain() return chain()
def response_from_kitty(self, boss: Boss, window: Optional[Window], payload_get: PayloadGetType) -> ResponseType: def response_from_kitty(self, boss: Boss, window: Optional[Window], payload_get: PayloadGetType) -> ResponseType:
windows = [boss.active_window] if payload_get('all'):
match = payload_get('match') windows: List[Optional[Window]] = list(boss.all_windows)
if match: else:
windows = list(boss.match_windows(match)) windows = [boss.active_window]
mt = payload_get('match_tab') match = payload_get('match')
if mt: if match:
windows = [] windows = list(boss.match_windows(match))
tabs = tuple(boss.match_tabs(mt)) mt = payload_get('match_tab')
if not tabs: if mt:
raise MatchError(payload_get('match_tab'), 'tabs') windows = []
for tab in tabs: tabs = tuple(boss.match_tabs(mt))
windows += tuple(tab) if not tabs:
raise MatchError(payload_get('match_tab'), 'tabs')
for tab in tabs:
windows += tuple(tab)
encoding, _, q = payload_get('data').partition(':') encoding, _, q = payload_get('data').partition(':')
if encoding == 'text': if encoding == 'text':
data = q.encode('utf-8') data = q.encode('utf-8')
elif encoding == 'base64': elif encoding == 'base64':
data = base64.standard_b64decode(q) data = base64.standard_b64decode(q)
elif encoding == 'kitty-key':
data = decode_key_event_as_window_system_key(q)
else: else:
raise TypeError(f'Invalid encoding for send-text data: {encoding}') raise TypeError(f'Invalid encoding for send-text data: {encoding}')
exclude_active = payload_get('exclude_active')
for window in windows: for window in windows:
if window is not None: if window is not None:
window.write_to_child(data) if not exclude_active or window is not boss.active_window:
if isinstance(data, WindowSystemKeyEvent):
kdata = interpret_key_event(data.code, 0, data.mods, window, data.action)
if kdata:
window.write_to_child(kdata)
else:
window.write_to_child(data)
send_text = SendText() send_text = SendText()

View File

@@ -155,6 +155,13 @@ def parse_rc_args(args: List[str]) -> Tuple[RCOptions, List[str]]:
return parse_args(args[1:], global_options_spec, 'command ...', msg, '{} @'.format(appname), result_class=RCOptions) return parse_args(args[1:], global_options_spec, 'command ...', msg, '{} @'.format(appname), result_class=RCOptions)
def create_basic_command(name: str, payload: Any = None, no_response: bool = False) -> Dict[str, Any]:
ans = {'cmd': name, 'version': version, 'no_response': no_response}
if payload is not None:
ans['payload'] = payload
return ans
def main(args: List[str]) -> None: def main(args: List[str]) -> None:
global_opts, items = parse_rc_args(args) global_opts, items = parse_rc_args(args)
global_opts.no_command_response = None global_opts.no_command_response = None
@@ -171,17 +178,11 @@ def main(args: List[str]) -> None:
emph(cmd), ', '.join(x.replace('_', '-') for x in all_command_names()))) emph(cmd), ', '.join(x.replace('_', '-') for x in all_command_names())))
opts, items = parse_subcommand_cli(c, items) opts, items = parse_subcommand_cli(c, items)
payload = c.message_to_kitty(global_opts, opts, items) payload = c.message_to_kitty(global_opts, opts, items)
send = {
'cmd': cmd,
'version': version,
}
if payload is not None:
send['payload'] = payload
if global_opts.no_command_response is not None: if global_opts.no_command_response is not None:
no_response = global_opts.no_command_response # type: ignore no_response = global_opts.no_command_response # type: ignore
else: else:
no_response = c.no_response no_response = c.no_response
send['no_response'] = no_response send = create_basic_command(cmd, payload=payload, no_response=no_response)
if not global_opts.to and 'KITTY_LISTEN_ON' in os.environ: if not global_opts.to and 'KITTY_LISTEN_ON' in os.environ:
global_opts.to = os.environ['KITTY_LISTEN_ON'] global_opts.to = os.environ['KITTY_LISTEN_ON']
response = do_io(global_opts.to, send, no_response) response = do_io(global_opts.to, send, no_response)

394
kitty/ringbuf.c Normal file
View File

@@ -0,0 +1,394 @@
/*
* ringbuf.c - C ring buffer (FIFO) implementation.
*
* Written in 2011 by Drew Hess <dhess-src@bothan.net>.
*
* To the extent possible under law, the author(s) have dedicated all
* copyright and related and neighboring rights to this software to
* the public domain worldwide. This software is distributed without
* any warranty.
*
* You should have received a copy of the CC0 Public Domain Dedication
* along with this software. If not, see
* <http://creativecommons.org/publicdomain/zero/1.0/>.
*/
#ifndef KITTY_DEBUG_BUILD
#define NDEBUG 1
#endif
#include "ringbuf.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/param.h>
#include <assert.h>
static inline size_t
size_t_min(size_t x, size_t y) {
return x > y ? y : x;
}
/*
* The code is written for clarity, not cleverness or performance, and
* contains many assert()s to enforce invariant assumptions and catch
* bugs. Feel free to optimize the code and to remove asserts for use
* in your own projects, once you're comfortable that it functions as
* intended.
*/
struct ringbuf_t
{
uint8_t *buf;
uint8_t *head, *tail;
size_t size;
};
ringbuf_t
ringbuf_new(size_t capacity)
{
ringbuf_t rb = malloc(sizeof(struct ringbuf_t));
if (rb) {
/* One byte is used for detecting the full condition. */
rb->size = capacity + 1;
rb->buf = malloc(rb->size);
if (rb->buf)
ringbuf_reset(rb);
else {
free(rb);
return 0;
}
}
return rb;
}
size_t
ringbuf_buffer_size(const struct ringbuf_t *rb)
{
return rb->size;
}
void
ringbuf_reset(ringbuf_t rb)
{
rb->head = rb->tail = rb->buf;
}
void
ringbuf_free(ringbuf_t *rb)
{
assert(rb && *rb);
free((*rb)->buf);
free(*rb);
*rb = 0;
}
size_t
ringbuf_capacity(const struct ringbuf_t *rb)
{
return ringbuf_buffer_size(rb) - 1;
}
/*
* Return a pointer to one-past-the-end of the ring buffer's
* contiguous buffer. You shouldn't normally need to use this function
* unless you're writing a new ringbuf_* function.
*/
static const uint8_t *
ringbuf_end(const struct ringbuf_t *rb)
{
return rb->buf + ringbuf_buffer_size(rb);
}
size_t
ringbuf_bytes_free(const struct ringbuf_t *rb)
{
if (rb->head >= rb->tail)
return ringbuf_capacity(rb) - (rb->head - rb->tail);
else
return rb->tail - rb->head - 1;
}
size_t
ringbuf_bytes_used(const struct ringbuf_t *rb)
{
return ringbuf_capacity(rb) - ringbuf_bytes_free(rb);
}
int
ringbuf_is_full(const struct ringbuf_t *rb)
{
return ringbuf_bytes_free(rb) == 0;
}
int
ringbuf_is_empty(const struct ringbuf_t *rb)
{
return ringbuf_bytes_free(rb) == ringbuf_capacity(rb);
}
const void *
ringbuf_tail(const struct ringbuf_t *rb)
{
return rb->tail;
}
const void *
ringbuf_head(const struct ringbuf_t *rb)
{
return rb->head;
}
/*
* Given a ring buffer rb and a pointer to a location within its
* contiguous buffer, return the a pointer to the next logical
* location in the ring buffer.
*/
static uint8_t *
ringbuf_nextp(ringbuf_t rb, const uint8_t *p)
{
/*
* The assert guarantees the expression (++p - rb->buf) is
* non-negative; therefore, the modulus operation is safe and
* portable.
*/
assert((p >= rb->buf) && (p < ringbuf_end(rb)));
return rb->buf + ((++p - rb->buf) % ringbuf_buffer_size(rb));
}
size_t
ringbuf_findchr(const struct ringbuf_t *rb, int c, size_t offset)
{
const uint8_t *bufend = ringbuf_end(rb);
size_t bytes_used = ringbuf_bytes_used(rb);
if (offset >= bytes_used)
return bytes_used;
const uint8_t *start = rb->buf +
(((rb->tail - rb->buf) + offset) % ringbuf_buffer_size(rb));
assert(bufend > start);
size_t n = size_t_min(bufend - start, bytes_used - offset);
const uint8_t *found = memchr(start, c, n);
if (found)
return offset + (found - start);
else
return ringbuf_findchr(rb, c, offset + n);
}
size_t
ringbuf_memset(ringbuf_t dst, int c, size_t len)
{
const uint8_t *bufend = ringbuf_end(dst);
size_t nwritten = 0;
size_t count = size_t_min(len, ringbuf_buffer_size(dst));
int overflow = count > ringbuf_bytes_free(dst);
while (nwritten != count) {
/* don't copy beyond the end of the buffer */
assert(bufend > dst->head);
size_t n = size_t_min(bufend - dst->head, count - nwritten);
memset(dst->head, c, n);
dst->head += n;
nwritten += n;
/* wrap? */
if (dst->head == bufend)
dst->head = dst->buf;
}
if (overflow) {
dst->tail = ringbuf_nextp(dst, dst->head);
assert(ringbuf_is_full(dst));
}
return nwritten;
}
void *
ringbuf_memcpy_into(ringbuf_t dst, const void *src, size_t count)
{
const uint8_t *u8src = src;
const uint8_t *bufend = ringbuf_end(dst);
int overflow = count > ringbuf_bytes_free(dst);
size_t nread = 0;
while (nread != count) {
/* don't copy beyond the end of the buffer */
assert(bufend > dst->head);
size_t n = size_t_min(bufend - dst->head, count - nread);
memcpy(dst->head, u8src + nread, n);
dst->head += n;
nread += n;
/* wrap? */
if (dst->head == bufend)
dst->head = dst->buf;
}
if (overflow) {
dst->tail = ringbuf_nextp(dst, dst->head);
assert(ringbuf_is_full(dst));
}
return dst->head;
}
ssize_t
ringbuf_read(int fd, ringbuf_t rb, size_t count)
{
const uint8_t *bufend = ringbuf_end(rb);
size_t nfree = ringbuf_bytes_free(rb);
/* don't write beyond the end of the buffer */
assert(bufend > rb->head);
count = size_t_min(bufend - rb->head, count);
ssize_t n = read(fd, rb->head, count);
if (n > 0) {
assert(rb->head + n <= bufend);
rb->head += n;
/* wrap? */
if (rb->head == bufend)
rb->head = rb->buf;
/* fix up the tail pointer if an overflow occurred */
if ((size_t)n > nfree) {
rb->tail = ringbuf_nextp(rb, rb->head);
assert(ringbuf_is_full(rb));
}
}
return n;
}
void *
ringbuf_memmove_from(void *dst, ringbuf_t src, size_t count)
{
size_t bytes_used = ringbuf_bytes_used(src);
if (count > bytes_used)
return 0;
uint8_t *u8dst = dst;
const uint8_t *bufend = ringbuf_end(src);
size_t nwritten = 0;
while (nwritten != count) {
assert(bufend > src->tail);
size_t n = size_t_min(bufend - src->tail, count - nwritten);
memcpy(u8dst + nwritten, src->tail, n);
src->tail += n;
nwritten += n;
/* wrap ? */
if (src->tail == bufend)
src->tail = src->buf;
}
assert(count + ringbuf_bytes_used(src) == bytes_used);
return src->tail;
}
unsigned char
ringbuf_move_char(ringbuf_t src) {
assert(!ringbuf_is_empty(src));
const uint8_t *bufend = ringbuf_end(src);
assert(bufend > src->tail);
uint8_t ans = *src->tail;
src->tail += 1;
if (src->tail == bufend)
src->tail = src->buf;
return ans;
}
size_t
ringbuf_memcpy_from(void *dst, const ringbuf_t src, size_t count)
{
size_t bytes_used = ringbuf_bytes_used(src);
if (count > bytes_used) count = bytes_used;
uint8_t *u8dst = dst;
const uint8_t *bufend = ringbuf_end(src);
size_t nwritten = 0;
const uint8_t* tail = src->tail;
while (nwritten != count) {
assert(bufend > tail);
size_t n = size_t_min(bufend - tail, count - nwritten);
memcpy(u8dst + nwritten, tail, n);
tail += n;
nwritten += n;
/* wrap ? */
if (tail == bufend)
tail = src->buf;
}
assert(ringbuf_bytes_used(src) == bytes_used);
return count;
}
ssize_t
ringbuf_write(int fd, ringbuf_t rb, size_t count)
{
size_t bytes_used = ringbuf_bytes_used(rb);
if (count > bytes_used)
return 0;
const uint8_t *bufend = ringbuf_end(rb);
assert(bufend > rb->head);
count = size_t_min(bufend - rb->tail, count);
ssize_t n = write(fd, rb->tail, count);
if (n > 0) {
assert(rb->tail + n <= bufend);
rb->tail += n;
/* wrap? */
if (rb->tail == bufend)
rb->tail = rb->buf;
assert(n + ringbuf_bytes_used(rb) == bytes_used);
}
return n;
}
void *
ringbuf_copy(ringbuf_t dst, ringbuf_t src, size_t count)
{
size_t src_bytes_used = ringbuf_bytes_used(src);
if (count > src_bytes_used)
return 0;
int overflow = count > ringbuf_bytes_free(dst);
const uint8_t *src_bufend = ringbuf_end(src);
const uint8_t *dst_bufend = ringbuf_end(dst);
size_t ncopied = 0;
while (ncopied != count) {
assert(src_bufend > src->tail);
size_t nsrc = size_t_min(src_bufend - src->tail, count - ncopied);
assert(dst_bufend > dst->head);
size_t n = size_t_min(dst_bufend - dst->head, nsrc);
memcpy(dst->head, src->tail, n);
src->tail += n;
dst->head += n;
ncopied += n;
/* wrap ? */
if (src->tail == src_bufend)
src->tail = src->buf;
if (dst->head == dst_bufend)
dst->head = dst->buf;
}
assert(count + ringbuf_bytes_used(src) == src_bytes_used);
if (overflow) {
dst->tail = ringbuf_nextp(dst, dst->head);
assert(ringbuf_is_full(dst));
}
return dst->head;
}

252
kitty/ringbuf.h Normal file
View File

@@ -0,0 +1,252 @@
#pragma once
/*
* ringbuf.h - C ring buffer (FIFO) interface.
*
* Written in 2011 by Drew Hess <dhess-src@bothan.net>.
*
* To the extent possible under law, the author(s) have dedicated all
* copyright and related and neighboring rights to this software to
* the public domain worldwide. This software is distributed without
* any warranty.
*
* You should have received a copy of the CC0 Public Domain Dedication
* along with this software. If not, see
* <http://creativecommons.org/publicdomain/zero/1.0/>.
*/
/*
* A byte-addressable ring buffer FIFO implementation.
*
* The ring buffer's head pointer points to the starting location
* where data should be written when copying data *into* the buffer
* (e.g., with ringbuf_read). The ring buffer's tail pointer points to
* the starting location where data should be read when copying data
* *from* the buffer (e.g., with ringbuf_write).
*/
#include <stddef.h>
#include <sys/types.h>
typedef struct ringbuf_t *ringbuf_t;
/*
* Create a new ring buffer with the given capacity (usable
* bytes). Note that the actual internal buffer size may be one or
* more bytes larger than the usable capacity, for bookkeeping.
*
* Returns the new ring buffer object, or 0 if there's not enough
* memory to fulfill the request for the given capacity.
*/
ringbuf_t
ringbuf_new(size_t capacity);
/*
* The size of the internal buffer, in bytes. One or more bytes may be
* unusable in order to distinguish the "buffer full" state from the
* "buffer empty" state.
*
* For the usable capacity of the ring buffer, use the
* ringbuf_capacity function.
*/
size_t
ringbuf_buffer_size(const struct ringbuf_t *rb);
/*
* Deallocate a ring buffer, and, as a side effect, set the pointer to
* 0.
*/
void
ringbuf_free(ringbuf_t *rb);
/*
* Reset a ring buffer to its initial state (empty).
*/
void
ringbuf_reset(ringbuf_t rb);
/*
* The usable capacity of the ring buffer, in bytes. Note that this
* value may be less than the ring buffer's internal buffer size, as
* returned by ringbuf_buffer_size.
*/
size_t
ringbuf_capacity(const struct ringbuf_t *rb);
/*
* The number of free/available bytes in the ring buffer. This value
* is never larger than the ring buffer's usable capacity.
*/
size_t
ringbuf_bytes_free(const struct ringbuf_t *rb);
/*
* The number of bytes currently being used in the ring buffer. This
* value is never larger than the ring buffer's usable capacity.
*/
size_t
ringbuf_bytes_used(const struct ringbuf_t *rb);
int
ringbuf_is_full(const struct ringbuf_t *rb);
int
ringbuf_is_empty(const struct ringbuf_t *rb);
/*
* Const access to the head and tail pointers of the ring buffer.
*/
const void *
ringbuf_tail(const struct ringbuf_t *rb);
const void *
ringbuf_head(const struct ringbuf_t *rb);
/*
* Locate the first occurrence of character c (converted to an
* unsigned char) in ring buffer rb, beginning the search at offset
* bytes from the ring buffer's tail pointer. The function returns the
* offset of the character from the ring buffer's tail pointer, if
* found. If c does not occur in the ring buffer, the function returns
* the number of bytes used in the ring buffer.
*
* Note that the offset parameter and the returned offset are logical
* offsets from the tail pointer, not necessarily linear offsets.
*/
size_t
ringbuf_findchr(const struct ringbuf_t *rb, int c, size_t offset);
/*
* Beginning at ring buffer dst's head pointer, fill the ring buffer
* with a repeating sequence of len bytes, each of value c (converted
* to an unsigned char). len can be as large as you like, but the
* function will never write more than ringbuf_buffer_size(dst) bytes
* in a single invocation, since that size will cause all bytes in the
* ring buffer to be written exactly once each.
*
* Note that if len is greater than the number of free bytes in dst,
* the ring buffer will overflow. When an overflow occurs, the state
* of the ring buffer is guaranteed to be consistent, including the
* head and tail pointers; old data will simply be overwritten in FIFO
* fashion, as needed. However, note that, if calling the function
* results in an overflow, the value of the ring buffer's tail pointer
* may be different than it was before the function was called.
*
* Returns the actual number of bytes written to dst: len, if
* len < ringbuf_buffer_size(dst), else ringbuf_buffer_size(dst).
*/
size_t
ringbuf_memset(ringbuf_t dst, int c, size_t len);
/*
* Copy n bytes from a contiguous memory area src into the ring buffer
* dst. Returns the ring buffer's new head pointer.
*
* It is possible to copy more data from src than is available in the
* buffer; i.e., it's possible to overflow the ring buffer using this
* function. When an overflow occurs, the state of the ring buffer is
* guaranteed to be consistent, including the head and tail pointers;
* old data will simply be overwritten in FIFO fashion, as
* needed. However, note that, if calling the function results in an
* overflow, the value of the ring buffer's tail pointer may be
* different than it was before the function was called.
*/
void *
ringbuf_memcpy_into(ringbuf_t dst, const void *src, size_t count);
/*
* This convenience function calls read(2) on the file descriptor fd,
* using the ring buffer rb as the destination buffer for the read,
* and returns the value returned by read(2). It will only call
* read(2) once, and may return a short count.
*
* It is possible to read more data from the file descriptor than is
* available in the buffer; i.e., it's possible to overflow the ring
* buffer using this function. When an overflow occurs, the state of
* the ring buffer is guaranteed to be consistent, including the head
* and tail pointers: old data will simply be overwritten in FIFO
* fashion, as needed. However, note that, if calling the function
* results in an overflow, the value of the ring buffer's tail pointer
* may be different than it was before the function was called.
*/
ssize_t
ringbuf_read(int fd, ringbuf_t rb, size_t count);
/*
* Copy n bytes from the ring buffer src, starting from its tail
* pointer, into a contiguous memory area dst. Returns the value of
* src's tail pointer after the copy is finished.
*
* Note that this copy is destructive with respect to the ring buffer:
* the n bytes copied from the ring buffer are no longer available in
* the ring buffer after the copy is complete, and the ring buffer
* will have n more free bytes than it did before the function was
* called.
*
* This function will *not* allow the ring buffer to underflow. If
* count is greater than the number of bytes used in the ring buffer,
* no bytes are copied, and the function will return 0.
*/
void *
ringbuf_memmove_from(void *dst, ringbuf_t src, size_t count);
/* ringbuf_memmove_from() optimized for a single character.
* Must only be called if the ringbuf is not empty */
unsigned char
ringbuf_move_char(ringbuf_t src);
/*
* Same as ringbuf_memmove_from() except that it does not change the ringbuffer
* and returns the actual number of bytes copied, which is the minimum of ringbuf_bytes_used
* and count.
*/
size_t
ringbuf_memcpy_from(void *dst, const ringbuf_t src, size_t count);
/*
* This convenience function calls write(2) on the file descriptor fd,
* using the ring buffer rb as the source buffer for writing (starting
* at the ring buffer's tail pointer), and returns the value returned
* by write(2). It will only call write(2) once, and may return a
* short count.
*
* Note that this copy is destructive with respect to the ring buffer:
* any bytes written from the ring buffer to the file descriptor are
* no longer available in the ring buffer after the copy is complete,
* and the ring buffer will have N more free bytes than it did before
* the function was called, where N is the value returned by the
* function (unless N is < 0, in which case an error occurred and no
* bytes were written).
*
* This function will *not* allow the ring buffer to underflow. If
* count is greater than the number of bytes used in the ring buffer,
* no bytes are written to the file descriptor, and the function will
* return 0.
*/
ssize_t
ringbuf_write(int fd, ringbuf_t rb, size_t count);
/*
* Copy count bytes from ring buffer src, starting from its tail
* pointer, into ring buffer dst. Returns dst's new head pointer after
* the copy is finished.
*
* Note that this copy is destructive with respect to the ring buffer
* src: any bytes copied from src into dst are no longer available in
* src after the copy is complete, and src will have 'count' more free
* bytes than it did before the function was called.
*
* It is possible to copy more data from src than is available in dst;
* i.e., it's possible to overflow dst using this function. When an
* overflow occurs, the state of dst is guaranteed to be consistent,
* including the head and tail pointers; old data will simply be
* overwritten in FIFO fashion, as needed. However, note that, if
* calling the function results in an overflow, the value dst's tail
* pointer may be different than it was before the function was
* called.
*
* It is *not* possible to underflow src; if count is greater than the
* number of bytes used in src, no bytes are copied, and the function
* returns 0.
*/
void *
ringbuf_copy(ringbuf_t dst, ringbuf_t src, size_t count);

View File

@@ -149,6 +149,8 @@ void
screen_reset(Screen *self) { screen_reset(Screen *self) {
if (self->linebuf == self->alt_linebuf) screen_toggle_screen_buffer(self, true, true); if (self->linebuf == self->alt_linebuf) screen_toggle_screen_buffer(self, true, true);
if (self->overlay_line.is_active) deactivate_overlay_line(self); if (self->overlay_line.is_active) deactivate_overlay_line(self);
self->main_savepoint.is_valid = false;
self->alt_savepoint.is_valid = false;
linebuf_clear(self->linebuf, BLANK_CHAR); linebuf_clear(self->linebuf, BLANK_CHAR);
historybuf_clear(self->historybuf); historybuf_clear(self->historybuf);
clear_hyperlink_pool(self->hyperlink_pool); clear_hyperlink_pool(self->hyperlink_pool);
@@ -999,8 +1001,8 @@ index_selection(const Screen *self, Selections *selections, bool up) {
if (self->overlay_line.is_active) deactivate_overlay_line(self); \ if (self->overlay_line.is_active) deactivate_overlay_line(self); \
linebuf_index(self->linebuf, top, bottom); \ linebuf_index(self->linebuf, top, bottom); \
INDEX_GRAPHICS(-1) \ INDEX_GRAPHICS(-1) \
if (self->linebuf == self->main_linebuf && bottom == self->lines - 1) { \ if (self->linebuf == self->main_linebuf && self->margin_top == 0) { \
/* Only add to history when no page margins have been set */ \ /* Only add to history when no top margin has been set */ \
linebuf_init_line(self->linebuf, bottom); \ linebuf_init_line(self->linebuf, bottom); \
historybuf_add_line(self->historybuf, self->linebuf->line, &self->as_ansi_buf); \ historybuf_add_line(self->historybuf, self->linebuf->line, &self->as_ansi_buf); \
self->history_line_added_count++; \ self->history_line_added_count++; \
@@ -1096,14 +1098,13 @@ screen_linefeed(Screen *self) {
void void
screen_save_cursor(Screen *self) { screen_save_cursor(Screen *self) {
SavepointBuffer *pts = self->linebuf == self->main_linebuf ? &self->main_savepoints : &self->alt_savepoints; Savepoint *sp = self->linebuf == self->main_linebuf ? &self->main_savepoint : &self->alt_savepoint;
Savepoint *sp;
buffer_push(pts, sp);
cursor_copy_to(self->cursor, &(sp->cursor)); cursor_copy_to(self->cursor, &(sp->cursor));
sp->mDECOM = self->modes.mDECOM; sp->mDECOM = self->modes.mDECOM;
sp->mDECAWM = self->modes.mDECAWM; sp->mDECAWM = self->modes.mDECAWM;
sp->mDECSCNM = self->modes.mDECSCNM; sp->mDECSCNM = self->modes.mDECSCNM;
COPY_CHARSETS(self, sp); COPY_CHARSETS(self, sp);
sp->is_valid = true;
} }
void void
@@ -1115,10 +1116,8 @@ screen_save_modes(Screen *self) {
void void
screen_restore_cursor(Screen *self) { screen_restore_cursor(Screen *self) {
SavepointBuffer *pts = self->linebuf == self->main_linebuf ? &self->main_savepoints : &self->alt_savepoints; Savepoint *sp = self->linebuf == self->main_linebuf ? &self->main_savepoint : &self->alt_savepoint;
Savepoint *sp; if (!sp->is_valid) {
buffer_pop(pts, sp);
if (sp == NULL) {
screen_cursor_position(self, 1, 1); screen_cursor_position(self, 1, 1);
screen_reset_mode(self, DECOM); screen_reset_mode(self, DECOM);
RESET_CHARSETS; RESET_CHARSETS;
@@ -2138,6 +2137,20 @@ draw(Screen *self, PyObject *src) {
Py_RETURN_NONE; Py_RETURN_NONE;
} }
extern void
parse_sgr(Screen *screen, uint32_t *buf, unsigned int num, unsigned int *params, PyObject *dump_callback, const char *report_name, Region *region);
static PyObject*
apply_sgr(Screen *self, PyObject *src) {
if (!PyUnicode_Check(src)) { PyErr_SetString(PyExc_TypeError, "A unicode string is required"); return NULL; }
if (PyUnicode_READY(src) != 0) { return PyErr_NoMemory(); }
Py_UCS4 *buf = PyUnicode_AsUCS4Copy(src);
if (!buf) return NULL;
unsigned int params[MAX_PARAMS] = {0};
parse_sgr(self, buf, PyUnicode_GET_LENGTH(src), params, NULL, "parse_sgr", NULL);
Py_RETURN_NONE;
}
static PyObject* static PyObject*
reset_mode(Screen *self, PyObject *args) { reset_mode(Screen *self, PyObject *args) {
int private = false; int private = false;
@@ -2784,6 +2797,7 @@ static PyMethodDef methods[] = {
MND(visual_line, METH_VARARGS) MND(visual_line, METH_VARARGS)
MND(current_url_text, METH_NOARGS) MND(current_url_text, METH_NOARGS)
MND(draw, METH_O) MND(draw, METH_O)
MND(apply_sgr, METH_O)
MND(cursor_position, METH_VARARGS) MND(cursor_position, METH_VARARGS)
MND(set_mode, METH_VARARGS) MND(set_mode, METH_VARARGS)
MND(reset_mode, METH_VARARGS) MND(reset_mode, METH_VARARGS)

View File

@@ -8,6 +8,7 @@
#include "graphics.h" #include "graphics.h"
#include "monotonic.h" #include "monotonic.h"
#define MAX_PARAMS 256
typedef enum ScrollTypes { SCROLL_LINE = -999999, SCROLL_PAGE, SCROLL_FULL } ScrollType; typedef enum ScrollTypes { SCROLL_LINE = -999999, SCROLL_PAGE, SCROLL_FULL } ScrollType;
@@ -58,16 +59,10 @@ typedef struct {
bool use_latin1; bool use_latin1;
Cursor cursor; Cursor cursor;
bool mDECOM, mDECAWM, mDECSCNM; bool mDECOM, mDECAWM, mDECSCNM;
bool is_valid;
} Savepoint; } Savepoint;
typedef struct {
Savepoint buf[SAVEPOINTS_SZ];
index_type start_of_data, count;
} SavepointBuffer;
typedef struct { typedef struct {
ScreenModes buf[SAVEPOINTS_SZ]; ScreenModes buf[SAVEPOINTS_SZ];
index_type start_of_data, count; index_type start_of_data, count;
@@ -97,7 +92,7 @@ typedef struct {
} last_rendered; } last_rendered;
bool use_latin1, is_dirty, scroll_changed, reload_all_gpu_data; bool use_latin1, is_dirty, scroll_changed, reload_all_gpu_data;
Cursor *cursor; Cursor *cursor;
SavepointBuffer main_savepoints, alt_savepoints; Savepoint main_savepoint, alt_savepoint;
SavemodesBuffer modes_savepoints; SavemodesBuffer modes_savepoints;
PyObject *callbacks, *test_child; PyObject *callbacks, *test_child;
LineBuf *linebuf, *main_linebuf, *alt_linebuf; LineBuf *linebuf, *main_linebuf, *alt_linebuf;

View File

@@ -705,6 +705,7 @@ PYWRAP1(set_options) {
S(pointer_shape_when_grabbed, pointer_shape); S(pointer_shape_when_grabbed, pointer_shape);
S(default_pointer_shape, pointer_shape); S(default_pointer_shape, pointer_shape);
S(pointer_shape_when_dragging, pointer_shape); S(pointer_shape_when_dragging, pointer_shape);
S(detect_urls, PyObject_IsTrue);
GA(tab_bar_style); GA(tab_bar_style);
global_state.tab_bar_hidden = PyUnicode_CompareWithASCIIString(ret, "hidden") == 0 ? true: false; global_state.tab_bar_hidden = PyUnicode_CompareWithASCIIString(ret, "hidden") == 0 ? true: false;

View File

@@ -72,6 +72,7 @@ typedef struct {
UrlPrefix *values; UrlPrefix *values;
size_t num, max_prefix_len; size_t num, max_prefix_len;
} url_prefixes; } url_prefixes;
bool detect_urls;
} Options; } Options;
typedef struct { typedef struct {

View File

@@ -13,7 +13,7 @@ from .fast_data_types import (
) )
from .layout.base import Rect from .layout.base import Rect
from .options_stub import Options from .options_stub import Options
from .rgb import Color, alpha_blend, color_from_int from .rgb import Color, alpha_blend, color_as_sgr, color_from_int, to_color
from .utils import color_as_int, log_error from .utils import color_as_int, log_error
from .window import calculate_gl_geometry from .window import calculate_gl_geometry
@@ -61,6 +61,35 @@ def compile_template(template: str) -> Any:
report_template_failure(template, str(e)) report_template_failure(template, str(e))
class ColorFormatter:
def __init__(self, which: str):
self.which = which
def __getattr__(self, name: str) -> str:
q = name
if q == 'default':
ans = '9'
else:
if name.startswith('_'):
q = '#' + name[1:]
c = to_color(q)
if c is None:
raise AttributeError(f'{name} is not a valid color')
ans = '8' + color_as_sgr(c)
return f'\x1b[{self.which}{ans}m'
class Formatter:
reset = '\x1b[0m'
fg = ColorFormatter('3')
bg = ColorFormatter('4')
bold = '\x1b[1m'
nobold = '\x1b[22m'
italic = '\x1b[3m'
noitalic = '\x1b[23m'
def draw_title(draw_data: DrawData, screen: Screen, tab: TabBarData, index: int) -> None: def draw_title(draw_data: DrawData, screen: Screen, tab: TabBarData, index: int) -> None:
if tab.needs_attention and draw_data.bell_on_tab: if tab.needs_attention and draw_data.bell_on_tab:
fg = screen.cursor.fg fg = screen.cursor.fg
@@ -81,13 +110,22 @@ def draw_title(draw_data: DrawData, screen: Screen, tab: TabBarData, index: int)
'index': index, 'index': index,
'layout_name': tab.layout_name, 'layout_name': tab.layout_name,
'num_windows': tab.num_windows, 'num_windows': tab.num_windows,
'title': tab.title 'title': tab.title,
'fmt': Formatter,
} }
title = eval(compile_template(template), {'__builtins__': {}}, eval_locals) title = eval(compile_template(template), {'__builtins__': {}}, eval_locals)
except Exception as e: except Exception as e:
report_template_failure(template, str(e)) report_template_failure(template, str(e))
title = tab.title title = tab.title
screen.draw(title) if '\x1b' in title:
import re
for x in re.split('(\x1b\\[[^m]*m)', title):
if x.startswith('\x1b') and x.endswith('m'):
screen.apply_sgr(x[2:-1])
else:
screen.draw(x)
else:
screen.draw(title)
def draw_tab_with_separator(draw_data: DrawData, screen: Screen, tab: TabBarData, before: int, max_title_length: int, index: int, is_last: bool) -> int: def draw_tab_with_separator(draw_data: DrawData, screen: Screen, tab: TabBarData, before: int, max_title_length: int, index: int, is_last: bool) -> int:

View File

@@ -721,6 +721,8 @@ class TabManager: # {{{
break break
elif self.opts.tab_switch_strategy == 'left': elif self.opts.tab_switch_strategy == 'left':
next_active_tab = max(0, self.active_tab_idx - 1) next_active_tab = max(0, self.active_tab_idx - 1)
elif self.opts.tab_switch_strategy == 'right':
next_active_tab = min(self.active_tab_idx, len(self.tabs) - 1)
if next_active_tab < 0: if next_active_tab < 0:
next_active_tab = max(0, min(self.active_tab_idx, len(self.tabs) - 1)) next_active_tab = max(0, min(self.active_tab_idx, len(self.tabs) - 1))

View File

@@ -241,7 +241,7 @@ def text_sanitizer(as_ansi: bool, add_wrap_markers: bool) -> Callable[[str], str
pat = getattr(text_sanitizer, 'pat', None) pat = getattr(text_sanitizer, 'pat', None)
if pat is None: if pat is None:
import re import re
pat = re.compile(r'\033\[.+?m') pat = re.compile('\033\\[.*?m')
setattr(text_sanitizer, 'pat', pat) setattr(text_sanitizer, 'pat', pat)
ansi, wrap_markers = not as_ansi, not add_wrap_markers ansi, wrap_markers = not as_ansi, not add_wrap_markers

View File

@@ -48,6 +48,15 @@ def parse_response(res):
return res.decode('ascii').partition(';')[2].partition('\033')[0] return res.decode('ascii').partition(';')[2].partition('\033')[0]
def parse_response_with_ids(res):
if not res:
return
a, b = res.decode('ascii').split(';', 1)
code = b.partition('\033')[0].split(':', 1)[0]
a = a.split('G', 1)[1]
return code, a
all_bytes = bytes(bytearray(range(256))) all_bytes = bytes(bytearray(range(256)))
@@ -90,20 +99,23 @@ def put_helpers(self, cw, ch):
s = self.create_screen(10, 5, cell_width=cw, cell_height=ch) s = self.create_screen(10, 5, cell_width=cw, cell_height=ch)
return s, 2 / s.columns, 2 / s.lines return s, 2 / s.columns, 2 / s.lines
def put_cmd(z=0, num_cols=0, num_lines=0, x_off=0, y_off=0, width=0, height=0, cell_x_off=0, cell_y_off=0): def put_cmd(z=0, num_cols=0, num_lines=0, x_off=0, y_off=0, width=0, height=0, cell_x_off=0, cell_y_off=0, placement_id=0):
return 'z=%d,c=%d,r=%d,x=%d,y=%d,w=%d,h=%d,X=%d,Y=%d' % (z, num_cols, num_lines, x_off, y_off, width, height, cell_x_off, cell_y_off) return 'z=%d,c=%d,r=%d,x=%d,y=%d,w=%d,h=%d,X=%d,Y=%d,p=%d' % (
z, num_cols, num_lines, x_off, y_off, width, height, cell_x_off, cell_y_off, placement_id)
def put_image(screen, w, h, **kw): def put_image(screen, w, h, **kw):
nonlocal iid nonlocal iid
iid += 1 iid += 1
cmd = 'a=T,f=24,i=%d,s=%d,v=%d,%s' % (iid, w, h, put_cmd(**kw)) imgid = kw.pop('id', None) or iid
cmd = 'a=T,f=24,i=%d,s=%d,v=%d,%s' % (imgid, w, h, put_cmd(**kw))
data = b'x' * w * h * 3 data = b'x' * w * h * 3
res = send_command(screen, cmd, data) res = send_command(screen, cmd, data)
return iid, parse_response(res) return imgid, parse_response(res)
def put_ref(screen, **kw): def put_ref(screen, **kw):
cmd = 'a=p,i=%d,%s' % (iid, put_cmd(**kw)) imgid = kw.pop('id', None) or iid
send_command(screen, cmd) cmd = 'a=p,i=%d,%s' % (imgid, put_cmd(**kw))
return imgid, parse_response_with_ids(send_command(screen, cmd))
def layers(screen, scrolled_by=0, xstart=-1, ystart=1): def layers(screen, scrolled_by=0, xstart=-1, ystart=1):
return screen.grman.update_layers(scrolled_by, xstart, ystart, dx, dy, screen.columns, screen.lines, cw, ch) return screen.grman.update_layers(scrolled_by, xstart, ystart, dx, dy, screen.columns, screen.lines, cw, ch)
@@ -125,6 +137,7 @@ class TestGraphics(BaseTest):
# Test load query # Test load query
self.ae(l('abcd', s=1, v=1, a='q'), 'OK') self.ae(l('abcd', s=1, v=1, a='q'), 'OK')
self.assertIsNone(l('abcd', s=1, v=1, a='q', q=1))
self.ae(g.image_count, 0) self.ae(g.image_count, 0)
# Test simple load # Test simple load
@@ -140,6 +153,8 @@ class TestGraphics(BaseTest):
self.ae(l('mnop', m=0), 'OK') self.ae(l('mnop', m=0), 'OK')
img = g.image_for_client_id(1) img = g.image_for_client_id(1)
self.ae(img['data'], b'abcdefghijklmnop') self.ae(img['data'], b'abcdefghijklmnop')
self.ae(l('abcd', s=10, v=10, q=1), 'ENODATA:Insufficient image data: 4 < 400')
self.ae(l('abcd', s=10, v=10, q=2), None)
# Test compression # Test compression
random_data = byte_block(3 * 1024) random_data = byte_block(3 * 1024)
@@ -216,6 +231,73 @@ class TestGraphics(BaseTest):
# test error handling for loading bad png data # test error handling for loading bad png data
self.assertRaisesRegex(ValueError, '[EBADPNG]', load_png_data, b'dsfsdfsfsfd') self.assertRaisesRegex(ValueError, '[EBADPNG]', load_png_data, b'dsfsdfsfsfd')
def test_gr_operations_with_numbers(self):
s = self.create_screen()
g = s.grman
def li(payload, **kw):
cmd = ','.join('%s=%s' % (k, v) for k, v in kw.items())
res = send_command(s, cmd, payload)
return parse_response_with_ids(res)
code, ids = li('abc', s=1, v=1, f=24, I=1, i=3)
self.ae(code, 'EINVAL')
code, ids = li('abc', s=1, v=1, f=24, I=1)
self.ae((code, ids), ('OK', 'i=1,I=1'))
img = g.image_for_client_number(1)
self.ae(img['client_number'], 1)
self.ae(img['client_id'], 1)
code, ids = li('abc', s=1, v=1, f=24, I=1)
self.ae((code, ids), ('OK', 'i=2,I=1'))
img = g.image_for_client_number(1)
self.ae(img['client_number'], 1)
self.ae(img['client_id'], 2)
code, ids = li('abc', s=1, v=1, f=24, I=1)
self.ae((code, ids), ('OK', 'i=3,I=1'))
code, ids = li('abc', s=1, v=1, f=24, i=5)
self.ae((code, ids), ('OK', 'i=5'))
code, ids = li('abc', s=1, v=1, f=24, I=3)
self.ae((code, ids), ('OK', 'i=4,I=3'))
# Test chunked load with number
self.assertIsNone(li('abcd', s=2, v=2, m=1, I=93))
self.assertIsNone(li('efgh', m=1))
self.assertIsNone(li('ijkx', m=1))
self.ae(li('mnop', m=0), ('OK', 'i=6,I=93'))
img = g.image_for_client_number(93)
self.ae(img['data'], b'abcdefghijkxmnop')
self.ae(img['client_id'], 6)
# test put with number
def put(**kw):
cmd = ','.join('%s=%s' % (k, v) for k, v in kw.items())
cmd = 'a=p,' + cmd
return parse_response_with_ids(send_command(s, cmd))
code, idstr = put(c=2, r=2, I=93)
self.ae((code, idstr), ('OK', 'i=6,I=93'))
code, idstr = put(c=2, r=2, I=94)
self.ae(code, 'ENOENT')
# test delete with number
def delete(ac='N', **kw):
cmd = 'a=d'
if ac:
cmd += ',d={}'.format(ac)
if kw:
cmd += ',' + ','.join('{}={}'.format(k, v) for k, v in kw.items())
send_command(s, cmd)
count = s.grman.image_count
put(i=1), put(i=2), put(i=3), put(i=4), put(i=5)
delete(I=94)
self.ae(s.grman.image_count, count)
delete(I=93)
self.ae(s.grman.image_count, count - 1)
delete(I=1)
self.ae(s.grman.image_count, count - 2)
def test_image_put(self): def test_image_put(self):
cw, ch = 10, 20 cw, ch = 10, 20
s, dx, dy, put_image, put_ref, layers, rect_eq = put_helpers(self, cw, ch) s, dx, dy, put_image, put_ref, layers, rect_eq = put_helpers(self, cw, ch)
@@ -226,7 +308,8 @@ class TestGraphics(BaseTest):
rect_eq(l0[0]['dest_rect'], -1, 1, -1 + dx, 1 - dy) rect_eq(l0[0]['dest_rect'], -1, 1, -1 + dx, 1 - dy)
self.ae(l0[0]['group_count'], 1) self.ae(l0[0]['group_count'], 1)
self.ae(s.cursor.x, 1), self.ae(s.cursor.y, 0) self.ae(s.cursor.x, 1), self.ae(s.cursor.y, 0)
put_ref(s, num_cols=s.columns, x_off=2, y_off=1, width=3, height=5, cell_x_off=3, cell_y_off=1, z=-1) iid, (code, idstr) = put_ref(s, num_cols=s.columns, x_off=2, y_off=1, width=3, height=5, cell_x_off=3, cell_y_off=1, z=-1, placement_id=17)
self.ae(idstr, f'i={iid},p=17')
l2 = layers(s) l2 = layers(s)
self.ae(len(l2), 2) self.ae(len(l2), 2)
rect_eq(l2[0]['src_rect'], 2 / 10, 1 / 20, (2 + 3) / 10, (1 + 5)/20) rect_eq(l2[0]['src_rect'], 2 / 10, 1 / 20, (2 + 3) / 10, (1 + 5)/20)
@@ -320,8 +403,13 @@ class TestGraphics(BaseTest):
delete('A') delete('A')
self.ae(s.grman.image_count, 0) self.ae(s.grman.image_count, 0)
iid = put_image(s, cw, ch)[0] iid = put_image(s, cw, ch)[0]
delete('I', i=iid, p=7)
self.ae(s.grman.image_count, 1)
delete('I', i=iid) delete('I', i=iid)
self.ae(s.grman.image_count, 0) self.ae(s.grman.image_count, 0)
iid = put_image(s, cw, ch, placement_id=9)[0]
delete('I', i=iid, p=9)
self.ae(s.grman.image_count, 0)
s.reset() s.reset()
put_image(s, cw, ch) put_image(s, cw, ch)
put_image(s, cw, ch) put_image(s, cw, ch)
@@ -335,3 +423,14 @@ class TestGraphics(BaseTest):
put_image(s, cw, ch, z=9) put_image(s, cw, ch, z=9)
delete('Z', z=9) delete('Z', z=9)
self.ae(s.grman.image_count, 0) self.ae(s.grman.image_count, 0)
# test put + delete + put
iid = 999999
self.ae(put_image(s, cw, ch, id=iid), (iid, 'OK'))
self.ae(put_ref(s, id=iid), (iid, ('OK', f'i={iid}')))
delete('i', i=iid)
self.ae(s.grman.image_count, 1)
self.ae(put_ref(s, id=iid), (iid, ('OK', f'i={iid}')))
delete('I', i=iid)
self.ae(put_ref(s, id=iid), (iid, ('ENOENT', f'i={iid}')))
self.ae(s.grman.image_count, 0)

View File

@@ -379,7 +379,7 @@ class TestParser(BaseTest):
for f in 'action delete_action transmission_type compressed'.split(): for f in 'action delete_action transmission_type compressed'.split():
k.setdefault(f, b'\0') k.setdefault(f, b'\0')
for f in ('format more id data_sz data_offset width height x_offset y_offset data_height data_width' for f in ('format more id data_sz data_offset width height x_offset y_offset data_height data_width'
' num_cells num_lines cell_x_offset cell_y_offset z_index').split(): ' num_cells num_lines cell_x_offset cell_y_offset z_index placement_id image_number quiet').split():
k.setdefault(f, 0) k.setdefault(f, 0)
p = k.pop('payload', '').encode('utf-8') p = k.pop('payload', '').encode('utf-8')
k['payload_sz'] = len(p) k['payload_sz'] = len(p)
@@ -395,11 +395,12 @@ class TestParser(BaseTest):
pb = partial(self.parse_bytes_dump, s) pb = partial(self.parse_bytes_dump, s)
uint32_max = 2**32 - 1 uint32_max = 2**32 - 1
t('i=%d' % uint32_max, id=uint32_max) t('i=%d' % uint32_max, id=uint32_max)
t('i=3,p=4', id=3, placement_id=4)
e('i=%d' % (uint32_max + 1), 'Malformed GraphicsCommand control block, number is too large') e('i=%d' % (uint32_max + 1), 'Malformed GraphicsCommand control block, number is too large')
pb('\033_Gi=12\033\\', c(id=12)) pb('\033_Gi=12\033\\', c(id=12))
t('a=t,t=d,s=100,z=-9', payload='X', action='t', transmission_type='d', data_width=100, z_index=-9, payload_sz=1) t('a=t,t=d,s=100,z=-9', payload='X', action='t', transmission_type='d', data_width=100, z_index=-9, payload_sz=1)
t('a=t,t=d,s=100,z=9', payload='payload', action='t', transmission_type='d', data_width=100, z_index=9, payload_sz=7) t('a=t,t=d,s=100,z=9', payload='payload', action='t', transmission_type='d', data_width=100, z_index=9, payload_sz=7)
t('a=t,t=d,s=100,z=9', action='t', transmission_type='d', data_width=100, z_index=9) t('a=t,t=d,s=100,z=9,q=2', action='t', transmission_type='d', data_width=100, z_index=9, quiet=2)
e(',s=1', 'Malformed GraphicsCommand control block, invalid key character: 0x2c') e(',s=1', 'Malformed GraphicsCommand control block, invalid key character: 0x2c')
e('W=1', 'Malformed GraphicsCommand control block, invalid key character: 0x57') e('W=1', 'Malformed GraphicsCommand control block, invalid key character: 0x57')
e('1=1', 'Malformed GraphicsCommand control block, invalid key character: 0x31') e('1=1', 'Malformed GraphicsCommand control block, invalid key character: 0x31')

View File

@@ -683,3 +683,36 @@ class TestScreen(BaseTest):
self.ae(s.current_url_text(), '123abcxyz') self.ae(s.current_url_text(), '123abcxyz')
self.ae('2', s.hyperlink_at(1, 3)) self.ae('2', s.hyperlink_at(1, 3))
self.ae(s.current_url_text(), 'Z Z') self.ae(s.current_url_text(), 'Z Z')
def test_bottom_margin(self):
s = self.create_screen(cols=80, lines=6, scrollback=4)
s.set_margins(0, 5)
for i in range(8):
s.draw(str(i))
s.linefeed()
s.carriage_return()
self.ae(str(s.linebuf), '4\n5\n6\n7\n\n')
self.ae(str(s.historybuf), '3\n2\n1\n0')
def test_top_margin(self):
s = self.create_screen(cols=80, lines=6, scrollback=4)
s.set_margins(2, 6)
for i in range(8):
s.draw(str(i))
s.linefeed()
s.carriage_return()
self.ae(str(s.linebuf), '0\n4\n5\n6\n7\n')
self.ae(str(s.historybuf), '')
def test_top_and_bottom_margin(self):
s = self.create_screen(cols=80, lines=6, scrollback=4)
s.set_margins(2, 5)
for i in range(8):
s.draw(str(i))
s.linefeed()
s.carriage_return()
self.ae(str(s.linebuf), '0\n5\n6\n7\n\n')
self.ae(str(s.historybuf), '')

View File

@@ -13,6 +13,7 @@ import shutil
import subprocess import subprocess
import sys import sys
import sysconfig import sysconfig
import platform
import time import time
from contextlib import suppress from contextlib import suppress
from functools import partial from functools import partial
@@ -47,6 +48,7 @@ version = tuple(
_plat = sys.platform.lower() _plat = sys.platform.lower()
is_macos = 'darwin' in _plat is_macos = 'darwin' in _plat
is_openbsd = 'openbsd' in _plat is_openbsd = 'openbsd' in _plat
is_arm = platform.processor() == 'arm'
Env = glfw.Env Env = glfw.Env
env = Env() env = Env()
PKGCONFIG = os.environ.get('PKGCONFIG_EXE', 'pkg-config') PKGCONFIG = os.environ.get('PKGCONFIG_EXE', 'pkg-config')
@@ -246,6 +248,10 @@ def init_env(
extra_logging: Iterable[str] = () extra_logging: Iterable[str] = ()
) -> Env: ) -> Env:
native_optimizations = native_optimizations and not sanitize and not debug native_optimizations = native_optimizations and not sanitize and not debug
if native_optimizations and is_macos and is_arm:
# see https://github.com/kovidgoyal/kitty/issues/3126
# -march=native is not supported when targeting Apple Silicon
native_optimizations = False
cc, ccver = cc_version() cc, ccver = cc_version()
print('CC:', cc, ccver) print('CC:', cc, ccver)
stack_protector = first_successful_compile(cc, '-fstack-protector-strong', '-fstack-protector') stack_protector = first_successful_compile(cc, '-fstack-protector-strong', '-fstack-protector')
@@ -298,6 +304,9 @@ def init_env(
cflags.append('-flto') cflags.append('-flto')
ldflags.append('-flto') ldflags.append('-flto')
if debug:
cflags.append('-DKITTY_DEBUG_BUILD')
if profile: if profile:
cppflags.append('-DWITH_PROFILER') cppflags.append('-DWITH_PROFILER')
cflags.append('-g3') cflags.append('-g3')