Compare commits

..

1 Commits
v0.42.2 ... cli

Author SHA1 Message Date
Kovid Goyal
2f8c392571 Cleanup reading of argv from files 2025-04-28 09:52:26 +05:30
326 changed files with 14443 additions and 13208 deletions

1
.ignore Normal file
View File

@@ -0,0 +1 @@
kittens/unicode_input/names.h

View File

@@ -50,7 +50,7 @@ def run_parsing_benchmark(cell_width: int = 10, cell_height: int = 20, scrollbac
screen = Screen(None, rows, columns, scrollback, cell_width, cell_height, 0, ToChild())
def parse_bytes(data: bytes|memoryview) -> None:
def parse_bytes(data: bytes) -> None:
data = memoryview(data)
while data:
dest = screen.test_create_write_buffer()

View File

@@ -302,9 +302,9 @@
"name": "wayland",
"os": "linux",
"unix": {
"filename": "wayland-1.23.1.tar.xz",
"hash": "sha256:864fb2a8399e2d0ec39d56e9d9b753c093775beadc6022ce81f441929a81e5ed",
"urls": ["https://gitlab.freedesktop.org/wayland/wayland/-/releases/1.23.1/downloads/{filename}"]
"filename": "wayland-1.23.0.tar.xz",
"hash": "sha256:05b3e1574d3e67626b5974f862f36b5b427c7ceeb965cb36a4e6c2d342e45ab2",
"urls": ["https://gitlab.freedesktop.org/wayland/wayland/-/releases/1.23.0/downloads/{filename}"]
}
},
@@ -312,9 +312,9 @@
"name": "wayland-protocols",
"os": "linux",
"unix": {
"filename": "wayland-protocols-1.44.tar.xz",
"hash": "sha256:3df1107ecf8bfd6ee878aeca5d3b7afd81248a48031e14caf6ae01f14eebb50e",
"urls": ["https://gitlab.freedesktop.org/wayland/wayland-protocols/-/releases/1.44/downloads/{filename}"]
"filename": "wayland-protocols-1.41.tar.xz",
"hash": "sha256:2786b6b1b79965e313f2c289c12075b9ed700d41844810c51afda10ee329576b",
"urls": ["https://gitlab.freedesktop.org/wayland/wayland-protocols/-/releases/1.41/downloads/{filename}"]
}
}

View File

@@ -13,6 +13,6 @@ for attr in ('linguist-generated', 'linguist-vendored'):
fname = line.split(':', 1)[0]
all_files.discard(fname)
all_files -= {'gen/rowcolumn-diacritics.txt'}
all_files -= {'gen/nerd-fonts-glyphs.txt', 'gen/rowcolumn-diacritics.txt'}
cp = subprocess.run(['cloc', '--list-file', '-'], input='\n'.join(all_files).encode())
raise SystemExit(cp.returncode)

View File

@@ -9,18 +9,6 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
Recent major new features
---------------------------
Access kitty with a single keypress [0.42]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. include:: quake-screenshots.rst
kitty now has a Quake like floating, translucent terminal window, so you can access
all that kitty goodness instantly with a single keypress.
See the screenshots on the side and head over to the :doc:`kitten page for details
on how to set it up </kittens/quick-access-terminal>`.
Multiple sized text [0.40]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -106,100 +94,29 @@ consumption to do the same tasks.
Detailed list of changes
-------------------------------------
0.42.2 [2025-07-16]
0.42.0 [future]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- A new :ref:`protocol extension <mouse_leave_window>` to notify terminal programs that have turned on SGR Pixel mouse reporting when the mouse leaves the window (:disc:`8808`)
- The :doc:`panel kitten </kittens/panel>` can now be used to :ref:`quake`
- clipboard kitten: Can now optionally take a password to avoid repeated
permission prompts when accessing the clipboard. Based on a
:ref:`protocol extension <clipboard_repeated_permission>`. (:iss:`8789`)
- A new :option:`launch --hold-after-ssh` to not close a launched window
that connects directly to a remote host because of
:option:`launch --cwd`:code:`=current` when the connection ends (:pull:`8807`)
- Fix :opt:`remember_window_position` not working because of a stupid typo (:iss:`8646`)
- A new :option:`kitty --grab-keyboard` that can be used to grab the keyboard so that global shortcuts are sent to kitty instead
- Remote control: Fix holding a remote control socket open causing the kitty I/O thread to go into a loop and not respond on other remote control sockets (:disc:`8670`)
- hints kitten: Preserve line breaks when the hint is over a line break (:iss:`8674`)
- Fix a segfault when using the :ac:`copy_ansi_to_clipboard` action (:iss:`8682`)
- Fix a crash when using linear easing curves for animations (:iss:`8692`)
- Graphics protocol: Add a note clarifying image update behavior on re-transmission (:iss:`8701`)
- Wayland GNOME: Fix incorrect OS Window tracking because GNOME has started
activating windows on non-current workspaces (:iss:`8716`)
- Fix a regression in 0.40.0 that broke rendering of VS15 variation selectors in some circumstances (:iss:`8731`, :iss:`8794`)
- Fix a regression in 0.40.0 that broke serialization of tab characters as ANSI text (:iss:`8741`)
- Fix a regression in 0.40.0 that broke erasing of characters in a line in the presence of wide characters (:iss:`8758`)
- Fix a regression in 0.40.0 that broke hyperlinking of wide characters (:iss:`8796`)
- Fix a regression that broke using :kbd:`esc` to exit visual select window mode (:iss:`8767`)
- kitten run-shell: Fix SIGINT blocked when execing the shell (:iss:`8754`)
0.42.1 [2025-05-17]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Fix ambiguous width and private use characters not being rendered when used with variable width text-sizing protocol escape codes
- Quick access terminal: Restore focus to previously active window when hiding the quick access terminal window on macOS (:iss:`8627`)
- Wayland: Fix an abort if the terminal program sets a window title longer than 2KB that contains CSI escape sequences and multibyte UTF-8 (:iss:`8619`)
- Quick access terminal: Allow toggling the window to full screen using the standard kitty :sc:`toggle_fullscreen` shortcut (:iss:`8626`)
- Quick access terminal: Allow configuring the monitor to display the panel on in Wayland/X11 (:iss:`8630`)
- A new setting :opt:`remember_window_position` to optionally use the position of the last closed kitty OS Window as the position of the first kitty OS Window when running a new kitty instance (:pull:`8601`)
- Panel kitten: A new ``center-sized`` value for :option:`--edge <kitty +kitten panel --edge>` to allow easily creating sized and centered panels
- Wayland: The `kitty --name` flag now sets the XDG *window tag* on compositors
that support the `xdg-toplevel-tag <https://wayland.app/protocols/xdg-toplevel-tag-v1>`__ protocol.
0.42.0 [2025-05-11]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- A new kitten: :doc:`quick-access-terminal </kittens/quick-access-terminal>` to :ref:`quake`
- The :doc:`panel kitten </kittens/panel>` works on macOS and X11 as well as Wayland (:iss:`2590`)
- The :doc:`panel kitten </kittens/panel>` now works on macOS as well as Wayland (:iss:`2590`)
- **Behavior change**: Now kitty does full grapheme segmentation following the
Unicode 16 spec when splitting text into cells (:iss:`8533`)
- **Behavior change**: The :ref:`automatic color switching functionality <auto_color_scheme>` now also controls background image settings (:iss:`8603`)
- panel kitten: Allow using :option:`kitty +kitten panel --single-instance` to create multiple panels in one process (:iss:`8549`)
- launch: Allow creating desktop panels such as those created by the :doc:`panel kitten </kittens/panel>` (:iss:`8549`)
- Remote control: Allow modifying desktop panels and showing/hiding OS Windows
using the ``kitten @ resize-os-window`` command (:iss:`8550`)
using the `kitten @ resize-os-window` command (:iss:`8550`)
- Remote control launch: Allow waiting for a program launched in a new window
to exit and get the exit code via the `kitty +launch
--wait-for-child-to-exit` command line flag (:disc:`8573`)
- Allow starting kitty with the OS window hidden via :option:`kitty --start-as=hidden <kitty --start-as>`, useful for single instance mode (:iss:`3466`)
- Allow starting kitty with the OS window hidden via :option:`kitty --start-as`\=hidden useful for single instance mode (:iss:`3466`)
- Allow configuring the mouse unhide behavior when using :opt:`mouse_hide_wait` (:pull:`8508`)
- diff kitten: Add half page and full page scroll vim-like bindings (:pull:`8514`)
- diff kitten: Allow diffing named pipes (:iss:`8597`)
- Fix a regression that caused automatic color themes to not be re-applied after config file reload (:iss:`8530`)
- Wayland: When the compositor supports the `xdg-system-bell
@@ -208,8 +125,8 @@ Detailed list of changes
- panel kitten: Allow specifying panel size in pixels in addition to cells
- Fix a regression in 0.36.0 that caused using = with single letter command
line flags to no longer work correctly (:iss:`8556`)
- Fix a regression in 0.36.0 that caused using = with single letter options to
no longer work correctly (:iss:`8556`)
- Single instance: Preserve environment variables from invoking environment in
newly created window (:disc:`8567`)
@@ -219,12 +136,6 @@ Detailed list of changes
- macOS: Fix text color in visual window select ignoring the color theme (:iss:`8579`)
- Launch action: Allow using an env var that resolves to a full command-line as the program to launch (:pull:`8613`)
- :ac:`change_font_size` allow multiplying/dividing the current font size in addition to incrementing it (:iss:`8616`)
- Box drawing: Improve appearance of rounder corners, giving them a uniform line width (:iss:`8299`)
0.41.1 [2025-04-03]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -138,29 +138,6 @@ the data, but create multiple references to it in the system clipboard. Alias
packets can be sent anytime after the initial write packet and before the end
of data packet.
.. _clipboard_repeated_permission:
Avoiding repeated permission prompts
--------------------------------------
.. versionadded:: using a password to avoid repeated confirmations was added in version 0.43.0
If a program like an editor wants to make use of the system clipboard, by
default, the user is prompted on every read request. This can become quite
fatiguing. To avoid this situation, this protocol allows sending a password
and human friendly name with ``type=write`` and ``type=read`` requests. The
terminal can then ask the user to allow all future requests using that
password. If the user agrees, future requests on the same tty will be
automatically allowed by the terminal. The editor or other program using
this facility should ideally use a password randomnly generated at startup,
such as a UUID4. However, terminals may implement permanent/stored passwords.
Users can then configure terminal programs they trust to use these password.
The password and the human name are encoded using the ``pw`` and ``name`` keys
in the metadata. The values are UTF-8 strings that are base64 encoded.
Specifying a password without a human friendly name is equivalent to not
specifying a password and the terminal must treat the request as though
it had no password.
Support for terminal multiplexers
------------------------------------

View File

@@ -69,7 +69,7 @@ selection_foreground The foreground color of selections
cursor The color of the text cursor Foreground color
cursor_text The color of text under the cursor Background color
visual_bell The color of a visual bell Automatic color selection based on current screen colors
transparent_background_color1..7 A background color that is rendered Unset
transparent_background_color1..8 A background color that is rendered Unset
with the specified opacity in cells that have
the specified background color. An opacity
value less than zero means, use the

View File

@@ -12,6 +12,7 @@ import os
import re
import subprocess
import sys
import textwrap
import time
from functools import lru_cache, partial
from typing import Any, Callable, Dict, Iterable, Iterator, List, Tuple
@@ -30,6 +31,7 @@ if kitty_src not in sys.path:
from kitty.conf.types import Definition, expand_opt_references # noqa
from kitty.constants import str_version, website_url # noqa
from kitty.fast_data_types import Shlex, TEXT_SIZE_CODE # noqa
from kittens.panel.main import default_quake_cmdline # noqa
# config {{{
# -- Project information -----------------------------------------------------
@@ -62,7 +64,7 @@ extensions = [
'sphinx.ext.extlinks',
'sphinx_copybutton',
'sphinx_inline_tabs',
'sphinxext.opengraph',
"sphinxext.opengraph",
]
# URL for OpenGraph tags
@@ -120,6 +122,9 @@ string_replacements = {
'_kitty_install_cmd': 'curl -L https://sw.kovidgoyal.net/kitty/installer.sh | sh /dev/stdin',
'_build_go_version': go_version('../go.mod'),
'_text_size_code': str(TEXT_SIZE_CODE),
'_default_quake_cmdline': textwrap.fill(
default_quake_cmdline, break_on_hyphens=False, break_long_words=False, initial_indent=' ' * 4, subsequent_indent=' ' * 8, width=77,
).replace('\n', ' \\\n'),
}
@@ -643,10 +648,33 @@ def monkeypatch_man_writer() -> None:
'''
Monkeypatch the docutils man translator to be nicer
'''
from docutils.nodes import figure
from docutils.writers.manpage import Translator
from docutils.nodes import Element
from docutils.writers.manpage import Table, Translator
from sphinx.writers.manpage import ManualPageTranslator
# Generate nicer tables https://sourceforge.net/p/docutils/bugs/475/
class PatchedTable(Table): # type: ignore
_options: list[str]
def __init__(self) -> None:
super().__init__()
self.needs_border_removal = self._options == ['center']
if self.needs_border_removal:
self._options = ['box', 'center']
def as_list(self) -> list[str]:
ans: list[str] = super().as_list()
if self.needs_border_removal:
# remove side and top borders as we use box in self._options
ans[2] = ans[2][1:]
a, b = ans[2].rpartition('|')[::2]
ans[2] = a + b
if ans[3] == '_\n':
del ans[3] # top border
del ans[-2] # bottom border
return ans
def visit_table(self: ManualPageTranslator, node: object) -> None:
setattr(self, '_active_table', PatchedTable())
setattr(ManualPageTranslator, 'visit_table', visit_table)
# Improve header generation
def header(self: ManualPageTranslator) -> str:
@@ -663,13 +691,13 @@ def monkeypatch_man_writer() -> None:
setattr(ManualPageTranslator, 'header', header)
def visit_image(self: ManualPageTranslator, node: figure) -> None:
def visit_image(self: ManualPageTranslator, node: Element) -> None:
pass
def depart_image(self: ManualPageTranslator, node: figure) -> None:
def depart_image(self: ManualPageTranslator, node: Element) -> None:
pass
def depart_figure(self: ManualPageTranslator, node: figure) -> None:
def depart_figure(self: ManualPageTranslator, node: Element) -> None:
self.body.append(' (images not supported)\n')
Translator.depart_figure(self, node)
@@ -677,8 +705,8 @@ def monkeypatch_man_writer() -> None:
setattr(ManualPageTranslator, 'depart_image', depart_image)
setattr(ManualPageTranslator, 'depart_figure', depart_figure)
orig_astext = getattr(ManualPageTranslator, 'astext')
def astext(self: ManualPageTranslator) -> Any:
orig_astext = Translator.astext
def astext(self: Translator) -> Any:
b = []
for line in self.body:
if line.startswith('.SH'):
@@ -687,9 +715,9 @@ def monkeypatch_man_writer() -> None:
parts[0] = parts[0].capitalize()
line = x + ' ' + '\n'.join(parts)
b.append(line)
setattr(self, 'body', b)
self.body = b
return orig_astext(self)
setattr(ManualPageTranslator, 'astext', astext)
setattr(Translator, 'astext', astext)
def setup_man_pages() -> None:

View File

@@ -395,7 +395,7 @@ Key Value
``c`` ``c=1`` if the terminal supports close events, otherwise the ``c``
must be omitted.
``o`` Comma separated list of occasions from the ``o`` key that the
``o`` Comma separated list of occassions from the ``o`` key that the
terminal implements. If no occasions are supported, the value
``o=always`` must be sent in the query response.
@@ -405,7 +405,7 @@ Key Value
``s`` Comma separated list of sound names from the table of standard sound names above.
Terminals will report the list of standard sound names they support.
Terminals *should* support at least ``system`` and ``silent``.
Terminals *should* support atleast ``system`` and ``silent``.
``u`` Comma separated list of urgency values that the terminal implements.
If urgency is not supported, the ``u`` key must be absent from the
@@ -450,10 +450,10 @@ Key Value Default Description
encoded UTF-8
application name
``g`` :ref:`identifier` ``unset`` Identifier for icon data. Make these globally unique,
``g`` :ref:`identifier` ``unset`` Identifier for icon data. Make these globally unqiue,
like an UUID.
``i`` :ref:`identifier` ``unset`` Identifier for the notification. Make these globally unique,
``i`` :ref:`identifier` ``unset`` Identifier for the notification. Make these globally unqiue,
like an UUID, so that terminal multiplexers can
direct responses to the correct window. Note that for backwards
compatibility reasons i=0 is special and should not be used.

View File

@@ -470,8 +470,7 @@ You need to make sure that the environment variables you define in your shell's
rc files are either also defined system wide or via the :opt:`env` directive in
:file:`kitty.conf`. Common environment variables that cause issues are those
related to localization, such as :envvar:`LANG`, ``LC_*`` and loading of
configuration files such as ``XDG_*``, :envvar:`KITTY_CONFIG_DIRECTORY` and,
most importantly, ``PATH`` to locate binaries.
configuration files such as ``XDG_*``, :envvar:`KITTY_CONFIG_DIRECTORY`.
To see the environment variables that kitty sees, you can add the following
mapping to :file:`kitty.conf`::

View File

@@ -100,7 +100,7 @@ code to demonstrate its use
buf = array.array('H', [0, 0, 0, 0])
fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, buf)
print((
'number of rows: {} number of columns: {} '
'number of rows: {} number of columns: {}'
'screen width: {} screen height: {}').format(*buf))
.. tab:: Go
@@ -154,9 +154,7 @@ You can also use the *CSI t* escape code to get the screen size. Send
``<ESC>[14t`` to ``STDOUT`` and kitty will reply on ``STDIN`` with
``<ESC>[4;<height>;<width>t`` where ``height`` and ``width`` are the window
size in pixels. This escape code is supported in many terminals, not just
kitty. A more precise version of this escape code, which is however supported
in less terminals is ``<ESC>[16t`` which causes the terminal to reply with the
pixel dimensions of a single cell.
kitty.
A minimal example
------------------
@@ -339,7 +337,7 @@ similar to reporting any other kind of I/O error. Since the file paths come
from potentially untrusted sources, terminal emulators **must** refuse to read
any device/socket/etc. special files. Only regular files are allowed.
Additionally, terminal emulators may refuse to read files in *sensitive*
parts of the filesystem, such as :file:`/proc`, :file:`/sys`, :file:`/dev`, etc.
parts of the filesystem, such as :file:`/proc`, :file:`/sys`, :file:`/dev/`, etc.
Local client
^^^^^^^^^^^^^^
@@ -469,11 +467,10 @@ 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 (i.e. has id=0), it will be ignored, i.e. the placement will not
get an id. In particular this means there can exist multiple images with
``image id=0, placement id=0``. Not specifying a placement id or using ``p=0``
for multiple put commands (``a=p``) with the same non-zero image id results in
multiple placements the image.
not have an id (i.e. has id=0), it will be ignored. In particular this means
there can exist multiple images with ``image id=0, placement id=0``. Not
specifying a placement id or using ``p=0`` for multiple put commands (``a=p``)
with the same non-zero image id results in multiple placements the image.
An example response::
@@ -484,14 +481,6 @@ second one will replace the first. This can be used to resize or move
placements around the screen, without flicker.
.. note::
When re-transmitting image data for a specific id, the existing image and
all its placements must be deleted. The new data replaces the old image data
but is not actually displayed until a placement for it is created. This is
to avoid divergent behavior in the case when unrelated programs happen to re-use
image ids in the same session.
.. versionadded:: 0.19.3
Support for specifying placement ids (see :doc:`kittens/query_terminal` to query kitty version)
@@ -897,12 +886,12 @@ on.
Finally, while transferring frame data, the frame *gap* can also be specified
using the ``z`` key. The gap is the number of milliseconds to wait before
displaying the next frame when the animation is running. A value of ``z=0`` is
ignored (acts as though ``z`` was unspecified), ``z=positive number`` sets the
gap to the specified number of milliseconds and ``z=negative number`` creates a
*gapless* frame. Gapless frames are not displayed to the user since they are
instantly skipped over, however they can be useful as the base data for
subsequent frames. For example, for an animation where the background remains
the same and a small object or two move.
ignored, ``z=positive number`` sets the gap to the specified number of
milliseconds and ``z=negative number`` creates a *gapless* frame. Gapless
frames are not displayed to the user since they are instantly skipped over,
however they can be useful as the base data for subsequent frames. For example,
for an animation where the background remains the same and a small object or two
move.
Controlling animations
~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -932,9 +921,9 @@ animation. ``s=2`` runs the animation, but in *loading* mode, in this mode when
reaching the last frame, instead of looping, the terminal will wait for the
arrival of more frames. ``s=3`` runs the animation normally, after the last
frame, the terminal loops back to the first frame. The number of loops can be
controlled by the ``v`` key. ``v=0`` is ignored (acts as though ``v`` was not
specified), ``v=1`` is loop infinitely, and any other positive number is loop
``number - 1`` times. Note that stopping the animation resets the loop counter.
controlled by the ``v`` key. ``v=0`` is ignored, ``v=1`` is loop infinitely,
and any other positive number is loop ``number - 1`` times. Note that stopping
the animation resets the loop counter.
Finally, the *gap* for frames can be set using the ``z`` key. This can be
specified either when the frame is created as part of the transmit escape code
@@ -1014,8 +1003,8 @@ take, and the default value they take when missing. All integers are 32-bit.
Key Value Default Description
======= ==================== ========= =================
``a`` Single character. ``t`` The overall action this graphics command is performing.
``(a, c, d, f, ``t`` - transmit data, ``T`` - transmit data and display image,
p, q, t, T)`` ``q`` - query terminal, ``p`` - put (display) previous transmitted image,
``(a, c, d, f, `` ``t`` - transmit data, ``T`` - transmit data and display image,
``p, q, t, T)`` ``q`` - query terminal, ``p`` - put (display) previous transmitted image,
``d`` - delete image, ``f`` - transmit data for animation frames,
``a`` - control animation, ``c`` - compose animation frames

View File

@@ -139,10 +139,6 @@ images.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
A video player that can play videos in the terminal.
.. code-block:: sh
mpv --profile=sw-fast --vo=kitty --vo-kitty-use-shm=yes --really-quiet video.mkv
.. _tool_timg:
`timg <https://github.com/hzeller/timg>`_
@@ -336,14 +332,6 @@ A kitten-panel based desktop panel for your desktop
Miscellaneous
------------------
.. tool_doom:
DOOM
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Play the classic shooter DOOM in `kitty <https://github.com/cryptocode/terminal-doom>`__ or even inside `neovim inside kitty
<https://github.com/seandewar/actually-doom.nvim>`__.
.. tool_gattino:
`gattino <https://github.com/salvozappa/gattino>`__

View File

@@ -149,14 +149,14 @@ user presses, for example, :kbd:`ctrl+shift+a` the escape code would be ``CSI
97;modifiers u``. It *must not* be ``CSI 65; modifiers u``.
If *alternate key reporting* is requested by the program running in the
terminal, the terminal can send two additional Unicode codepoints, the *shifted
key* and *base layout key*, separated by colons. The shifted key is simply the
upper-case version of ``unicode-codepoint``, or more technically, the shifted
version, in the currently active keyboard layout. So `a` becomes `A` and so on,
based on the current keyboard layout. This is needed to be able to match
against a shortcut such as :kbd:`ctrl+plus` which depending on the type of
keyboard could be either :kbd:`ctrl+shift+equal` or :kbd:`ctrl+plus`. Note that
the shifted key must be present only if shift is also present in the modifiers.
terminal, the terminal can send two additional Unicode codepoints, the
*shifted key* and *base layout key*, separated by colons.
The shifted key is simply the upper-case version of ``unicode-codepoint``, or
more technically, the shifted version. So `a` becomes `A` and so on, based on
the current keyboard layout. This is needed to be able to match against a
shortcut such as :kbd:`ctrl+plus` which depending on the type of keyboard could
be either :kbd:`ctrl+shift+equal` or :kbd:`ctrl+plus`. Note that the shifted
key must be present only if shift is also present in the modifiers.
The *base layout key* is the key corresponding to the physical key in the
standard PC-101 key layout. So for example, if the user is using a Cyrillic
@@ -247,11 +247,7 @@ enhancement <progressive_enhancement>` mechanism described below. Some examples:
If multiple code points are present, they must be separated by colons. If no
known key is associated with the text the key number ``0`` must be used. The
associated text must not contain control codes (control codes are code points
below U+0020 and codepoints in the C0 and C1 blocks). In the above example, the
:kbd:`option` modifier is consumed by macOS itself to produce the text å
and therefore not reported in the keyboard protocol. On some platforms
composition keys might produce no key information at all, in which case the key
number ``0`` must be used.
below U+0020 and codepoints in the C0 and C1 blocks).
Non-Unicode keys
@@ -358,7 +354,7 @@ are the :kbd:`Enter`, :kbd:`Tab` and :kbd:`Backspace` keys which still generate
bytes as in legacy mode this is to allow the user to type and execute commands
in the shell such as ``reset`` after a program that sets this mode crashes
without clearing it. Note that the Lock modifiers are not reported for text
producing keys, to keep them usable in legacy programs. To get lock modifiers
producing keys, to keep them useable in legacy programs. To get lock modifiers
for all keys use the :ref:`report_all_keys` enhancement.
.. _report_events:

View File

@@ -1,42 +0,0 @@
Selecting files, fast
========================
.. only:: man
Overview
--------------
.. versionadded:: 0.43.0
The choose-files kitten is designed to allow you to select files, very fast,
with just a few key strokes. It operates like `fzf
<https://github.com/junegunn/fzf/>`__ and similar fuzzy finders, except that
it is specialised for finding files. As such it supports features such as
filtering by file type, file type icons, content previews (coming soon) and
so on, out of the box. It can be used as a drop in (but much more efficient and
keyboard friendly) replacement for the :guilabel:`File open and save`
dialog boxes common to GUI programs. On Linux, with the help of the
:doc:`desktop-ui </kittens/desktop-ui>` kitten, you can even convince
most GUI programs on your computer to use this kitten instead of regular file
dialogs.
Simply run it as::
kitten choose-files
to select a single file from the tree rooted at the current working directory.
Configuration
------------------------
You can configure various aspects of the kitten's operation by creating a
:file:`choose-files.conf` in your :ref:`kitty config folder <confloc>`.
See below for the supported configuration directives.
.. include:: /generated/conf-kitten-choose_files.rst
.. include:: /generated/cli-kitten-choose_files.rst

View File

@@ -118,11 +118,11 @@ that is, they are used to set the variable value for some font characteristic.
<https://harfbuzz.github.io/harfbuzz-hb-common.html#hb-feature-from-string>`__
``system``
This can be used to pass an arbitrary string, usually a family or full name
This can be used to pass an arbitrary string, usuall a family or full name
to the OS font selection APIs. Should not be used in conjunction with any
other keys. Is the same as specifying just the font name without any keys.
In addition to these keys, any four letter key is treated as the name of a
variable characteristic of the font. Its value is used to set the value for
variable characteristic of the font. It's value is used to set the value for
the name.

View File

@@ -1,21 +0,0 @@
Using terminal programs to provide Linux desktop components
===============================================================
.. only:: man
Overview
--------------
.. versionadded:: 0.43.0
Power users of terminals on Linux also often like to use bare bones window
managers instead of full fledged desktop environments. This kitten helps
provide parts of the desktop environment that are missing from such setups,
and does so using keyboard friendly, terminal first UI components. Some of its
features are:
* Replace the typical File Open/Save dialogs used in GUI programs with the
fast and keyboard centric :doc:`choose-files </kittens/choose-files>` kitten
running in a semi-transparent kitty overlay.
* Allow simple command line based management of the desktop light/dark modes.

View File

@@ -8,43 +8,52 @@ Draw a GPU accelerated dock panel on your desktop
Overview
--------------
.. include:: ../quake-screenshots.rst
Draw the desktop wallpaper or docks and panels using arbitrary
terminal programs, For example, have `btop
<https://github.com/aristocratos/btop>`__ or `cava
<https://github.com/karlstav/cava/>`__ be your desktop wallpaper.
You can use this kitten to draw a GPU accelerated panel on the edge of your
screen or as the desktop wallpaper, that shows the output from an arbitrary
terminal program.
It is useful for showing status information or notifications on your desktop
using terminal programs instead of GUI toolkits.
using terminal programs instead of GUI toolkits. It can also be used for a
:ref:`Quake like quick access terminal <quake>`.
.. figure:: ../screenshots/panel.png
:alt: Screenshot, showing a sample panel
:align: center
:width: 100%
Screenshot, showing a sample panel
The screenshot to the side shows some uses of the panel kitten to draw various
desktop components such as the background, a quick access floating terminal and
a dock panel showing system information (Linux only).
The screenshot above shows a sample panel that displays the current desktop and
window title as well as miscellaneous system information such as network
activity, CPU load, date/time, etc.
.. versionadded:: 0.42.0
Support for macOS and support for Wayland was added in 0.34.0
Support for macOS, see :ref:`compatibility matrix <panel_compat>` for details.
and X11 (background and overlay).
.. note::
.. versionadded:: 0.34.0
Support for Wayland. See :ref:`below <panel_compat>` for which
Wayland compositors work.
This kitten currently only works on macOS and Wayland compositors
that support the `wlr layer shell protocol
<https://wayland.app/protocols/wlr-layer-shell-unstable-v1#compositor-support>`__
(which is almost all of them except GNOME). On macOS the panels do not
prevent other windows from floating over them because of limitations in
Cocoa. On X11, only the ``top`` and ``bottom`` panels are widely supported,
the other types depend on the window manager used.
Using this kitten is simple, for example::
kitten panel sh -c 'printf "\n\n\nHello, world."; sleep 5s'
kitty +kitten panel sh -c 'printf "\n\n\nHello, world."; sleep 5s'
This will show ``Hello, world.`` at the top edge of your screen for five
seconds. Here, the terminal program we are running is :program:`sh` with a script
to print out ``Hello, world!``. You can make the terminal program as complex as
you like, as demonstrated in the screenshots.
you like, as demonstrated in the screenshot above.
If you are on Wayland or macOS, you can, for instance, run::
If you are on Wayland or macOS, you can, for instance run::
kitten panel --edge=background htop
kitty +kitten panel --edge=background htop
to display ``htop`` as your desktop background. Remember this works in everything
but GNOME and also, in sway, you have to disable the background wallpaper as
@@ -53,13 +62,40 @@ sway renders that over the panel kitten surface.
There are projects that make use of this facility to implement generalised
panels and desktop components:
.. _panel_projects:
* `kitty panel <https://github.com/5hubham5ingh/kitty-panel>`__
* `pawbar <https://github.com/codelif/pawbar>`__
.. _remote_control_panel:
.. _quake:
Make a Quake like quick access terminal
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. versionadded:: 0.42.0
Support for quake mode, works only on macOS and Wayland, except for GNOME.
This kitten can be used to make a quick access terminal, that appears and
disappears at a key press. To do so use the following command:
.. code-block:: sh
_default_quake_cmdline
Run this command in a terminal, and a quick access kitty panel will show up at
the top of your screen. Run it again, and the panel will be hidden.
Simply bind this command to some key press in your window manager or desktop
environment settings and then you have a quick access terminal at a single key press.
You can use the various panel options to configure the size, appearance and
position of the quick access panel. In particular, the :option:`kitty +kitten panel --config` and
:option:`kitty +kitten panel --override` options can be used to theme the terminal appropriately,
making it look different from regular kitty terminal instances.
.. note::
If you want to start the quake terminal hidden, use
:option:`kitty +kitten panel --start-as-hidden`, useful if you are starting it in the background
during computer startup.
Controlling panels via remote control
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -67,7 +103,7 @@ Controlling panels via remote control
You can control panels via the kitty :doc:`remote control </remote-control>` facility. Create a panel
with remote control enabled::
kitten panel -o allow_remote_control=socket-only --lines=2 \
kitty +kitten panel -o allow_remote_control=socket-only --lines=2 \
--listen-on=unix:/tmp/panel kitten run-shell
@@ -89,132 +125,3 @@ To create a new panel running the program top, in the same instance
.. include:: ../generated/cli-kitten-panel.rst
.. _quake_ss:
How the screenshots were generated
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The system statistics in the background were created using::
kitten panel --edge=background -o background_opacity=0.2 -o background=black btop
This creates a kitty background window and inside it runs the `btop
<https://github.com/aristocratos/btop>`__ program to display the statistics.
The floating quick access window was created by running::
kitten quick-access-terminal kitten run-shell \
zsh -c 'printf "\e]66;s=4;Quick access kitty in Hyprland\a\n\n\n\nAlso uses kitty to draw desktop background\n"'
This starts the quick access window and inside it runs ``kitten run-shell``, which
in turn first runs ``zsh`` to print out the message and then starts the users login
shell.
The Linux dock panel was::
wm bar
This is a custom program I wrote for my personal use. It uses kitty's kitten
infrastructure to implement the bar in a `few hundred lines of code
<https://github.com/kovidgoyal/wm/blob/master/bar/main.go>`__.
This was designed for my personal use only, but, there are :ref:`public projects implementing
general purpose panels using kitty <panel_projects>`.
.. _panel_compat:
Compatibility with various platforms
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. only:: man
See the HTML documentation for the compatibility matrix.
.. only:: not man
Generated with the help of the :file:`panels.py` test script.
.. tab:: Wayland
Below is a list of the status of various Wayland compositors. The panel kitten
relies of the `wlr layer shell protocol
<https://wayland.app/protocols/wlr-layer-shell-unstable-v1#compositor-support>`__,
which is technically supported by almost all Wayland compositors, but the
implementation in some of them is quite buggy.
🟢 **Hyprland**
Fully working, no known issues
🟢 **labwc**
Fully working, no known issues
🟠 **KDE** (kwin)
Mostly working, except that clicks outside background panels cause kwin to :iss:`erroneously hide the panel <8715>`. KDE uses an `undocumented mapping <https://invent.kde.org/plasma/kwin/-/blob/3dc5cee6b34792486b343098e55e7f2b90dfcd00/src/layershellv1window.cpp#L24>`__ under Wayland to set the window type from the :code:`kitten panel --app-id` flag. You might want to use :code:`--app-id=dock` so that KDE treats the window as a dock panel, and disables window appearing/disappearing animations for it.
🟠 **Sway**
Renders its configured background over the background window instead of
under it. This is because it uses the wlr protocol for backgrounds itself.
🟠 **river**
Not all functionality has been tested, but the quick access terminal
appears as it should and the keyboard focus is properly restored too.
🟠 **niri**
Hiding a dock panel (unmapping the window) does not release the space used
by the dock.
🔴 **GNOME** (mutter)
Does not implement the wlr protocol at all, nothing works.
.. tab:: macOS
Mostly everything works, with the notable exception that dock panels do not
prevent other windows from covering them. This is because Apple does not
provide and way to do this in their APIs.
.. tab:: X11
Support is highly dependent on the quirks of individual window
managers. See the matrix below:
.. list-table:: Compatibility matrix
:header-rows: 1
:stub-columns: 1
* - WM
- Desktop
- Dock
- Quick
- Notes
* - KDE
- 🟠
- 🟢
- 🟢
- transparency does not work for :option:`--edge=background <--edge>`
* - GNOME
- 🟢
- 🟢
- 🟢
-
* - XFCE
- 🟢
- 🟢
- 🟢
-
* - i3
- 🔴
- 🟠
- 🔴
- only top and bottom dock panels, without transparency
* - xmonad
- 🔴
- 🔴
- 🔴
- doesn't support the needed NET_WM protocols

View File

@@ -1,84 +0,0 @@
.. _quake:
Make a Quake like quick access terminal
====================================================================================================
.. highlight:: sh
.. only:: man
Overview
--------------
.. include:: ../quake-screenshots.rst
.. versionadded:: 0.42.0
See :ref:`here for what platforms it works on <panel_compat>`.
This kitten can be used to make a quick access terminal, that appears and
disappears at a key press. To do so use the following command:
.. code-block:: sh
kitten quick-access-terminal
Run this command in a terminal, and a quick access kitty window will show up at
the top of your screen. Run it again, and the window will be hidden.
To make the terminal appear and disappear at a key press:
.. |macOs| replace:: :guilabel:`System Preferences->Keyboard->Keyboard Shortcuts->Services->General`
.. only:: not man
.. tab:: Linux
Simply bind the above command to some key press in your window manager or desktop
environment settings and then you have a quick access terminal at a single key press.
.. tab:: macOS
In kitty, run the above command to show the quick access window, then close
it by running the command again or pressing :kbd:`ctrl+d`. Now go to |macOS| and set a shortcut for
the :guilabel:`Quick access to kitty` entry.
.. only:: man
In Linux, simply assign the above command to a global shortcut in your
window manager. In macOS, go to |macOS| and set a shortcut
for the :guilabel:`Quick access to kitty` entry.
Configuration
------------------------
You can configure the appearance and behavior of the quick access window
by creating a :file:`quick-access-terminal.conf` file in your
:ref:`kitty config folder <confloc>`. In particular, you can use the
:opt:`kitty_conf <kitten-quick_access_terminal.kitty_conf>` option to change
various kitty settings, just for the quick access window.
.. note::
This kitten uses the :doc:`panel kitten </kittens/panel>` under the
hood. You can use the :ref:`techniques described there <remote_control_panel>`
for remote controlling the quick access window, remember to add
``kitty_override allow_remote_control=socket-only`` and ``kitty_override
listen_on=unix:/tmp/whatever`` to
:file:`quick-access-terminal.conf`.
See below for the supported configuration directives:
.. include:: /generated/conf-kitten-quick_access_terminal.rst
.. include:: /generated/cli-kitten-quick_access_terminal.rst
Sample quick-access-terminal.conf
---------------------------------------
You can download a sample :file:`quick-access-terminal.conf` file with all default settings and
comments describing each setting by clicking: :download:`sample quick-access-terminal.conf
</generated/conf/quick_access_terminal.conf>`.

View File

@@ -70,8 +70,7 @@ This works by creating three files: :file:`dark-theme.auto.conf`,
:file:`light-theme.auto.conf` and :file:`no-preference-theme.auto.conf` in the
kitty config directory. When these files exist, kitty queries the OS for its color scheme
and uses the appropriate file. Note that the colors in these files override all other
colors, and also all background image settings,
even those specified using the :option:`kitty --override` command line flag.
colors, even those specified using the :option:`kitty --override` command line flag.
kitty will also automatically change colors when the OS color scheme changes,
for example, during night/day transitions.

View File

@@ -13,8 +13,6 @@ Extend with kittens
kittens/themes
kittens/choose-fonts
kittens/hints
kittens/quick-access-terminal
kittens/panel
kittens/remote_file
kittens/hyperlinked_grep
kittens/transfer
@@ -53,16 +51,6 @@ Some prominent kittens:
filenames, words, lines, etc. from the terminal screen.
:doc:`Quick access terminal <kittens/quick-access-terminal>`
Get access to a quick access floating, semi-transparent kitty window
with a single keypress.
:doc:`Panel <kittens/panel>`
Draw the desktop wallpaper or docks and panels using arbitrary
terminal programs.
:doc:`Remote file <kittens/remote_file>`
Edit, open, or download remote files over SSH easily, by simply clicking on
the filename.

View File

@@ -310,7 +310,7 @@ below::
# Beep on unknown keys
map --new-mode XXX --on-unknown beep ...
# Ignore unknown keys silently
# Ingore unknown keys silently
map --new-mode XXX --on-unknown ignore ...
# Beep and exit the keyboard mode on unknown key
map --new-mode XXX --on-unknown end ...

View File

@@ -29,20 +29,6 @@ there is only one number to reset these attributes, SGR 22, which resets both.
There is no way to reset one and not the other. kitty uses 221 and 222 to reset
bold and faint independently.
.. _mouse_leave_window:
Reporting when the mouse leaves the window
----------------------------------------------
kitty extends the SGR Pixel mouse reporting protocol created by xterm to
also report when the mouse leaves the window. This is event is delivered
encoded as a normal SGR pixel event except that the eight bit is set on the
first number. Additionally, bit 5 is set to indicate this is a motion related event.
The remaining bits 1-7 (except 5) are used to encode button and modifier information.
When bit 8 is set it means the event is a mouse has left the window event,
and all other bits should be ignored. The pixel position values must also
be ignored as they may not be accurate.
kitty specific private escape codes
---------------------------------------

View File

@@ -48,7 +48,7 @@ action per entry if you like, for example:
action change_font_size current -2
In the launch specification you can expand environment variables, as shown in
In the action specification you can expand environment variables, as shown in
the examples above. In addition to regular environment variables, there are
some special variables, documented below:
@@ -126,12 +126,11 @@ lines. The various available criteria are:
.. _launch_actions:
Scripting the opening of files with kitty
Scripting the opening of files with kitty on macOS
-------------------------------------------------------
On macOS you can use :guilabel:`Open With` in Finder or drag and drop files and
URLs onto the kitty dock icon to open them with kitty. Similarly on Linux, you
can associate certain files types to open in kitty. The default actions are:
URLs onto the kitty dock icon to open them with kitty. The default actions are:
* Open text files in your editor and images using the icat kitten.
* Run shell scripts in a shell

View File

@@ -101,7 +101,7 @@ ASCII only text.
foot, iterm2 and Terminal.app are left out as they do not run under X11.
Alacritty+tmux is included just to show the effect of putting a terminal
multiplexer into the mix (halving throughput) and because alacritty isn't
multiplexer into the mix (halving throughput) and because alacritty isnt
remotely comparable to any of the other terminals feature wise without tmux.
.. note::

View File

@@ -1,29 +0,0 @@
.. only:: not man
.. sidebar::
**Screenshots**
.. figure:: /screenshots/quake-macos.webp
:alt: Screenshot, showing the kitty floating quick access terminal above the background which is the program btop, running inside kitty, on macOS
:align: center
:width: 100%
macOS
.. figure:: /screenshots/quake-hypr.webp
:alt: Screenshot, showing the kitty floating quick access terminal above the background which is the program btop, running inside kitty, on Hyprland in Linux
:align: center
:width: 100%
Linux
.. figure:: /screenshots/panel.png
:alt: Screenshot, showing a sample panel
:align: center
:width: 100%
A sample panel on Linux
How the screenshots :ref:`were generated <quake_ss>`.

View File

@@ -186,7 +186,7 @@ Now, using this password, you can, in scripts run the command::
Any script with access to the password can now change colors in kitty using
remote control, but only that and nothing else. You can even supply the
password via the :envvar:`KITTY_RC_PASSWORD` environment variable, or the
file :file:`~/.config/kitty/rc-pass` to avoid having to type it repeatedly.
file :file:`~/.config/kitty/rc-password` to avoid having to type it repeatedly.
See :option:`kitten @ --password-file` and :option:`kitten @ --password-env`.
The :opt:`remote_control_password` can be specified multiple times to create

Binary file not shown.

Before

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

View File

@@ -255,17 +255,17 @@ Detecting if the terminal supports this protocol
-----------------------------------------------------
To detect support for this protocol use the `CPR (Cursor Position Report)
<https://vt100.net/docs/vt510-rm/CPR.html>`__ escape code. Send a ``CR``
(carriage return) followed by ``CPR`` followed by ``\e]_text_size_code;w=2; \a``
which will draw a space character in two cells, followed by another ``CPR``.
Then send ``\e]_text_size_code;s=2; \a`` which will draw a space in a ``2 by 2``
block of cells, followed by another ``CPR``.
<https://vt100.net/docs/vt510-rm/CPR.html>`__ escape code. Send a ``CPR``
followed by ``\e]_text_size_code;w=2; \a`` which will draw a space character in
two cells, followed by another ``CPR``. Then send ``\e]_text_size_code;s=2; \a``
which will draw a space in a ``2 by 2`` block of cells, followed by another
``CPR``.
Then wait for the three responses from the terminal to the three CPR queries.
If the cursor position in the three responses is the same, the terminal does
not support this protocol at all, if the second response has the cursor
moved by two cells, then the width part is supported and if the third response has the
cursor moved by another two cells, then the scale part is supported.
not support this protocol at all, if the second response has a different cursor
position then the width part is supported and if the third response has yet
another position, the scale part is supported.
Interaction with other terminal controls

View File

@@ -201,7 +201,6 @@ def make_bitfields() -> None:
)
mb('tools/vt', 'CellColor', 'is_idx 1', 'red 8', 'green 8', 'blue 8')
mb('tools/vt', 'LineAttrs', 'prompt_kind 2',)
mb('kittens/choose_files', 'CombinedScore', 'score 16', 'length 16', 'index 32')
# }}}
# Completions {{{
@@ -260,9 +259,9 @@ def completion_for_launch_wrappers(*names: str) -> None:
def generate_completions_for_kitty() -> None:
print('package completion\n')
print('import "github.com/kovidgoyal/kitty/tools/cli"')
print('import "github.com/kovidgoyal/kitty/tools/cmd/tool"')
print('import "github.com/kovidgoyal/kitty/tools/cmd/at"')
print('import "kitty/tools/cli"')
print('import "kitty/tools/cmd/tool"')
print('import "kitty/tools/cmd/at"')
print('func kitty(root *cli.Command) {')
@@ -451,6 +450,15 @@ def go_code_for_remote_command(name: str, cmd: RemoteCommand, template: str) ->
# kittens {{{
@lru_cache
def wrapped_kittens() -> tuple[str, ...]:
with open('shell-integration/ssh/kitty') as f:
for line in f:
if line.startswith(' wrapped_kittens="'):
val = line.strip().partition('"')[2][:-1]
return tuple(sorted(filter(None, val.split())))
raise Exception('Failed to read wrapped kittens from kitty wrapper script')
def generate_conf_parser(kitten: str, defn: Definition) -> None:
with replace_if_needed(f'kittens/{kitten}/conf_generated.go'):
@@ -459,7 +467,7 @@ def generate_conf_parser(kitten: str, defn: Definition) -> None:
def generate_extra_cli_parser(name: str, spec: str) -> None:
print('import "github.com/kovidgoyal/kitty/tools/cli"')
print('import "kitty/tools/cli"')
go_opts = tuple(go_options_for_seq(parse_option_spec(spec)[0]))
print(f'type {name}_options struct ''{')
for opt in go_opts:
@@ -509,7 +517,7 @@ def kitten_clis() -> None:
has_underscore = '_' in kitten
print(f'package {kitten}')
print('import "fmt"')
print('import "github.com/kovidgoyal/kitty/tools/cli"')
print('import "kitty/tools/cli"')
print('var _ = fmt.Sprintf')
print('func create_cmd(root *cli.Command, run_func func(*cli.Command, *Options, []string)(int, error)) {')
print('ans := root.AddSubCommand(&cli.Command{')
@@ -531,7 +539,7 @@ def kitten_clis() -> None:
for opt in gopts:
print(opt.as_option('ans'))
od.append(opt.struct_declaration())
ser.append('\n'.join(opt.as_string_for_commandline()))
ser.append(opt.as_string_for_commandline())
if ac is not None:
print(''.join(ac.as_go_code('ans.ArgCompleter', ' = ')))
if not kcd:
@@ -545,11 +553,8 @@ def kitten_clis() -> None:
print('\n'.join(od))
print('}')
print('func (opts Options) AsCommandLine() (ans []string) {')
if ser:
print('\t sval := ""')
print('\t _ = sval')
for x in ser:
print('\t' + x)
for x in ser:
print('\t' + x)
print('return')
print('}')
@@ -615,7 +620,6 @@ def generate_constants() -> str:
from kitty.config import option_names_for_completion
from kitty.fast_data_types import FILE_TRANSFER_CODE
from kitty.options.utils import allowed_shell_integration_values, url_style_map
from kitty.simple_cli_definitions import CONFIG_HELP
del sys.modules['kittens.hints.main']
del sys.modules['kittens.query_terminal.main']
ref_map = load_ref_map()
@@ -648,7 +652,6 @@ const HintsDefaultRegex = `{DEFAULT_REGEX}`
const DefaultTermName = `{Options.term}`
const DefaultUrlStyle = `{url_style}`
const DefaultUrlColor = `{Options.url_color.as_sharp}`
const ConfigHelp = "{serialize_as_go_string(CONFIG_HELP)}"
var Version VersionType = VersionType{{Major: {kc.version.major}, Minor: {kc.version.minor}, Patch: {kc.version.patch},}}
var DefaultPager []string = []string{{ {dp} }}
var FunctionalKeyNameAliases = map[string]string{serialize_go_dict(functional_key_name_aliases)}
@@ -726,7 +729,7 @@ def update_at_commands() -> None:
odef = '\n'.join(opt_def)
code = f'''
package at
import "github.com/kovidgoyal/kitty/tools/cli"
import "kitty/tools/cli"
type rc_global_options struct {{
{sdef}
}}
@@ -754,7 +757,7 @@ def update_completion() -> None:
with replace_if_needed('tools/cmd/edit_in_kitty/launch_generated.go'):
print('package edit_in_kitty')
print('import "github.com/kovidgoyal/kitty/tools/cli"')
print('import "kitty/tools/cli"')
print('func AddCloneSafeOpts(cmd *cli.Command) {')
completion_for_launch_wrappers('cmd')
print(''.join(CompletionSpec.from_string('type:file mime:text/* group:"Text files"').as_go_code('cmd.ArgCompleter', ' = ')))

11101
gen/nerd-fonts-glyphs.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,18 @@ from functools import lru_cache, partial
from html.entities import html5
from io import StringIO
from math import ceil, log
from typing import Callable, DefaultDict, Iterator, Literal, NamedTuple, Optional, Protocol, Sequence, TypedDict, TypeVar, Union
from typing import (
Callable,
DefaultDict,
Iterator,
Literal,
NamedTuple,
Optional,
Protocol,
Sequence,
TypedDict,
Union,
)
from urllib.request import urlopen
if __name__ == '__main__' and not __package__:
@@ -32,7 +43,8 @@ if len(non_characters) != 66:
emoji_skin_tone_modifiers = frozenset(range(0x1f3fb, 0x1F3FF + 1))
def fetch_url(url: str) -> str:
def get_data(fname: str, folder: str = 'UCD') -> Iterable[str]:
url = f'https://www.unicode.org/Public/{folder}/latest/{fname}'
bn = os.path.basename(url)
local = os.path.join('/tmp', bn)
if os.path.exists(local):
@@ -42,12 +54,7 @@ def fetch_url(url: str) -> str:
data = urlopen(url).read()
with open(local, 'wb') as f:
f.write(data)
return data.decode()
def get_data(fname: str, folder: str = 'UCD') -> Iterable[str]:
url = f'https://www.unicode.org/Public/{folder}/latest/{fname}'
for line in fetch_url(url).splitlines():
for line in data.decode('utf-8').splitlines():
line = line.strip()
if line and not line.startswith('#'):
yield line
@@ -144,14 +151,13 @@ def parse_ucd() -> None:
# the future.
marks.add(codepoint)
gndata = fetch_url('https://raw.githubusercontent.com/ryanoasis/nerd-fonts/refs/heads/master/glyphnames.json')
for name, val in json.loads(gndata).items():
if name != 'METADATA':
codepoint = int(val['code'], 16)
category, sep, name = name.rpartition('-')
name = name or category
name = name.replace('_', ' ')
print(11111111, name)
with open('gen/nerd-fonts-glyphs.txt') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
code, category, name = line.split(' ', 2)
codepoint = int(code, 16)
if name and codepoint not in name_map:
name_map[codepoint] = name.upper()
for word in name.lower().split():
@@ -456,9 +462,7 @@ def mask_for(bits: int) -> int:
return ~((~0) << bits)
HashableType = TypeVar('HashableType', bound=Hashable)
def splitbins(t: tuple[HashableType, ...], property_size: int, use_fixed_shift: int = 0) -> tuple[list[int], list[int], list[HashableType], int]:
def splitbins[T: Hashable](t: tuple[T, ...], property_size: int, use_fixed_shift: int = 0) -> tuple[list[int], list[int], list[T], int]:
if use_fixed_shift:
candidates = range(use_fixed_shift, use_fixed_shift + 1)
else:
@@ -469,8 +473,8 @@ def splitbins(t: tuple[HashableType, ...], property_size: int, use_fixed_shift:
n >>= 1
maxshift += 1
candidates = range(maxshift + 1)
t3: list[HashableType] = []
tmap: dict[HashableType, int] = {}
t3: list[T] = []
tmap: dict[T, int] = {}
seen = set()
for x in t:
if x not in seen:

View File

@@ -828,7 +828,7 @@ int _glfwPlatformInit(bool *supports_window_occlusion)
{
debug_key("---------------- key down -------------------\n");
debug_key("%s\n", [[event description] UTF8String]);
if (!_glfw.ignoreOSKeyboardProcessing && !_glfw.keyboard_grabbed) {
if (!_glfw.ignoreOSKeyboardProcessing) {
// first check if there is a global menu bar shortcut
if ([[NSApp mainMenu] performKeyEquivalent:event]) {
debug_key("keyDown triggered global menu bar action ignoring\n");
@@ -1144,4 +1144,3 @@ void _glfwPlatformUpdateTimer(unsigned long long timer_id, monotonic_t interval,
}
void _glfwPlatformInputColorScheme(GLFWColorScheme appearance UNUSED) { }
bool _glfwPlatformGrabKeyboard(bool grab UNUSED) { return true; /* directly uses _glfw.keyboard_grabbed */ }

View File

@@ -125,7 +125,6 @@ typedef struct _GLFWwindowNS
id delegate;
id view;
id layer;
pid_t previous_front_most_application;
bool maximized;
bool retina;

View File

@@ -1621,12 +1621,6 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) {
@implementation GLFWWindow
static void
handle_screen_size_change(_GLFWwindow *window, NSNotification *notification UNUSED) {
if (!window || !window->ns.layer_shell.is_active) return;
_glfwPlatformSetLayerShellConfig(window, NULL);
}
- (instancetype)initWithGlfwWindow:(NSRect)contentRect
styleMask:(NSWindowStyleMask)style
backing:(NSBackingStoreType)backingStoreType
@@ -1636,13 +1630,6 @@ handle_screen_size_change(_GLFWwindow *window, NSNotification *notification UNUS
if (self != nil) {
glfw_window = initWindow;
self.tabbingMode = NSWindowTabbingModeDisallowed;
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserverForName:NSApplicationDidChangeScreenParametersNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification * _Nonnull notification) {
handle_screen_size_change(glfw_window, notification);
}];
}
return self;
}
@@ -1650,7 +1637,6 @@ handle_screen_size_change(_GLFWwindow *window, NSNotification *notification UNUS
- (void) removeGLFWWindow
{
glfw_window = NULL;
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (BOOL)validateMenuItem:(NSMenuItem *)item {
@@ -1940,22 +1926,6 @@ screen_for_window_center(_GLFWwindow *window) {
return NSScreen.mainScreen;
}
const GLFWLayerShellConfig*
_glfwPlatformGetLayerShellConfig(_GLFWwindow *window) {
return &window->ns.layer_shell.config;
}
static NSScreen*
screen_for_name(const char *name) {
int count = 0;
GLFWmonitor **monitors = glfwGetMonitors(&count);
for (int i = 0; i < count; i++) {
const char *q = glfwGetMonitorName(monitors[i]);
if (q && strcmp(q, name) == 0) return ((_GLFWmonitor*)monitors[i])->ns.screen;
}
return NULL;
}
bool
_glfwPlatformSetLayerShellConfig(_GLFWwindow* window, const GLFWLayerShellConfig *value) {
#define config window->ns.layer_shell.config
@@ -1986,10 +1956,6 @@ _glfwPlatformSetLayerShellConfig(_GLFWwindow* window, const GLFWLayerShellConfig
// HACK: Changing the style mask can cause the first responder to be cleared
[nswindow makeFirstResponder:window->ns.view];
NSScreen *screen = screen_for_window_center(window);
if (config.output_name[0]) {
NSScreen *q = screen_for_name(config.output_name);
if (q) screen = q;
}
unsigned cell_width, cell_height; double left_edge_spacing, top_edge_spacing, right_edge_spacing, bottom_edge_spacing;
float xscale = (float)config.expected.xscale, yscale = (float)config.expected.yscale;
_glfwPlatformGetWindowContentScale(window, &xscale, &yscale);
@@ -2031,11 +1997,6 @@ _glfwPlatformSetLayerShellConfig(_GLFWwindow* window, const GLFWLayerShellConfig
x += width - panel_width + 1.;
width = panel_width;
break;
case GLFW_EDGE_CENTER_SIZED:
x += (width - panel_width) / 2;
y += (height - panel_height) / 2;
width = panel_width; height = panel_height;
break;
default: // top left
y += height - panel_height + 1.;
height = panel_height; width = panel_width;
@@ -2045,10 +2006,8 @@ _glfwPlatformSetLayerShellConfig(_GLFWwindow* window, const GLFWLayerShellConfig
if (height < 1.) height = NSWidth(screen.visibleFrame);
}
if (config.edge != GLFW_EDGE_CENTER_SIZED) {
x += config.requested_left_margin; width -= config.requested_left_margin + config.requested_right_margin;
y += config.requested_bottom_margin; height -= config.requested_top_margin + config.requested_bottom_margin;
}
x += config.requested_left_margin; width -= config.requested_left_margin + config.requested_right_margin;
y += config.requested_bottom_margin; height -= config.requested_top_margin + config.requested_bottom_margin;
[nswindow setAnimationBehavior:animation_behavior];
[nswindow setLevel:level];
@@ -2228,39 +2187,14 @@ void _glfwPlatformMaximizeWindow(_GLFWwindow* window)
void _glfwPlatformShowWindow(_GLFWwindow* window)
{
NSRunningApplication *app = [[NSWorkspace sharedWorkspace] frontmostApplication];
window->ns.previous_front_most_application = 0;
if (app && app.processIdentifier != getpid()) window->ns.previous_front_most_application = app.processIdentifier;
if (window->ns.layer_shell.is_active && window->ns.layer_shell.config.type == GLFW_LAYER_SHELL_BACKGROUND) {
[window->ns.object orderBack:nil];
} else [window->ns.object orderFront:nil];
debug("Previously active application pid: %d bundle identifier: %s\n",
window->ns.previous_front_most_application, app ? app.bundleIdentifier.UTF8String : "");
}
void _glfwPlatformHideWindow(_GLFWwindow* window)
{
[window->ns.object orderOut:nil];
pid_t prev_app_pid = window->ns.previous_front_most_application; window->ns.previous_front_most_application = 0;
NSRunningApplication *app;
if (window->ns.layer_shell.is_active && prev_app_pid > 0 && (app = [NSRunningApplication runningApplicationWithProcessIdentifier:prev_app_pid])) {
unsigned num_visible = 0;
for (_GLFWwindow *w = _glfw.windowListHead; w; w = w->next) {
if (_glfwPlatformWindowVisible(w)) num_visible++;
}
if (!num_visible) {
// yieldActivationToApplication was introduced in macOS 14 (Sonoma)
SEL selector = NSSelectorFromString(@"yieldActivationToApplication:");
if ([NSApp respondsToSelector:selector]) {
[NSApp performSelector:selector withObject:app];
[app activateWithOptions:0];
} else {
#define NSApplicationActivateIgnoringOtherApps 2
[app activateWithOptions:NSApplicationActivateIgnoringOtherApps];
#undef NSApplicationActivateIgnoringOtherApps
}
}
}
}
void _glfwPlatformRequestWindowAttention(_GLFWwindow* window UNUSED)
@@ -3304,7 +3238,6 @@ glfwGetCocoaKeyEquivalent(uint32_t glfw_key, int glfw_mods, int *cocoa_mods) {
return _glfwPlatformGetNativeKeyForKey(glfw_key);
}
GLFWAPI bool glfwIsLayerShellSupported(void) { return true; }
//////////////////////////////////////////////////////////////////////////
////// GLFW internal API //////

11
glfw/dbus_glfw.c vendored
View File

@@ -214,6 +214,13 @@ typedef struct {
void *user_data;
} MethodResponse;
static const char*
format_message_error(DBusError *err) {
static char buf[1024];
snprintf(buf, sizeof(buf), "[%s] %s", err->name ? err->name : "", err->message);
return buf;
}
static void
method_reply_received(DBusPendingCall *pending, void *user_data) {
MethodResponse *res = (MethodResponse*)user_data;
@@ -221,7 +228,7 @@ method_reply_received(DBusPendingCall *pending, void *user_data) {
if (msg) {
DBusError err;
dbus_error_init(&err);
if (dbus_set_error_from_message(&err, msg)) res->callback(NULL, &err, res->user_data);
if (dbus_set_error_from_message(&err, msg)) res->callback(NULL, format_message_error(&err), res->user_data);
else res->callback(msg, NULL, res->user_data);
}
}
@@ -236,7 +243,7 @@ call_method_with_msg(DBusConnection *conn, DBusMessage *msg, int timeout, dbus_p
DBusError error; dbus_error_init(&error);
RAII_MSG(reply, dbus_connection_send_with_reply_and_block(session_bus, msg, timeout, &error));
if (dbus_error_is_set(&error)) {
callback(reply, &error, user_data);
callback(reply, error.message, user_data);
return false;
} else if (reply) {
callback(reply, NULL, user_data);

2
glfw/dbus_glfw.h vendored
View File

@@ -33,7 +33,7 @@
static inline void cleanup_msg(void *p) { DBusMessage *m = *(DBusMessage**)p; if (m) dbus_message_unref(m); m = NULL; }
#define RAII_MSG(name, initializer) __attribute__((cleanup(cleanup_msg))) DBusMessage *name = initializer
typedef void(*dbus_pending_callback)(DBusMessage *msg, const DBusError *err, void* data);
typedef void(*dbus_pending_callback)(DBusMessage *msg, const char* err, void* data);
typedef struct {
EventLoopData* eld;

View File

@@ -323,12 +323,16 @@ def generate_wrappers(glfw_header: str) -> None:
void glfwWaylandRunWithActivationToken(GLFWwindow *handle, GLFWactivationcallback cb, void *cb_data)
bool glfwWaylandSetTitlebarColor(GLFWwindow *handle, uint32_t color, bool use_system_color)
void glfwWaylandRedrawCSDWindowTitle(GLFWwindow *handle)
bool glfwWaylandIsLayerShellSupported(void)
bool glfwWaylandIsWindowFullyCreated(GLFWwindow *handle)
bool glfwWaylandBeep(GLFWwindow *handle)
GLFWLayerShellConfig* glfwWaylandLayerShellConfig(GLFWwindow *handle)
pid_t glfwWaylandCompositorPID(void)
unsigned long long glfwDBusUserNotify(const GLFWDBUSNotificationData *n, GLFWDBusnotificationcreatedfun callback, void *data)
void glfwDBusSetUserNotificationHandler(GLFWDBusnotificationactivatedfun handler)
int glfwSetX11LaunchCommand(GLFWwindow *handle, char **argv, int argc)
void glfwSetX11WindowAsDock(int32_t x11_window_id)
void glfwSetX11WindowStrut(int32_t x11_window_id, uint32_t dimensions[12])
'''.splitlines():
if line:
functions.append(Function(line.strip(), check_fail=False))

16
glfw/glfw3.h vendored
View File

@@ -1056,7 +1056,6 @@ typedef enum {
#define GLFW_WAYLAND_APP_ID 0x00025001
#define GLFW_WAYLAND_BGCOLOR 0x00025002
#define GLFW_WAYLAND_WINDOW_TAG 0x00025003
/*! @} */
#define GLFW_NO_API 0
@@ -1303,24 +1302,19 @@ typedef struct GLFWkeyevent
typedef enum { GLFW_LAYER_SHELL_NONE, GLFW_LAYER_SHELL_BACKGROUND, GLFW_LAYER_SHELL_PANEL, GLFW_LAYER_SHELL_TOP, GLFW_LAYER_SHELL_OVERLAY } GLFWLayerShellType;
typedef enum { GLFW_EDGE_TOP, GLFW_EDGE_BOTTOM, GLFW_EDGE_LEFT, GLFW_EDGE_RIGHT, GLFW_EDGE_CENTER, GLFW_EDGE_NONE, GLFW_EDGE_CENTER_SIZED } GLFWEdge;
typedef enum { GLFW_EDGE_TOP, GLFW_EDGE_BOTTOM, GLFW_EDGE_LEFT, GLFW_EDGE_RIGHT, GLFW_EDGE_CENTER, GLFW_EDGE_NONE } GLFWEdge;
typedef enum { GLFW_FOCUS_NOT_ALLOWED, GLFW_FOCUS_EXCLUSIVE, GLFW_FOCUS_ON_DEMAND} GLFWFocusPolicy;
typedef struct GLFWLayerShellConfig {
GLFWLayerShellType type;
GLFWEdge edge;
struct {
GLFWEdge edge;
int requested_top_margin, requested_left_margin, requested_bottom_margin, requested_right_margin;
} previous;
bool was_toggled_to_fullscreen;
char output_name[128];
char output_name[64];
GLFWFocusPolicy focus_policy;
unsigned x_size_in_cells, x_size_in_pixels;
unsigned y_size_in_cells, y_size_in_pixels;
int requested_top_margin, requested_left_margin, requested_bottom_margin, requested_right_margin;
int requested_exclusive_zone, hide_on_focus_loss;
int requested_exclusive_zone;
unsigned override_exclusive_zone;
void (*size_callback)(GLFWwindow *window, float xscale, float yscale, unsigned *cell_width, unsigned *cell_height, double *left_edge_spacing, double *top_edge_spacing, double *right_edge_spacing, double *bottom_edge_spacing);
struct { float xscale, yscale; } expected;
@@ -1983,7 +1977,6 @@ GLFWAPI GLFWdrawtextfun glfwSetDrawTextFunction(GLFWdrawtextfun function);
GLFWAPI GLFWcurrentselectionfun glfwSetCurrentSelectionCallback(GLFWcurrentselectionfun callback);
GLFWAPI GLFWhascurrentselectionfun glfwSetHasCurrentSelectionCallback(GLFWhascurrentselectionfun callback);
GLFWAPI GLFWimecursorpositionfun glfwSetIMECursorPositionCallback(GLFWimecursorpositionfun callback);
GLFWAPI bool glfwIsLayerShellSupported(void);
/*! @brief Terminates the GLFW library.
*
@@ -2386,7 +2379,6 @@ GLFWAPI void glfwGetMonitorContentScale(GLFWmonitor* monitor, float* xscale, flo
* @ingroup monitor
*/
GLFWAPI const char* glfwGetMonitorName(GLFWmonitor* monitor);
GLFWAPI const char* glfwGetMonitorDescription(GLFWmonitor* monitor);
/*! @brief Sets the user pointer of the specified monitor.
*
@@ -2881,7 +2873,6 @@ GLFWAPI GLFWwindow* glfwCreateWindow(int width, int height, const char* title, G
GLFWAPI bool glfwToggleFullscreen(GLFWwindow *window, unsigned int flags);
GLFWAPI bool glfwIsFullscreen(GLFWwindow *window, unsigned int flags);
GLFWAPI bool glfwAreSwapsAllowed(const GLFWwindow* window);
GLFWAPI const GLFWLayerShellConfig* glfwGetLayerShellConfig(GLFWwindow* handle);
GLFWAPI bool glfwSetLayerShellConfig(GLFWwindow* handle, const GLFWLayerShellConfig *value);
/*! @brief Destroys the specified window and its context.
@@ -4247,7 +4238,6 @@ GLFWAPI void glfwPostEmptyEvent(void);
GLFWAPI bool glfwGetIgnoreOSKeyboardProcessing(void);
GLFWAPI void glfwSetIgnoreOSKeyboardProcessing(bool enabled);
GLFWAPI bool glfwGrabKeyboard(int grab);
/*! @brief Returns the value of an input option for the specified window.
*

12
glfw/ibus_glfw.c vendored
View File

@@ -373,9 +373,9 @@ read_ibus_address(_GLFWIBUSData *ibus) {
}
void
input_context_created(DBusMessage *msg, const DBusError *err, void *data) {
if (err) {
_glfwInputError(GLFW_PLATFORM_ERROR, "IBUS: Failed to create input context with error: %s: %s", err->name, err->message);
input_context_created(DBusMessage *msg, const char* errmsg, void *data) {
if (errmsg) {
_glfwInputError(GLFW_PLATFORM_ERROR, "IBUS: Failed to create input context with error: %s", errmsg);
return;
}
const char *path = NULL;
@@ -488,15 +488,15 @@ glfw_ibus_set_cursor_geometry(_GLFWIBUSData *ibus, int x, int y, int w, int h) {
}
void
key_event_processed(DBusMessage *msg, const DBusError *err, void *data) {
key_event_processed(DBusMessage *msg, const char* errmsg, void *data) {
uint32_t handled = 0;
_GLFWIBUSKeyEvent *ev = (_GLFWIBUSKeyEvent*)data;
// Restore key's text from the text embedded in the structure.
ev->glfw_ev.text = ev->__embedded_text;
bool is_release = ev->glfw_ev.action == GLFW_RELEASE;
bool failed = false;
if (err) {
_glfwInputError(GLFW_PLATFORM_ERROR, "IBUS: Failed to process key with error: %s: %s", err->name, err->message);
if (errmsg) {
_glfwInputError(GLFW_PLATFORM_ERROR, "IBUS: Failed to process key with error: %s", errmsg);
failed = true;
} else {
glfw_dbus_get_args(msg, "Failed to get IBUS handled key from reply", DBUS_TYPE_BOOLEAN, &handled, DBUS_TYPE_INVALID);

7
glfw/input.c vendored
View File

@@ -684,13 +684,6 @@ GLFWAPI void glfwSetIgnoreOSKeyboardProcessing(bool enabled) {
_glfw.ignoreOSKeyboardProcessing = enabled;
}
GLFWAPI bool glfwGrabKeyboard(int grab) {
if (grab == 0 || grab == 1) {
if (_glfwPlatformGrabKeyboard(grab)) _glfw.keyboard_grabbed = grab;
}
return _glfw.keyboard_grabbed;
}
GLFWAPI int glfwGetInputMode(GLFWwindow* handle, int mode)
{
_GLFWwindow* window = (_GLFWwindow*) handle;

13
glfw/internal.h vendored
View File

@@ -331,7 +331,7 @@ struct _GLFWwndconfig
char instanceName[256];
} x11;
struct {
char appId[256], windowTag[256];
char appId[256];
uint32_t bgcolor;
} wl;
};
@@ -490,7 +490,7 @@ struct _GLFWwindow
//
struct _GLFWmonitor
{
const char *name, *description;
char* name;
void* userPointer;
// Physical dimensions in millimeters.
@@ -616,7 +616,7 @@ struct _GLFWlibrary
_GLFWtls contextSlot;
_GLFWmutex errorLock;
bool ignoreOSKeyboardProcessing, keyboard_grabbed;
bool ignoreOSKeyboardProcessing;
struct {
bool available;
@@ -668,10 +668,6 @@ struct _GLFWlibrary
//
extern _GLFWlibrary _glfw;
typedef struct GeometryRect { int x, y, width, height; } GeometryRect;
typedef struct MonitorGeometry {
GeometryRect full, workarea;
} MonitorGeometry;
//////////////////////////////////////////////////////////////////////////
////// GLFW platform API //////
@@ -719,7 +715,6 @@ void _glfwPlatformSetWindowTitle(_GLFWwindow* window, const char* title);
void _glfwPlatformSetWindowIcon(_GLFWwindow* window,
int count, const GLFWimage* images);
bool _glfwPlatformSetLayerShellConfig(_GLFWwindow* window, const GLFWLayerShellConfig *value);
const GLFWLayerShellConfig* _glfwPlatformGetLayerShellConfig(_GLFWwindow* window);
void _glfwPlatformGetWindowPos(_GLFWwindow* window, int* xpos, int* ypos);
void _glfwPlatformSetWindowPos(_GLFWwindow* window, int xpos, int ypos);
void _glfwPlatformGetWindowSize(_GLFWwindow* window, int* width, int* height);
@@ -883,8 +878,6 @@ unsigned long long _glfwPlatformAddTimer(monotonic_t interval, bool repeats, GLF
void _glfwPlatformUpdateTimer(unsigned long long timer_id, monotonic_t interval, bool enabled);
void _glfwPlatformRemoveTimer(unsigned long long timer_id);
int _glfwPlatformSetWindowBlur(_GLFWwindow* handle, int value);
MonitorGeometry _glfwPlatformGetMonitorGeometry(_GLFWmonitor* monitor);
bool _glfwPlatformGrabKeyboard(bool grab);
char* _glfw_strdup(const char* source);

View File

@@ -24,47 +24,15 @@ static int theme_size = -1;
static GLFWColorScheme appearance = GLFW_COLOR_SCHEME_NO_PREFERENCE;
static bool cursor_theme_changed = false, appearance_initialized = false;
#define HANDLER(name_) static void name_(DBusMessage *msg, const DBusError* err, void *data) { \
#define HANDLER(name) static void name(DBusMessage *msg, const char* errmsg, void *data) { \
(void)data; \
if (err) { \
_glfwInputError(GLFW_PLATFORM_ERROR, "%s: failed with error: %s: %s", #name_, err->name, err->message); \
if (errmsg) { \
_glfwInputError(GLFW_PLATFORM_ERROR, "%s: failed with error: %s", #name, errmsg); \
return; \
}
HANDLER(get_color_scheme_legacy)
DBusMessageIter iter, variant_iter, variant_iter2;
if (!dbus_message_iter_init(msg, &iter)) return;
dbus_message_iter_recurse(&iter, &variant_iter);
int type = dbus_message_iter_get_arg_type(&variant_iter);
if (type != DBUS_TYPE_VARIANT) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Read for color-scheme did not return a variant"); return;
}
dbus_message_iter_recurse(&variant_iter, &variant_iter2);
if (type != DBUS_TYPE_VARIANT) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Read for color-scheme did not return a nested variant"); return;
}
uint32_t val;
dbus_message_iter_get_basic(&variant_iter2, &val);
if (val < 3) appearance = val;
}
static void
get_color_scheme(DBusMessage *msg, const DBusError* err, void *data) {
(void) data;
if (err) {
if (strcmp("org.freedesktop.DBus.Error.UnknownMethod", err->name) == 0) {
DBusConnection *session_bus = glfw_dbus_session_bus();
if (session_bus) {
const char *namespace = FDO_DESKTOP_NAMESPACE, *key = FDO_APPEARANCE_KEY;
glfw_dbus_call_blocking_method(session_bus, DESKTOP_SERVICE, DESKTOP_PATH, DESKTOP_INTERFACE, "Read", DBUS_TIMEOUT_USE_DEFAULT,
get_color_scheme_legacy, NULL, DBUS_TYPE_STRING, &namespace, DBUS_TYPE_STRING, &key, DBUS_TYPE_INVALID);
}
return;
} else {
_glfwInputError(GLFW_PLATFORM_ERROR, "%s: failed with error: %s: %s", "get_color_scheme", err->name, err->message);
return;
}
}
HANDLER(get_color_scheme)
uint32_t val;
DBusMessageIter iter, variant_iter;
if (!dbus_message_iter_init(msg, &iter)) return;

10
glfw/linux_notify.c vendored
View File

@@ -32,9 +32,9 @@ glfw_dbus_set_user_notification_activated_handler(GLFWDBusnotificationactivatedf
}
void
notification_created(DBusMessage *msg, const DBusError* err, void *data) {
if (err) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Notify: Failed to create notification error: %s: %s", err->name, err->message);
notification_created(DBusMessage *msg, const char* errmsg, void *data) {
if (errmsg) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Notify: Failed to create notification error: %s", errmsg);
if (data) free(data);
return;
}
@@ -95,9 +95,9 @@ cancel_user_notification(DBusConnection *session_bus, uint32_t *id) {
}
static void
got_capabilities(DBusMessage *msg, const DBusError* err, void* data UNUSED) {
got_capabilities(DBusMessage *msg, const char* err, void* data UNUSED) {
if (err) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Notify: Failed to get server capabilities error: %s: %s", err->name, err->message);
_glfwInputError(GLFW_PLATFORM_ERROR, "Notify: Failed to get server capabilities error: %s", err);
return;
}
#define check_call(func, err, ...) if (!func(__VA_ARGS__)) { _glfwInputError(GLFW_PLATFORM_ERROR, "Notify: GetCapabilities: %s", err); return; }

15
glfw/monitor.c vendored
View File

@@ -186,8 +186,7 @@ void _glfwFreeMonitor(_GLFWmonitor* monitor)
_glfwFreeGammaArrays(&monitor->currentRamp);
free(monitor->modes);
free((void*)monitor->name);
free((void*)monitor->description);
free(monitor->name);
free(monitor);
}
@@ -394,19 +393,9 @@ GLFWAPI const char* glfwGetMonitorName(GLFWmonitor* handle)
assert(monitor != NULL);
_GLFW_REQUIRE_INIT_OR_RETURN(NULL);
return monitor->name ? monitor->name : "";
return monitor->name;
}
GLFWAPI const char* glfwGetMonitorDescription(GLFWmonitor* handle)
{
_GLFWmonitor* monitor = (_GLFWmonitor*) handle;
assert(monitor != NULL);
_GLFW_REQUIRE_INIT_OR_RETURN(NULL);
return monitor->description ? monitor->description : "";
}
GLFWAPI void glfwSetMonitorUserPointer(GLFWmonitor* handle, void* pointer)
{
_GLFWmonitor* monitor = (_GLFWmonitor*) handle;

View File

@@ -84,10 +84,8 @@
"staging/fractional-scale/fractional-scale-v1.xml",
"staging/single-pixel-buffer/single-pixel-buffer-v1.xml",
"unstable/idle-inhibit/idle-inhibit-unstable-v1.xml",
"unstable/keyboard-shortcuts-inhibit/keyboard-shortcuts-inhibit-unstable-v1.xml",
"staging/xdg-toplevel-icon/xdg-toplevel-icon-v1.xml",
"staging/xdg-system-bell/xdg-system-bell-v1.xml",
"staging/xdg-toplevel-tag/xdg-toplevel-tag-v1.xml",
"kwin-blur-v1.xml",
"wlr-layer-shell-unstable-v1.xml"

13
glfw/window.c vendored
View File

@@ -495,10 +495,6 @@ GLFWAPI void glfwWindowHintString(int hint, const char* value)
strncpy(_glfw.hints.window.wl.appId, value,
sizeof(_glfw.hints.window.wl.appId) - 1);
return;
case GLFW_WAYLAND_WINDOW_TAG:
strncpy(_glfw.hints.window.wl.windowTag, value,
sizeof(_glfw.hints.window.wl.windowTag) - 1);
return;
}
_glfwInputError(GLFW_INVALID_ENUM, "Invalid window hint string 0x%08X", hint);
@@ -555,15 +551,6 @@ GLFWAPI void glfwSetWindowShouldClose(GLFWwindow* handle, int value)
window->shouldClose = value;
}
GLFWAPI const GLFWLayerShellConfig* glfwGetLayerShellConfig(GLFWwindow* handle) {
_GLFWwindow* window = (_GLFWwindow*) handle;
assert(window != NULL);
_GLFW_REQUIRE_INIT_OR_RETURN(false);
return _glfwPlatformGetLayerShellConfig(window);
}
GLFWAPI bool glfwSetLayerShellConfig(GLFWwindow* handle, const GLFWLayerShellConfig *value) {
_GLFWwindow* window = (_GLFWwindow*) handle;
assert(window != NULL);

10
glfw/wl_init.c vendored
View File

@@ -600,16 +600,11 @@ static void registryHandleGlobal(void* data UNUSED,
else if (is(zwp_idle_inhibit_manager_v1)) {
_glfw.wl.idle_inhibit_manager = wl_registry_bind(registry, name, &zwp_idle_inhibit_manager_v1_interface, 1);
}
else if (is(zwp_keyboard_shortcuts_inhibit_manager_v1)) {
_glfw.wl.keyboard_shortcuts_inhibit_manager = wl_registry_bind(registry, name, &zwp_keyboard_shortcuts_inhibit_manager_v1_interface, 1);
}
else if (is(xdg_toplevel_icon_manager_v1)) {
_glfw.wl.xdg_toplevel_icon_manager_v1 = wl_registry_bind(registry, name, &xdg_toplevel_icon_manager_v1_interface, 1);
}
else if (is(xdg_system_bell_v1)) {
_glfw.wl.xdg_system_bell_v1 = wl_registry_bind(registry, name, &xdg_system_bell_v1_interface, 1);
} else if (is(xdg_toplevel_tag_manager_v1)) {
_glfw.wl.xdg_toplevel_tag_manager_v1 = wl_registry_bind(registry, name, &xdg_toplevel_tag_manager_v1_interface, 1);
}
#undef is
}
@@ -721,7 +716,6 @@ get_compositor_missing_capabilities(void) {
C(cursor_shape, wp_cursor_shape_manager_v1); C(layer_shell, zwlr_layer_shell_v1);
C(single_pixel_buffer, wp_single_pixel_buffer_manager_v1); C(preferred_scale, has_preferred_buffer_scale);
C(idle_inhibit, idle_inhibit_manager); C(icon, xdg_toplevel_icon_manager_v1); C(bell, xdg_system_bell_v1);
C(window-tag, xdg_toplevel_tag_manager_v1); C(keyboard_shortcuts_inhibit, keyboard_shortcuts_inhibit_manager);
if (_glfw.wl.xdg_wm_base_version < 6) p += snprintf(p, sizeof(buf) - (p - buf), "%s ", "window-state-suspended");
if (_glfw.wl.xdg_wm_base_version < 5) p += snprintf(p, sizeof(buf) - (p - buf), "%s ", "window-capabilities");
#undef C
@@ -900,8 +894,6 @@ void _glfwPlatformTerminate(void)
xdg_toplevel_icon_manager_v1_destroy(_glfw.wl.xdg_toplevel_icon_manager_v1);
if (_glfw.wl.xdg_system_bell_v1)
xdg_system_bell_v1_destroy(_glfw.wl.xdg_system_bell_v1);
if (_glfw.wl.xdg_toplevel_tag_manager_v1)
xdg_toplevel_tag_manager_v1_destroy(_glfw.wl.xdg_toplevel_tag_manager_v1);
if (_glfw.wl.wp_single_pixel_buffer_manager_v1)
wp_single_pixel_buffer_manager_v1_destroy(_glfw.wl.wp_single_pixel_buffer_manager_v1);
if (_glfw.wl.wp_cursor_shape_manager_v1)
@@ -916,8 +908,6 @@ void _glfwPlatformTerminate(void)
zwlr_layer_shell_v1_destroy(_glfw.wl.zwlr_layer_shell_v1);
if (_glfw.wl.idle_inhibit_manager)
zwp_idle_inhibit_manager_v1_destroy(_glfw.wl.idle_inhibit_manager);
if (_glfw.wl.keyboard_shortcuts_inhibit_manager)
zwp_keyboard_shortcuts_inhibit_manager_v1_destroy(_glfw.wl.keyboard_shortcuts_inhibit_manager);
if (_glfw.wl.registry)
wl_registry_destroy(_glfw.wl.registry);

24
glfw/wl_monitor.c vendored
View File

@@ -41,15 +41,20 @@ static void outputHandleGeometry(void* data,
int32_t physicalWidth,
int32_t physicalHeight,
int32_t subpixel UNUSED,
const char* make UNUSED,
const char* model UNUSED,
const char* make,
const char* model,
int32_t transform UNUSED)
{
struct _GLFWmonitor *monitor = data;
char name[1024];
monitor->wl.x = x;
monitor->wl.y = y;
monitor->widthMM = physicalWidth;
monitor->heightMM = physicalHeight;
snprintf(name, sizeof(name), "%s %s", make, model);
monitor->name = _glfw_strdup(name);
}
static void outputHandleMode(void* data,
@@ -100,20 +105,15 @@ static void outputHandleName(void* data,
struct wl_output* output UNUSED,
const char* name) {
struct _GLFWmonitor *monitor = data;
if (name) {
if (monitor->name) free((void*)monitor->name);
monitor->name = _glfw_strdup(name);
}
if (name) strncpy(monitor->wl.friendly_name, name, sizeof(monitor->wl.friendly_name)-1);
}
static void outputHandleDescription(void* data,
struct wl_output* output UNUSED,
const char* description) {
struct _GLFWmonitor *monitor = data;
if (description) {
if (monitor->description) free((void*)monitor->description);
monitor->description = _glfw_strdup(description);
}
if (description) strncpy(monitor->wl.description, description, sizeof(monitor->wl.description)-1);
}
static const struct wl_output_listener outputListener = {
@@ -142,8 +142,8 @@ void _glfwAddOutputWayland(uint32_t name, uint32_t version)
return;
}
// The actual name of this output will be set in the handlers.
monitor = _glfwAllocMonitor("unnamed", 0, 0);
// The actual name of this output will be set in the geometry handler.
monitor = _glfwAllocMonitor(NULL, 0, 0);
output = wl_registry_bind(_glfw.wl.registry,
name,

8
glfw/wl_platform.h vendored
View File

@@ -66,10 +66,8 @@ typedef VkBool32 (APIENTRY *PFN_vkGetPhysicalDeviceWaylandPresentationSupportKHR
#include "wayland-wlr-layer-shell-unstable-v1-client-protocol.h"
#include "wayland-single-pixel-buffer-v1-client-protocol.h"
#include "wayland-idle-inhibit-unstable-v1-client-protocol.h"
#include "wayland-keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h"
#include "wayland-xdg-toplevel-icon-v1-client-protocol.h"
#include "wayland-xdg-system-bell-v1-client-protocol.h"
#include "wayland-xdg-toplevel-tag-v1-client-protocol.h"
#define _glfw_dlopen(name) dlopen(name, RTLD_LAZY | RTLD_LOCAL)
#define _glfw_dlclose(handle) dlclose(handle)
@@ -212,7 +210,7 @@ typedef struct _GLFWwindowWayland
double cursorPosX, cursorPosY, allCursorPosX, allCursorPosY;
char* title;
char appId[256], windowTag[256];
char appId[256];
// We need to track the monitors the window spans on to calculate the
// optimal scaling factor.
@@ -291,7 +289,6 @@ typedef struct _GLFWwindowWayland
WaylandWindowState toplevel_states;
uint32_t decoration_mode;
} current, pending;
struct zwp_keyboard_shortcuts_inhibitor_v1 *keyboard_shortcuts_inhibitor;
} _GLFWwindowWayland;
typedef enum _GLFWWaylandOfferType
@@ -343,7 +340,6 @@ typedef struct _GLFWlibraryWayland
struct xdg_activation_v1* xdg_activation_v1;
struct xdg_toplevel_icon_manager_v1* xdg_toplevel_icon_manager_v1;
struct xdg_system_bell_v1* xdg_system_bell_v1;
struct xdg_toplevel_tag_manager_v1* xdg_toplevel_tag_manager_v1;
struct wp_cursor_shape_manager_v1* wp_cursor_shape_manager_v1;
struct wp_cursor_shape_device_v1* wp_cursor_shape_device_v1;
struct wp_fractional_scale_manager_v1 *wp_fractional_scale_manager_v1;
@@ -352,7 +348,6 @@ typedef struct _GLFWlibraryWayland
struct zwlr_layer_shell_v1* zwlr_layer_shell_v1; uint32_t zwlr_layer_shell_v1_version;
struct wp_single_pixel_buffer_manager_v1 *wp_single_pixel_buffer_manager_v1;
struct zwp_idle_inhibit_manager_v1* idle_inhibit_manager;
struct zwp_keyboard_shortcuts_inhibit_manager_v1 *keyboard_shortcuts_inhibit_manager;
int compositorVersion;
int seatVersion;
@@ -411,6 +406,7 @@ typedef struct _GLFWmonitorWayland
{
struct wl_output* output;
uint32_t name;
char friendly_name[64], description[64];
int currentMode;
int x;

87
glfw/wl_window.c vendored
View File

@@ -46,18 +46,6 @@
static bool
is_layer_shell(_GLFWwindow *window) { return window->wl.layer_shell.config.type != GLFW_LAYER_SHELL_NONE; }
static void
inhibit_shortcuts_for(_GLFWwindow *window, bool inhibit) {
if (inhibit) {
if (window->wl.keyboard_shortcuts_inhibitor) return;
window->wl.keyboard_shortcuts_inhibitor = zwp_keyboard_shortcuts_inhibit_manager_v1_inhibit_shortcuts(_glfw.wl.keyboard_shortcuts_inhibit_manager, window->wl.surface, _glfw.wl.seat);
} else {
if (!window->wl.keyboard_shortcuts_inhibitor) return;
zwp_keyboard_shortcuts_inhibitor_v1_destroy(window->wl.keyboard_shortcuts_inhibitor);
window->wl.keyboard_shortcuts_inhibitor = NULL;
}
}
static void
activation_token_done(void *data, struct xdg_activation_token_v1 *xdg_token, const char *token) {
for (size_t i = 0; i < _glfw.wl.activation_requests.sz; i++) {
@@ -628,7 +616,6 @@ static bool createSurface(_GLFWwindow* window,
update_regions(window);
wl_surface_set_buffer_scale(window->wl.surface, scale);
if (_glfw.keyboard_grabbed) inhibit_shortcuts_for(window, true);
return true;
}
@@ -809,6 +796,7 @@ apply_xdg_configure_changes(_GLFWwindow *window) {
window->wl.current.toplevel_states = new_states;
window->wl.current.width = width;
window->wl.current.height = height;
_glfwInputWindowFocus(window, window->wl.current.toplevel_states & TOPLEVEL_STATE_ACTIVATED);
if (live_resize_done) report_live_resize(window, false);
}
}
@@ -983,25 +971,13 @@ find_output_by_name(const char* name) {
if (!name || !name[0]) return NULL;
for (int i = 0; i < _glfw.monitorCount; i++) {
_GLFWmonitor *m = _glfw.monitors[i];
if (strcmp(m->name, name) == 0) return m->wl.output;
if (strcmp(m->wl.friendly_name, name) == 0) return m->wl.output;
}
return NULL;
}
static enum zwlr_layer_shell_v1_layer
get_layer_shell_layer(const _GLFWwindow *window) {
enum zwlr_layer_shell_v1_layer which_layer = ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND; // Default to background
switch (window->wl.layer_shell.config.type) {
case GLFW_LAYER_SHELL_BACKGROUND: case GLFW_LAYER_SHELL_NONE: break;
case GLFW_LAYER_SHELL_PANEL: which_layer = ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM; break;
case GLFW_LAYER_SHELL_TOP: which_layer = ZWLR_LAYER_SHELL_V1_LAYER_TOP; break;
case GLFW_LAYER_SHELL_OVERLAY: which_layer = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY; break;
}
return which_layer;
}
static void
layer_set_properties(const _GLFWwindow *window, bool during_creation, uint32_t width, uint32_t height) {
layer_set_properties(const _GLFWwindow *window, uint32_t width, uint32_t height) {
#define config window->wl.layer_shell.config
enum zwlr_layer_surface_v1_anchor which_anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT;
int exclusive_zone = config.requested_exclusive_zone;
@@ -1040,13 +1016,12 @@ layer_set_properties(const _GLFWwindow *window, bool during_creation, uint32_t w
if (!config.override_exclusive_zone) exclusive_zone = width;
break;
case GLFW_EDGE_CENTER:
break;
case GLFW_EDGE_CENTER_SIZED:
panel_width = width; panel_height = height;
which_anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT | ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM;
break;
case GLFW_EDGE_NONE:
which_anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP;
panel_width = width; panel_height = height;
panel_width = width;
panel_height = height;
break;
}
}
@@ -1056,7 +1031,6 @@ layer_set_properties(const _GLFWwindow *window, bool during_creation, uint32_t w
zwlr_layer_surface_v1_set_anchor(surface, which_anchor);
zwlr_layer_surface_v1_set_exclusive_zone(surface, exclusive_zone);
zwlr_layer_surface_v1_set_margin(surface, config.requested_top_margin, config.requested_right_margin, config.requested_bottom_margin, config.requested_left_margin);
if (!during_creation) zwlr_layer_surface_v1_set_layer(surface, get_layer_shell_layer(window));
zwlr_layer_surface_v1_set_keyboard_interactivity(surface, focus_policy);
#undef surface
#undef config
@@ -1067,10 +1041,8 @@ calculate_layer_size(_GLFWwindow *window, uint32_t *width, uint32_t *height) {
const GLFWLayerShellConfig *config = &window->wl.layer_shell.config;
GLFWvidmode m = {0};
if (window->wl.monitorsCount) _glfwPlatformGetVideoMode(window->wl.monitors[0], &m);
int monitor_width = m.width, monitor_height = m.height;
const int y_margin = config->requested_bottom_margin + config->requested_top_margin, x_margin = config->requested_left_margin + config->requested_right_margin;
monitor_width = monitor_width > x_margin ? monitor_width - x_margin : 0;
monitor_height = monitor_height > y_margin ? monitor_height - y_margin : 0;
const int monitor_width = MAX(0, m.width - x_margin), monitor_height = MAX(0, m.height - y_margin);
float xscale = (float)config->expected.xscale, yscale = (float)config->expected.yscale;
if (window->wl.window_fully_created) _glfwPlatformGetWindowContentScale(window, &xscale, &yscale);
unsigned cell_width, cell_height; double left_edge_spacing, top_edge_spacing, right_edge_spacing, bottom_edge_spacing;
@@ -1082,9 +1054,9 @@ calculate_layer_size(_GLFWwindow *window, uint32_t *width, uint32_t *height) {
if (!*height) *height = monitor_height;
return;
}
debug("Calculating layer shell window size at scale: %f\n", xscale);
const unsigned xsz = config->x_size_in_pixels ? (unsigned)(config->x_size_in_pixels * xscale) : (cell_width * config->x_size_in_cells);
const unsigned ysz = config->y_size_in_pixels ? (unsigned)(config->y_size_in_pixels * yscale) : (cell_height * config->y_size_in_cells);
debug("Calculating layer shell window size at scale: %f cell_size: %u %u sz: %u %u\n", xscale, cell_width, cell_height, xsz, ysz);
if (config->edge == GLFW_EDGE_LEFT || config->edge == GLFW_EDGE_RIGHT) {
if (!*height) *height = monitor_height;
double spacing = spacing_x;
@@ -1104,7 +1076,6 @@ calculate_layer_size(_GLFWwindow *window, uint32_t *width, uint32_t *height) {
*width = (uint32_t)(1. + spacing_x);
*height = (uint32_t)(1. + spacing_y);
}
}
static void
@@ -1117,6 +1088,8 @@ layer_surface_handle_configure(void* data, struct zwlr_layer_surface_v1* surface
window->wl.once.surface_configured = true;
update_fully_created_on_configure(window);
}
GLFWvidmode m = {0};
if (window->wl.monitorsCount) _glfwPlatformGetVideoMode(window->wl.monitors[0], &m);
calculate_layer_size(window, &width, &height);
zwlr_layer_surface_v1_ack_configure(surface, serial);
if ((int)width != window->wl.width || (int)height != window->wl.height) {
@@ -1125,7 +1098,7 @@ layer_surface_handle_configure(void* data, struct zwlr_layer_surface_v1* surface
window->wl.width = width; window->wl.height = height;
resizeFramebuffer(window);
_glfwInputWindowDamage(window);
layer_set_properties(window, false, window->wl.width, window->wl.height);
layer_set_properties(window, window->wl.width, window->wl.height);
if (window->wl.wp_viewport) wp_viewport_set_destination(window->wl.wp_viewport, window->wl.width, window->wl.height);
}
commit_window_surface_if_safe(window);
@@ -1154,15 +1127,22 @@ create_layer_shell_surface(_GLFWwindow *window) {
}
window->decorated = false; // shell windows must not have decorations
struct wl_output *wl_output = find_output_by_name(window->wl.layer_shell.config.output_name);
enum zwlr_layer_shell_v1_layer which_layer = ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND; // Default to background
switch (window->wl.layer_shell.config.type) {
case GLFW_LAYER_SHELL_BACKGROUND: break; case GLFW_LAYER_SHELL_NONE: break;
case GLFW_LAYER_SHELL_PANEL: which_layer = ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM; break;
case GLFW_LAYER_SHELL_TOP: which_layer = ZWLR_LAYER_SHELL_V1_LAYER_TOP; break;
case GLFW_LAYER_SHELL_OVERLAY: which_layer = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY; break;
}
#define ls window->wl.layer_shell.zwlr_layer_surface_v1
ls = zwlr_layer_shell_v1_get_layer_surface(
_glfw.wl.zwlr_layer_shell_v1, window->wl.surface, wl_output, get_layer_shell_layer(window), window->wl.appId[0] ? window->wl.appId : "kitty");
_glfw.wl.zwlr_layer_shell_v1, window->wl.surface, wl_output, which_layer, "kitty");
if (!ls) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: layer-surface creation failed");
return false;
}
zwlr_layer_surface_v1_add_listener(ls, &zwlr_layer_surface_v1_listener, window);
layer_set_properties(window, true, window->wl.width, window->wl.height);
layer_set_properties(window, window->wl.width, window->wl.height);
if (window->wl.wp_viewport) wp_viewport_set_destination(window->wl.wp_viewport, window->wl.width, window->wl.height);
commit_window_surface(window);
wl_display_roundtrip(_glfw.wl.display);
@@ -1210,10 +1190,8 @@ create_window_desktop_surface(_GLFWwindow* window)
zxdg_toplevel_decoration_v1_add_listener(window->wl.xdg.decoration, &xdgDecorationListener, window);
}
if (window->wl.appId[0])
if (strlen(window->wl.appId))
xdg_toplevel_set_app_id(window->wl.xdg.toplevel, window->wl.appId);
if (window->wl.windowTag[0] && _glfw.wl.xdg_toplevel_tag_manager_v1)
xdg_toplevel_tag_manager_v1_set_toplevel_tag(_glfw.wl.xdg_toplevel_tag_manager_v1, window->wl.xdg.toplevel, window->wl.windowTag);
if (window->wl.title)
xdg_toplevel_set_title(window->wl.xdg.toplevel, window->wl.title);
@@ -1481,8 +1459,6 @@ void _glfwPlatformDestroyWindow(_GLFWwindow* window)
if (window->id == _glfw.wl.keyRepeatInfo.keyboardFocusId) {
_glfw.wl.keyRepeatInfo.keyboardFocusId = 0;
}
if (window->wl.keyboard_shortcuts_inhibitor)
zwp_keyboard_shortcuts_inhibitor_v1_destroy(window->wl.keyboard_shortcuts_inhibitor);
if (window->wl.temp_buffer_used_during_window_creation)
wl_buffer_destroy(window->wl.temp_buffer_used_during_window_creation);
@@ -1751,7 +1727,7 @@ void _glfwPlatformShowWindow(_GLFWwindow* window)
window->wl.visible = true;
} else {
// workaround for kwin layer shell bug: https://bugs.kde.org/show_bug.cgi?id=503121
if (is_layer_shell(window)) layer_set_properties(window, false, window->wl.width, window->wl.height);
if (is_layer_shell(window)) layer_set_properties(window, window->wl.width, window->wl.height);
window->wl.visible = true;
commit_window_surface(window);
}
@@ -1776,7 +1752,7 @@ _glfwPlatformSetLayerShellConfig(_GLFWwindow* window, const GLFWLayerShellConfig
if (value) window->wl.layer_shell.config = *value;
uint32_t width, height;
calculate_layer_size(window, &width, &height);
layer_set_properties(window, false, width, height);
layer_set_properties(window, width, height);
commit_window_surface(window);
return true;
}
@@ -2834,16 +2810,6 @@ _glfwPlatformSetWindowBlur(_GLFWwindow *window, int blur_radius) {
return has_blur ? 1 : 0;
}
bool
_glfwPlatformGrabKeyboard(bool grab) {
if (!_glfw.wl.keyboard_shortcuts_inhibit_manager) {
_glfwInputError(GLFW_PLATFORM_ERROR, "The Wayland compositor does not implement inhibit-keyboard-shortcuts, cannot grab keyboard");
return false;
}
for (_GLFWwindow* window = _glfw.windowListHead; window; window = window->next) inhibit_shortcuts_for(window, grab);
return true;
}
//////////////////////////////////////////////////////////////////////////
////// GLFW native API //////
//////////////////////////////////////////////////////////////////////////
@@ -2919,12 +2885,11 @@ GLFWAPI void glfwWaylandRedrawCSDWindowTitle(GLFWwindow *handle) {
if (csd_change_title(window)) commit_window_surface_if_safe(window);
}
const GLFWLayerShellConfig*
_glfwPlatformGetLayerShellConfig(_GLFWwindow *window) {
return &window->wl.layer_shell.config;
GLFWAPI GLFWLayerShellConfig* glfwWaylandLayerShellConfig(GLFWwindow *handle) {
return &((_GLFWwindow*)handle)->wl.layer_shell.config;
}
GLFWAPI bool glfwIsLayerShellSupported(void) { return _glfw.wl.zwlr_layer_shell_v1 != NULL; }
GLFWAPI bool glfwWaylandIsLayerShellSupported(void) { return _glfw.wl.zwlr_layer_shell_v1 != NULL; }
GLFWAPI bool glfwWaylandIsWindowFullyCreated(GLFWwindow *handle) { return handle != NULL && ((_GLFWwindow*)handle)->wl.window_fully_created; }

72
glfw/x11_init.c vendored
View File

@@ -47,11 +47,16 @@
//
static Atom getAtomIfSupported(Atom* supportedAtoms,
unsigned long atomCount,
const Atom atom)
const char* atomName)
{
for (unsigned long i = 0; i < atomCount; i++) {
if (supportedAtoms[i] == atom) return atom;
const Atom atom = XInternAtom(_glfw.x11.display, atomName, False);
for (unsigned long i = 0; i < atomCount; i++)
{
if (supportedAtoms[i] == atom)
return atom;
}
return None;
}
@@ -115,36 +120,39 @@ static void detectEWMH(void)
// See which of the atoms we support that are supported by the WM
#define ALL_ATOMS \
S(NET_WM_STATE) S(NET_WM_STATE_ABOVE) S(NET_WM_STATE_BELOW) S(NET_WM_STATE_FULLSCREEN) \
S(NET_WM_STATE_MAXIMIZED_VERT) S(NET_WM_STATE_MAXIMIZED_HORZ) S(NET_WM_STATE_DEMANDS_ATTENTION) \
S(NET_WM_STATE_SKIP_TASKBAR) S(NET_WM_STATE_SKIP_PAGER) S(NET_WM_STATE_STICKY) \
\
S(NET_WM_FULLSCREEN_MONITORS) S(NET_WM_STRUT_PARTIAL) \
\
S(NET_WM_WINDOW_TYPE) S(NET_WM_WINDOW_TYPE_NORMAL) S(NET_WM_WINDOW_TYPE_DOCK) S(NET_WM_WINDOW_TYPE_DESKTOP) \
S(NET_WM_WINDOW_TYPE_UTILITY) S(NET_WM_WINDOW_TYPE_SPLASH) S(NET_WM_WINDOW_TYPE_DIALOG) S(NET_WM_WINDOW_TYPE_MENU) \
S(NET_WM_WINDOW_TYPE_NOTIFICATION) \
\
S(NET_WORKAREA) S(NET_CURRENT_DESKTOP) S(NET_ACTIVE_WINDOW) S(NET_FRAME_EXTENTS) S(NET_REQUEST_FRAME_EXTENTS) \
\
S(NET_WM_ALLOWED_ACTIONS) S(NET_WM_ACTION_MOVE) S(NET_WM_ACTION_RESIZE) S(NET_WM_ACTION_MINIMIZE) \
S(NET_WM_ACTION_SHADE) S(NET_WM_ACTION_STICK) S(NET_WM_ACTION_MAXIMIZE_HORZ) S(NET_WM_ACTION_MAXIMIZE_VERT) \
S(NET_WM_ACTION_FULLSCREEN) S(NET_WM_ACTION_CHANGE_DESKTOP) S(NET_WM_ACTION_CLOSE) S(NET_WM_ACTION_ABOVE) \
S(NET_WM_ACTION_BELOW) S(NET_WM_ACTION_ABOVE_BELOW)
_glfw.x11.NET_WM_STATE =
getAtomIfSupported(supportedAtoms, atomCount, "_NET_WM_STATE");
_glfw.x11.NET_WM_STATE_ABOVE =
getAtomIfSupported(supportedAtoms, atomCount, "_NET_WM_STATE_ABOVE");
_glfw.x11.NET_WM_STATE_FULLSCREEN =
getAtomIfSupported(supportedAtoms, atomCount, "_NET_WM_STATE_FULLSCREEN");
_glfw.x11.NET_WM_STATE_MAXIMIZED_VERT =
getAtomIfSupported(supportedAtoms, atomCount, "_NET_WM_STATE_MAXIMIZED_VERT");
_glfw.x11.NET_WM_STATE_MAXIMIZED_HORZ =
getAtomIfSupported(supportedAtoms, atomCount, "_NET_WM_STATE_MAXIMIZED_HORZ");
_glfw.x11.NET_WM_STATE_DEMANDS_ATTENTION =
getAtomIfSupported(supportedAtoms, atomCount, "_NET_WM_STATE_DEMANDS_ATTENTION");
_glfw.x11.NET_WM_FULLSCREEN_MONITORS =
getAtomIfSupported(supportedAtoms, atomCount, "_NET_WM_FULLSCREEN_MONITORS");
_glfw.x11.NET_WM_WINDOW_TYPE =
getAtomIfSupported(supportedAtoms, atomCount, "_NET_WM_WINDOW_TYPE");
_glfw.x11.NET_WM_WINDOW_TYPE_NORMAL =
getAtomIfSupported(supportedAtoms, atomCount, "_NET_WM_WINDOW_TYPE_NORMAL");
_glfw.x11.NET_WM_WINDOW_TYPE_DOCK =
getAtomIfSupported(supportedAtoms, atomCount, "_NET_WM_WINDOW_TYPE_DOCK");
_glfw.x11.NET_WORKAREA =
getAtomIfSupported(supportedAtoms, atomCount, "_NET_WORKAREA");
_glfw.x11.NET_CURRENT_DESKTOP =
getAtomIfSupported(supportedAtoms, atomCount, "_NET_CURRENT_DESKTOP");
_glfw.x11.NET_ACTIVE_WINDOW =
getAtomIfSupported(supportedAtoms, atomCount, "_NET_ACTIVE_WINDOW");
_glfw.x11.NET_FRAME_EXTENTS =
getAtomIfSupported(supportedAtoms, atomCount, "_NET_FRAME_EXTENTS");
_glfw.x11.NET_REQUEST_FRAME_EXTENTS =
getAtomIfSupported(supportedAtoms, atomCount, "_NET_REQUEST_FRAME_EXTENTS");
_glfw.x11.NET_WM_STRUT_PARTIAL =
getAtomIfSupported(supportedAtoms, atomCount, "_NET_WM_STRUT_PARTIAL");
static const char* atom_names[40] = {
#define S(x) "_" #x,
ALL_ATOMS
};
#undef S
Atom atoms[arraysz(atom_names)];
XInternAtoms(_glfw.x11.display, (char**)atom_names, arraysz(atom_names), False, atoms);
unsigned i = 0;
#define S(name) _glfw.x11.name = getAtomIfSupported(supportedAtoms, atomCount, atoms[i++]);
ALL_ATOMS
#undef S
#undef ALL_ATOMS
XFree(supportedAtoms);
}

103
glfw/x11_monitor.c vendored
View File

@@ -350,79 +350,96 @@ void _glfwPlatformGetMonitorContentScale(_GLFWmonitor* monitor UNUSED,
*yscale = _glfw.x11.contentScaleY;
}
MonitorGeometry
_glfwPlatformGetMonitorGeometry(_GLFWmonitor *monitor) {
MonitorGeometry ans = {0};
if (!monitor) return ans;
if (_glfw.x11.randr.available && !_glfw.x11.randr.monitorBroken) {
void _glfwPlatformGetMonitorWorkarea(_GLFWmonitor* monitor, int* xpos, int* ypos, int* width, int* height)
{
int areaX = 0, areaY = 0, areaWidth = 0, areaHeight = 0;
if (_glfw.x11.randr.available && !_glfw.x11.randr.monitorBroken)
{
XRRScreenResources* sr =
XRRGetScreenResourcesCurrent(_glfw.x11.display, _glfw.x11.root);
XRRCrtcInfo* ci = XRRGetCrtcInfo(_glfw.x11.display, sr, monitor->x11.crtc);
ans.full.x = ci->x;
ans.full.y = ci->y;
areaX = ci->x;
areaY = ci->y;
const XRRModeInfo* mi = getModeInfo(sr, ci->mode);
if (ci->rotation == RR_Rotate_90 || ci->rotation == RR_Rotate_270) {
ans.full.width = mi->height;
ans.full.height = mi->width;
} else {
ans.full.width = mi->width;
ans.full.height = mi->height;
if (ci->rotation == RR_Rotate_90 || ci->rotation == RR_Rotate_270)
{
areaWidth = mi->height;
areaHeight = mi->width;
}
else
{
areaWidth = mi->width;
areaHeight = mi->height;
}
XRRFreeCrtcInfo(ci);
XRRFreeScreenResources(sr);
} else {
ans.full.width = DisplayWidth(_glfw.x11.display, _glfw.x11.screen);
ans.full.height = DisplayHeight(_glfw.x11.display, _glfw.x11.screen);
}
ans.workarea = *(&ans.full);
else
{
areaWidth = DisplayWidth(_glfw.x11.display, _glfw.x11.screen);
areaHeight = DisplayHeight(_glfw.x11.display, _glfw.x11.screen);
}
if (_glfw.x11.NET_WORKAREA && _glfw.x11.NET_CURRENT_DESKTOP) {
if (_glfw.x11.NET_WORKAREA && _glfw.x11.NET_CURRENT_DESKTOP)
{
Atom* extents = NULL;
Atom* desktop = NULL;
const unsigned long extentCount = _glfwGetWindowPropertyX11(
_glfw.x11.root, _glfw.x11.NET_WORKAREA, XA_CARDINAL, (unsigned char**) &extents);
const unsigned long extentCount =
_glfwGetWindowPropertyX11(_glfw.x11.root,
_glfw.x11.NET_WORKAREA,
XA_CARDINAL,
(unsigned char**) &extents);
if (_glfwGetWindowPropertyX11(_glfw.x11.root, _glfw.x11.NET_CURRENT_DESKTOP, XA_CARDINAL, (unsigned char**) &desktop) > 0) {
if (extentCount >= 4 && *desktop < extentCount / 4) {
if (_glfwGetWindowPropertyX11(_glfw.x11.root,
_glfw.x11.NET_CURRENT_DESKTOP,
XA_CARDINAL,
(unsigned char**) &desktop) > 0)
{
if (extentCount >= 4 && *desktop < extentCount / 4)
{
const int globalX = extents[*desktop * 4 + 0];
const int globalY = extents[*desktop * 4 + 1];
const int globalWidth = extents[*desktop * 4 + 2];
const int globalHeight = extents[*desktop * 4 + 3];
if (ans.workarea.x < globalX) {
ans.workarea.width -= globalX - ans.workarea.x;
ans.workarea.x = globalX;
if (areaX < globalX)
{
areaWidth -= globalX - areaX;
areaX = globalX;
}
if (ans.workarea.y < globalY) {
ans.workarea.height -= globalY - ans.workarea.y;
ans.workarea.y = globalY;
if (areaY < globalY)
{
areaHeight -= globalY - areaY;
areaY = globalY;
}
if (ans.workarea.x + ans.workarea.width > globalX + globalWidth)
ans.workarea.width = globalX - ans.workarea.x + globalWidth;
if (ans.workarea.y + ans.workarea.height > globalY + globalHeight)
ans.workarea.height = globalY - ans.workarea.y + globalHeight;
if (areaX + areaWidth > globalX + globalWidth)
areaWidth = globalX - areaX + globalWidth;
if (areaY + areaHeight > globalY + globalHeight)
areaHeight = globalY - areaY + globalHeight;
}
}
if (extents) XFree(extents);
if (desktop) XFree(desktop);
}
return ans;
}
void _glfwPlatformGetMonitorWorkarea(_GLFWmonitor* monitor, int* xpos, int* ypos, int* width, int* height) {
MonitorGeometry ans = _glfwPlatformGetMonitorGeometry(monitor);
if (extents)
XFree(extents);
if (desktop)
XFree(desktop);
}
if (xpos)
*xpos = ans.workarea.x;
*xpos = areaX;
if (ypos)
*ypos = ans.workarea.y;
*ypos = areaY;
if (width)
*width = ans.workarea.width;
*width = areaWidth;
if (height)
*height = ans.workarea.height;
*height = areaHeight;
}
GLFWvidmode* _glfwPlatformGetVideoModes(_GLFWmonitor* monitor, int* count)

13
glfw/x11_platform.h vendored
View File

@@ -205,10 +205,6 @@ typedef struct _GLFWwindowX11
// The last position the cursor was warped to by GLFW
int warpCursorPosX, warpCursorPosY;
struct {
bool is_active;
GLFWLayerShellConfig config;
} layer_shell;
} _GLFWwindowX11;
typedef struct MimeAtom {
@@ -251,22 +247,19 @@ typedef struct _GLFWlibraryX11
Atom WM_STATE;
Atom WM_DELETE_WINDOW;
Atom NET_WM_NAME;
Atom NET_WM_ALLOWED_ACTIONS, NET_WM_ACTION_MOVE, NET_WM_ACTION_RESIZE, NET_WM_ACTION_MINIMIZE, NET_WM_ACTION_SHADE, NET_WM_ACTION_STICK, NET_WM_ACTION_MAXIMIZE_HORZ, NET_WM_ACTION_MAXIMIZE_VERT, NET_WM_ACTION_FULLSCREEN, NET_WM_ACTION_CHANGE_DESKTOP, NET_WM_ACTION_CLOSE, NET_WM_ACTION_ABOVE, NET_WM_ACTION_BELOW, NET_WM_ACTION_ABOVE_BELOW;
Atom NET_WM_ICON_NAME;
Atom NET_WM_ICON;
Atom NET_WM_PID;
Atom NET_WM_PING;
Atom NET_WM_WINDOW_TYPE, NET_WM_WINDOW_TYPE_NORMAL, NET_WM_WINDOW_TYPE_DOCK, NET_WM_WINDOW_TYPE_DESKTOP, NET_WM_WINDOW_TYPE_UTILITY, NET_WM_WINDOW_TYPE_SPLASH, NET_WM_WINDOW_TYPE_DIALOG, NET_WM_WINDOW_TYPE_MENU, NET_WM_WINDOW_TYPE_NOTIFICATION;
Atom NET_WM_WINDOW_TYPE;
Atom NET_WM_WINDOW_TYPE_NORMAL;
Atom NET_WM_WINDOW_TYPE_DOCK;
Atom NET_WM_STATE;
Atom NET_WM_STATE_ABOVE;
Atom NET_WM_STATE_BELOW;
Atom NET_WM_STATE_FULLSCREEN;
Atom NET_WM_STATE_MAXIMIZED_VERT;
Atom NET_WM_STATE_MAXIMIZED_HORZ;
Atom NET_WM_STATE_DEMANDS_ATTENTION;
Atom NET_WM_STATE_SKIP_TASKBAR;
Atom NET_WM_STATE_SKIP_PAGER;
Atom NET_WM_STATE_STICKY;
Atom NET_WM_BYPASS_COMPOSITOR;
Atom NET_WM_FULLSCREEN_MONITORS;
Atom NET_WM_WINDOW_OPACITY;

316
glfw/x11_window.c vendored
View File

@@ -530,204 +530,19 @@ static void enableCursor(_GLFWwindow* window)
updateCursorImage(window);
}
typedef unsigned long strut_type;
typedef struct WindowGeometry {
int x, y, width, height;
bool needs_strut;
strut_type struts[12];
} WindowGeometry;
#define config (window->x11.layer_shell.config)
static _GLFWmonitor*
find_monitor_by_name(const char* name) {
if (!name || !name[0]) return (_GLFWmonitor*)glfwGetPrimaryMonitor();;
for (int i = 0; i < _glfw.monitorCount; i++) {
_GLFWmonitor *m = _glfw.monitors[i];
if (strcmp(m->name, name) == 0) return m;
}
return (_GLFWmonitor*)glfwGetPrimaryMonitor();;
}
static WindowGeometry
calculate_layer_geometry(_GLFWwindow *window) {
_GLFWmonitor *monitor = find_monitor_by_name(config.output_name);
MonitorGeometry mg = _glfwPlatformGetMonitorGeometry((_GLFWmonitor*)glfwGetPrimaryMonitor());
WindowGeometry ans = {0};
debug_rendering("Monitor: %s full: %dx%d@%dx%d workarea: %dx%d@%dx%d\n", monitor->name,
mg.full.width, mg.full.height, mg.full.x, mg.full.y, mg.workarea.width, mg.workarea.height, mg.workarea.x, mg.workarea.y);
ans.width = mg.full.width; ans.height = mg.full.height;
ans.x = mg.full.x; ans.y = mg.full.y;
ans.needs_strut = config.type == GLFW_LAYER_SHELL_PANEL;
if (config.type == GLFW_LAYER_SHELL_BACKGROUND) {
ans.x += config.requested_left_margin; ans.y += config.requested_top_margin;
ans.width -= config.requested_left_margin + config.requested_right_margin;
ans.height -= config.requested_top_margin + config.requested_bottom_margin;
return ans;
}
float xscale = (float)config.expected.xscale, yscale = (float)config.expected.yscale;
_glfwPlatformGetWindowContentScale(window, &xscale, &yscale);
unsigned cell_width, cell_height; double left_edge_spacing, top_edge_spacing, right_edge_spacing, bottom_edge_spacing;
config.size_callback((GLFWwindow*)window, xscale, yscale, &cell_width, &cell_height, &left_edge_spacing, &top_edge_spacing, &right_edge_spacing, &bottom_edge_spacing);
double spacing_x = left_edge_spacing + right_edge_spacing;
double spacing_y = top_edge_spacing + bottom_edge_spacing;
double xsz = config.x_size_in_pixels ? (unsigned)(config.x_size_in_pixels * xscale) : (cell_width * config.x_size_in_cells);
double ysz = config.y_size_in_pixels ? (unsigned)(config.y_size_in_pixels * yscale) : (cell_height * config.y_size_in_cells);
ans.width = (int)(1. + spacing_x + xsz); ans.height = (int)(1. + spacing_y + ysz);
GeometryRect m = config.type == GLFW_LAYER_SHELL_TOP || config.type == GLFW_LAYER_SHELL_OVERLAY ? mg.workarea : mg.full;
static const struct {
unsigned left, right, top, bottom, left_start_y, left_end_y, right_start_y, right_end_y, top_start_x, top_end_x, bottom_start_x, bottom_end_x;
} s = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
switch (config.edge) {
case GLFW_EDGE_LEFT:
ans.x = m.x + config.requested_left_margin;
ans.y = m.y + config.requested_top_margin;
ans.height = m.height - config.requested_bottom_margin - config.requested_top_margin;
ans.struts[s.left] = ans.width; ans.struts[s.left_end_y] = ans.height;
break;
case GLFW_EDGE_RIGHT:
ans.x = m.x + m.width - config.requested_right_margin - ans.width;
ans.y = m.y + config.requested_top_margin;
ans.height = m.height - config.requested_bottom_margin - config.requested_top_margin;
ans.struts[s.right] = ans.width; ans.struts[s.right_end_y] = ans.height;
break;
case GLFW_EDGE_TOP:
ans.x = m.x + config.requested_left_margin;
ans.y = m.y + config.requested_top_margin;
ans.width = m.width - config.requested_right_margin - config.requested_left_margin;
ans.struts[s.top] = ans.height; ans.struts[s.top_end_x] = ans.width;
break;
case GLFW_EDGE_BOTTOM:
ans.x = m.x + config.requested_left_margin;
ans.y = m.height - config.requested_bottom_margin - ans.height;
ans.width = m.width - config.requested_right_margin - config.requested_left_margin;
ans.struts[s.bottom] = ans.height; ans.struts[s.bottom_end_x] = ans.width;
break;
case GLFW_EDGE_CENTER_SIZED:
ans.needs_strut = false;
ans.x = (m.width - ans.width) / 2;
ans.y = (m.height - ans.height) / 2;
break;
default:
ans.needs_strut = false;
ans.x = m.x + config.requested_left_margin;
ans.y = m.y + config.requested_top_margin;
ans.height = m.height - config.requested_bottom_margin - config.requested_top_margin;
ans.width = m.width - config.requested_right_margin - config.requested_left_margin;
break;
}
debug_rendering("Calculating layer geometry at scale: %f cell size: (%u, %u) -> %dx%d@%dx%d needs_strut: %d\n",
xscale, cell_width, cell_height, ans.width, ans.height, ans.x, ans.y, ans.needs_strut)
return ans;
}
GLFWAPI bool glfwIsLayerShellSupported(void) { return _glfw.x11.NET_WM_WINDOW_TYPE != 0 && _glfw.x11.NET_WM_STATE != 0; }
static bool
update_wm_hints(_GLFWwindow *window, const WindowGeometry *wg, const _GLFWwndconfig *wndconfig) {
XWMHints* hints = XAllocWMHints();
bool is_layer_shell = window->x11.layer_shell.is_active;
bool ok = false;
if (hints) {
ok = true;
hints->flags = StateHint | InputHint;
hints->initial_state = NormalState;
hints->input = true;
if (is_layer_shell && config.focus_policy == GLFW_FOCUS_NOT_ALLOWED) hints->input = false;
XSetWMHints(_glfw.x11.display, window->x11.handle, hints);
XFree(hints);
} else _glfwInputError(GLFW_OUT_OF_MEMORY, "X11: Failed to allocate WM hints");
if (_glfw.x11.NET_WM_WINDOW_TYPE) {
Atom type = 0;
if (is_layer_shell) {
const char *name = NULL;
#define S(which) type = _glfw.x11.which; name = #which
switch (config.type) {
case GLFW_LAYER_SHELL_BACKGROUND: S(NET_WM_WINDOW_TYPE_DESKTOP); break;
case GLFW_LAYER_SHELL_PANEL: S(NET_WM_WINDOW_TYPE_DOCK); break;
default: S(NET_WM_WINDOW_TYPE_NORMAL); break;
}
#undef S
if (!type) {
_glfwInputError(GLFW_PLATFORM_ERROR, "X11: Window manager does not support _%s", name);
ok = false;
}
} else if (_glfw.x11.NET_WM_WINDOW_TYPE_NORMAL) type = _glfw.x11.NET_WM_WINDOW_TYPE_NORMAL;
if (type) XChangeProperty(
_glfw.x11.display, window->x11.handle, _glfw.x11.NET_WM_WINDOW_TYPE, XA_ATOM, 32, PropModeReplace, (unsigned char*) &type, 1);
} else if (is_layer_shell) {
_glfwInputError(GLFW_PLATFORM_ERROR, "X11: Window manager does not support _NET_WM_WINDOW_TYPE");
ok = false;
}
if (is_layer_shell) {
if (_glfw.x11.NET_WM_STRUT_PARTIAL) {
XChangeProperty(
_glfw.x11.display, window->x11.handle, _glfw.x11.NET_WM_STRUT_PARTIAL, XA_CARDINAL, 32, PropModeReplace,
(unsigned char*)(wg->needs_strut ? wg->struts : (strut_type[12]){0}), 12);
} else if (wg->needs_strut) {
_glfwInputError(GLFW_PLATFORM_ERROR, "X11: Window manager does not support _NET_WM_STRUT_PARTIAL");
ok = false;
}
}
if (ok) {
updateNormalHints(window, wg->width, wg->height);
Atom states[8]; unsigned count = 0;
if (is_layer_shell) {
_glfwPlatformSetWindowDecorated(window, false);
if (_glfw.x11.NET_WM_STATE_STICKY) states[count++] = _glfw.x11.NET_WM_STATE_STICKY;
if (_glfw.x11.NET_WM_STATE_SKIP_PAGER) states[count++] = _glfw.x11.NET_WM_STATE_SKIP_PAGER;
if (_glfw.x11.NET_WM_STATE_SKIP_TASKBAR) states[count++] = _glfw.x11.NET_WM_STATE_SKIP_TASKBAR;
#define S(x) if (_glfw.x11.x) { states[count++] = _glfw.x11.x; } else { _glfwInputError(GLFW_PLATFORM_ERROR, "X11: Window manager does not support _%s", #x); ok = false; }
switch (config.type) {
case GLFW_LAYER_SHELL_NONE: break;
case GLFW_LAYER_SHELL_BACKGROUND: S(NET_WM_STATE_BELOW); break;
case GLFW_LAYER_SHELL_PANEL:
// i3 does not support NET_WM_STATE_BELOW but panels work without it
if (_glfw.x11.NET_WM_STATE_BELOW) { S(NET_WM_STATE_BELOW); }
break;
case GLFW_LAYER_SHELL_TOP: case GLFW_LAYER_SHELL_OVERLAY: S(NET_WM_STATE_ABOVE); break;
}
#undef S
} else if (wndconfig) {
if (!wndconfig->decorated) _glfwPlatformSetWindowDecorated(window, false);
if (_glfw.x11.NET_WM_STATE && !window->monitor) {
if (wndconfig->floating) {
if (_glfw.x11.NET_WM_STATE_ABOVE) states[count++] = _glfw.x11.NET_WM_STATE_ABOVE;
}
if (wndconfig->maximized) {
if (_glfw.x11.NET_WM_STATE_MAXIMIZED_VERT && _glfw.x11.NET_WM_STATE_MAXIMIZED_HORZ) {
states[count++] = _glfw.x11.NET_WM_STATE_MAXIMIZED_VERT;
states[count++] = _glfw.x11.NET_WM_STATE_MAXIMIZED_HORZ;
window->x11.maximized = true;
}
}
}
}
if (count && _glfw.x11.NET_WM_STATE) XChangeProperty(_glfw.x11.display, window->x11.handle, _glfw.x11.NET_WM_STATE,
XA_ATOM, 32, PropModeReplace, (unsigned char*) states, count);
}
if (!wndconfig && ok) {
_glfwPlatformSetWindowPos(window, wg->x, wg->y);
_glfwPlatformSetWindowSize(window, wg->width, wg->height);
}
return ok;
#undef config
}
// Create the X11 window (and its colormap)
//
static bool createNativeWindow(_GLFWwindow* window,
const _GLFWwndconfig* wndconfig,
Visual* visual, int depth)
{
WindowGeometry wg = {.width=wndconfig->width, .height=wndconfig->height};
if (window->x11.layer_shell.is_active) {
wg = calculate_layer_geometry(window);
window->resizable = false;
int width = wndconfig->width;
int height = wndconfig->height;
if (wndconfig->scaleToMonitor)
{
width *= (int)_glfw.x11.contentScaleX;
height *= (int)_glfw.x11.contentScaleY;
}
// Create a colormap based on the visual used by the current context
@@ -748,11 +563,10 @@ static bool createNativeWindow(_GLFWwindow* window,
_glfwGrabErrorHandlerX11();
window->x11.parent = _glfw.x11.root;
debug_rendering("Creating window with geometry: %dx%d@%dx%d\n", wg.width, wg.height, wg.x, wg.y);
window->x11.handle = XCreateWindow(_glfw.x11.display,
_glfw.x11.root,
wg.x, wg.y, // Position
wg.width, wg.height,
0, 0, // Position
width, height,
0, // Border width
depth, // Color depth
InputOutput,
@@ -774,6 +588,39 @@ static bool createNativeWindow(_GLFWwindow* window,
_glfw.x11.context,
(XPointer) window);
if (!wndconfig->decorated)
_glfwPlatformSetWindowDecorated(window, false);
if (_glfw.x11.NET_WM_STATE && !window->monitor)
{
Atom states[3];
int count = 0;
if (wndconfig->floating)
{
if (_glfw.x11.NET_WM_STATE_ABOVE)
states[count++] = _glfw.x11.NET_WM_STATE_ABOVE;
}
if (wndconfig->maximized)
{
if (_glfw.x11.NET_WM_STATE_MAXIMIZED_VERT &&
_glfw.x11.NET_WM_STATE_MAXIMIZED_HORZ)
{
states[count++] = _glfw.x11.NET_WM_STATE_MAXIMIZED_VERT;
states[count++] = _glfw.x11.NET_WM_STATE_MAXIMIZED_HORZ;
window->x11.maximized = true;
}
}
if (count)
{
XChangeProperty(_glfw.x11.display, window->x11.handle,
_glfw.x11.NET_WM_STATE, XA_ATOM, 32,
PropModeReplace, (unsigned char*) states, count);
}
}
// Declare the WM protocols supported by GLFW
{
Atom protocols[] =
@@ -796,9 +643,32 @@ static bool createNativeWindow(_GLFWwindow* window,
(unsigned char*) &pid, 1);
}
if (!update_wm_hints(window, &wg, wndconfig)) return false;
// without this floating window position is incorrect on KDE
if (window->x11.layer_shell.is_active) _glfwPlatformSetWindowPos(window, wg.x, wg.y);
if (_glfw.x11.NET_WM_WINDOW_TYPE && _glfw.x11.NET_WM_WINDOW_TYPE_NORMAL)
{
Atom type = _glfw.x11.NET_WM_WINDOW_TYPE_NORMAL;
XChangeProperty(_glfw.x11.display, window->x11.handle,
_glfw.x11.NET_WM_WINDOW_TYPE, XA_ATOM, 32,
PropModeReplace, (unsigned char*) &type, 1);
}
// Set ICCCM WM_HINTS property
{
XWMHints* hints = XAllocWMHints();
if (!hints)
{
_glfwInputError(GLFW_OUT_OF_MEMORY,
"X11: Failed to allocate WM hints");
return false;
}
hints->flags = StateHint;
hints->initial_state = NormalState;
XSetWMHints(_glfw.x11.display, window->x11.handle, hints);
XFree(hints);
}
updateNormalHints(window, width, height);
// Set ICCCM WM_CLASS property
{
@@ -1561,7 +1431,6 @@ static void processEvent(XEvent *event)
if (event->xconfigure.width != window->x11.width ||
event->xconfigure.height != window->x11.height)
{
debug_rendering("Window resized to: %d %d from: %d %d\n", event->xconfigure.width, event->xconfigure.height, window->x11.width, window->x11.height);
_glfwInputFramebufferSize(window,
event->xconfigure.width,
event->xconfigure.height);
@@ -1596,9 +1465,9 @@ static void processEvent(XEvent *event)
return;
}
}
if (xpos != window->x11.xpos || ypos != window->x11.ypos)
{
debug_rendering("Window moved to: %d %d from: %d %d\n", xpos, ypos, window->x11.xpos, window->x11.xpos);
_glfwInputWindowPos(window, xpos, ypos);
window->x11.xpos = xpos;
window->x11.ypos = ypos;
@@ -2000,12 +1869,9 @@ void _glfwPushSelectionToManagerX11(void)
int _glfwPlatformCreateWindow(_GLFWwindow* window, const _GLFWwndconfig* wndconfig, const _GLFWctxconfig* ctxconfig, const _GLFWfbconfig* fbconfig, const GLFWLayerShellConfig *lsc)
{
(void)lsc;
Visual* visual = NULL;
int depth;
if (lsc) {
window->x11.layer_shell.is_active = true;
window->x11.layer_shell.config = *lsc;
} else window->x11.layer_shell.is_active = false;
if (ctxconfig->client != GLFW_NO_API)
{
@@ -2097,16 +1963,8 @@ void _glfwPlatformDestroyWindow(_GLFWwindow* window)
XFlush(_glfw.x11.display);
}
const GLFWLayerShellConfig*
_glfwPlatformGetLayerShellConfig(_GLFWwindow *window) {
return &window->x11.layer_shell.config;
}
bool
_glfwPlatformSetLayerShellConfig(_GLFWwindow* window, const GLFWLayerShellConfig *value) {
if (value) window->x11.layer_shell.config = *value;
WindowGeometry wg = calculate_layer_geometry(window);
update_wm_hints(window, &wg, NULL);
bool _glfwPlatformSetLayerShellConfig(_GLFWwindow* window, const GLFWLayerShellConfig *value) {
(void)window; (void)value;
return false;
}
@@ -2463,11 +2321,6 @@ void _glfwPlatformShowWindow(_GLFWwindow* window)
return;
XMapWindow(_glfw.x11.display, window->x11.handle);
// without this floating window position is incorrect on KDE
if (window->x11.layer_shell.is_active) {
WindowGeometry wg = calculate_layer_geometry(window);
_glfwPlatformSetWindowPos(window, wg.x, wg.y);
}
waitForVisibilityNotify(window);
}
@@ -3384,17 +3237,6 @@ _glfwPlatformSetWindowBlur(_GLFWwindow *window, int blur_radius) {
}
bool
_glfwPlatformGrabKeyboard(bool grab) {
int result;
if (grab) {
result = XGrabKeyboard(_glfw.x11.display, _glfw.x11.root, True, GrabModeAsync, GrabModeAsync, CurrentTime);
} else {
result = XUngrabKeyboard(_glfw.x11.display, CurrentTime);
}
return result == GrabSuccess;
}
//////////////////////////////////////////////////////////////////////////
////// GLFW native API //////
//////////////////////////////////////////////////////////////////////////
@@ -3431,4 +3273,18 @@ GLFWAPI int glfwSetX11LaunchCommand(GLFWwindow *handle, char **argv, int argc)
return XSetCommand(_glfw.x11.display, window->x11.handle, argv, argc);
}
GLFWAPI void glfwSetX11WindowAsDock(int32_t x11_window_id) {
_GLFW_REQUIRE_INIT();
Atom type = _glfw.x11.NET_WM_WINDOW_TYPE_DOCK;
XChangeProperty(_glfw.x11.display, x11_window_id,
_glfw.x11.NET_WM_WINDOW_TYPE, XA_ATOM, 32,
PropModeReplace, (unsigned char*) &type, 1);
}
GLFWAPI void glfwSetX11WindowStrut(int32_t x11_window_id, uint32_t dimensions[12]) {
_GLFW_REQUIRE_INIT();
XChangeProperty(_glfw.x11.display, x11_window_id,
_glfw.x11.NET_WM_STRUT_PARTIAL, XA_CARDINAL, 32,
PropModeReplace, (unsigned char*) dimensions, 12);
}

15
go.mod
View File

@@ -1,4 +1,4 @@
module github.com/kovidgoyal/kitty
module kitty
go 1.23.0
@@ -6,27 +6,22 @@ toolchain go1.24.1
require (
github.com/ALTree/bigfloat v0.2.0
github.com/alecthomas/chroma/v2 v2.19.0
github.com/bmatcuk/doublestar/v4 v4.9.0
github.com/alecthomas/chroma/v2 v2.17.0
github.com/bmatcuk/doublestar/v4 v4.8.1
github.com/dlclark/regexp2 v1.11.5
github.com/edwvee/exiffix v0.0.0-20240229113213-0dbb146775be
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1
github.com/kovidgoyal/imaging v1.6.4
github.com/seancfoley/ipaddress-go v1.7.1
github.com/shirou/gopsutil/v3 v3.24.5
github.com/zeebo/xxh3 v1.0.2
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b
golang.org/x/image v0.29.0
golang.org/x/sys v0.34.0
golang.org/x/text v0.27.0
golang.org/x/image v0.26.0
golang.org/x/sys v0.32.0
howett.net/plist v1.0.1
)
// Uncomment the following to use a local checkout of dbus
// replace github.com/kovidgoyal/dbus => ../dbus
require (
github.com/disintegration/imaging v1.6.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect

20
go.sum
View File

@@ -2,12 +2,12 @@ github.com/ALTree/bigfloat v0.2.0 h1:AwNzawrpFuw55/YDVlcPw0F0cmmXrmngBHhVrvdXPvM
github.com/ALTree/bigfloat v0.2.0/go.mod h1:+NaH2gLeY6RPBPPQf4aRotPPStg+eXc8f9ZaE4vRfD4=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.19.0 h1:Im+SLRgT8maArxv81mULDWN8oKxkzboH07CHesxElq4=
github.com/alecthomas/chroma/v2 v2.19.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
github.com/alecthomas/chroma/v2 v2.17.0 h1:3r2Cgk+nXNICMBxIFGnTRTbQFUwMiLisW+9uos0TtUI=
github.com/alecthomas/chroma/v2 v2.17.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/bmatcuk/doublestar/v4 v4.9.0 h1:DBvuZxjdKkRP/dr4GVV4w2fnmrk5Hxc90T51LZjv0JA=
github.com/bmatcuk/doublestar/v4 v4.9.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
@@ -28,8 +28,6 @@ github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSo
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1 h1:rMY/hWfcVzBm6BLX6YLA+gLJEpuXBed/VP6YEkXt8R4=
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1/go.mod h1:RbNG3Q1g6GUy1/WzWVx+S24m7VKyvl57vV+cr2hpt50=
github.com/kovidgoyal/imaging v1.6.4 h1:K0idhRPXnRrJBKnBYcTfI1HTWSNDeAn7hYDvf9I0dCk=
github.com/kovidgoyal/imaging v1.6.4/go.mod h1:bEIgsaZmXlvFfkv/CUxr9rJook6AQkJnpB5EPosRfRY=
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik=
@@ -65,18 +63,16 @@ github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaD
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b h1:r+vk0EmXNmekl0S0BascoeeoHk/L7wmaW2QF90K+kYI=
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas=
golang.org/x/image v0.29.0/go.mod h1:RVJROnf3SLK8d26OW91j4FrIHGbsJ8QnbEocVTOWQDA=
golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -4,13 +4,13 @@ package ask
import (
"fmt"
"github.com/kovidgoyal/kitty/tools/cli/markup"
"github.com/kovidgoyal/kitty/tools/tty"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/style"
"github.com/kovidgoyal/kitty/tools/wcswidth"
"io"
"kitty/tools/cli/markup"
"kitty/tools/tty"
"kitty/tools/tui/loop"
"kitty/tools/utils"
"kitty/tools/utils/style"
"kitty/tools/wcswidth"
"os"
"regexp"
"strings"

View File

@@ -9,9 +9,9 @@ import (
"path/filepath"
"time"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/tui/readline"
"github.com/kovidgoyal/kitty/tools/utils"
"kitty/tools/tui/loop"
"kitty/tools/tui/readline"
"kitty/tools/utils"
)
var _ = fmt.Print

View File

@@ -6,9 +6,9 @@ import (
"errors"
"fmt"
"github.com/kovidgoyal/kitty/tools/cli"
"github.com/kovidgoyal/kitty/tools/cli/markup"
"github.com/kovidgoyal/kitty/tools/tui"
"kitty/tools/cli"
"kitty/tools/cli/markup"
"kitty/tools/tui"
)
var _ = fmt.Print

View File

@@ -1,402 +0,0 @@
package choose_files
import (
"fmt"
"io/fs"
"slices"
"sync"
"github.com/kovidgoyal/kitty/tools/utils"
)
var _ = fmt.Print
type CollectionIndex struct {
Slice, Pos int
}
func (c CollectionIndex) Compare(o CollectionIndex) int {
if c.Slice == o.Slice {
return c.Pos - o.Pos
}
return c.Slice - o.Slice
}
func (c CollectionIndex) Less(o CollectionIndex) bool {
return c.Slice < o.Slice || (c.Slice == o.Slice && c.Pos < o.Pos)
}
func (c *CollectionIndex) NextSlice() {
c.Slice++
c.Pos = 0
}
type ResultCollection struct {
slices [][]ResultItem
append_idx CollectionIndex
batch_size int
}
func NewResultCollection(batch_size int) (ans *ResultCollection) {
batch_size = max(1, batch_size)
return &ResultCollection{
batch_size: batch_size,
slices: [][]ResultItem{make([]ResultItem, batch_size)},
}
}
func (c *ResultCollection) Len() int {
return c.batch_size*(len(c.slices)-1) + c.append_idx.Pos
}
func (c *ResultCollection) NextAppendPointer() (ans *ResultItem) {
s := c.slices[c.append_idx.Slice]
ans = &s[c.append_idx.Pos]
if c.append_idx.Pos+1 < len(s) {
c.append_idx.Pos++
} else if c.append_idx.Slice+1 < len(c.slices) {
c.append_idx.NextSlice()
} else {
c.slices = append(c.slices, make([]ResultItem, 4096))
c.append_idx.NextSlice()
}
return
}
func (c *ResultCollection) Batch(offset *CollectionIndex) (ans []ResultItem) {
if offset.Slice == c.append_idx.Slice {
if offset.Pos < c.append_idx.Pos {
ans = c.slices[offset.Slice][offset.Pos:c.append_idx.Pos]
offset.Pos = c.append_idx.Pos
}
} else if offset.Slice < c.append_idx.Slice {
ans = c.slices[offset.Slice][offset.Pos:]
offset.NextSlice()
}
return
}
func (c *ResultCollection) NextDir(offset *CollectionIndex) (ans string, ignore_files []ignore_file_with_prefix) {
for ans == "" && offset.Compare(c.append_idx) < 0 {
if c.slices[offset.Slice][offset.Pos].ftype&fs.ModeDir != 0 {
ans = c.slices[offset.Slice][offset.Pos].text
ignore_files = c.slices[offset.Slice][offset.Pos].ignore_files
}
offset.Pos++
if offset.Pos >= len(c.slices[offset.Slice]) {
offset.NextSlice()
}
}
return
}
type SortedResults struct {
slices [][]*ResultItem
mutex sync.Mutex
len int
}
func NewSortedResults() *SortedResults { return &SortedResults{} }
func (s *SortedResults) lock() { s.mutex.Lock() }
func (s *SortedResults) unlock() { s.mutex.Unlock() }
func (s *SortedResults) Len() int {
s.lock()
defer s.unlock()
return s.len
}
func (s *SortedResults) Clear() {
s.lock()
defer s.unlock()
s.slices = nil
s.len = 0
}
func (s *SortedResults) At(pos CollectionIndex) (ans *ResultItem) {
s.lock()
defer s.unlock()
if pos.Slice < len(s.slices) {
s := s.slices[pos.Slice]
if pos.Pos < len(s) {
ans = s[pos.Pos]
}
}
return
}
func (s *SortedResults) RenderedMatches(pos CollectionIndex, max_num int) (ans []*ResultItem) {
s.lock()
defer s.unlock()
if pos.Slice >= len(s.slices) {
return
}
if max_num < 0 {
max_num = s.len
}
ans = make([]*ResultItem, 0, max_num)
for ; pos.Slice < len(s.slices) && max_num > 0; pos.NextSlice() {
sl := s.slices[pos.Slice]
if pos.Pos >= len(sl) {
continue
}
sl = sl[pos.Pos:min(len(sl), pos.Pos+max_num)]
ans = append(ans, sl...)
max_num -= len(sl)
}
return
}
func (s *SortedResults) merge_slice(idx int, sl []*ResultItem) {
sz := len(s.slices[idx])
maxs := sl[len(sl)-1].score
limit := idx + 1
for limit < len(s.slices) {
q := s.slices[limit]
if q[0].score > maxs {
break
}
sz += len(q)
limit++
}
ans := make([]*ResultItem, 0, sz)
a := 0
b := CollectionIndex{Slice: idx}
ss := s.slices[b.Slice]
for a < len(sl) {
if sl[a].score <= ss[b.Pos].score {
ans = append(ans, sl[a])
a++
} else {
ans = append(ans, ss[b.Pos])
b.Pos++
if b.Pos >= len(ss) {
b.NextSlice()
if b.Slice >= limit {
break
}
ss = s.slices[b.Slice]
}
}
}
ans = append(ans, sl[a:]...)
for ; b.Slice < limit; b.NextSlice() {
ans = append(ans, s.slices[b.Slice][b.Pos:]...)
}
s.slices = slices.Replace(s.slices, idx, limit, ans)
}
func (s *SortedResults) AddSortedSlice(sl []*ResultItem) {
if len(sl) == 0 {
return
}
s.lock()
defer s.unlock()
s.len += len(sl)
if len(s.slices) == 0 {
s.slices = append(s.slices, sl)
return
}
sl_min, sl_max := sl[0].score, sl[len(sl)-1].score
for i, q := range s.slices {
switch {
case sl_max <= q[0].score:
s.slices = slices.Insert(s.slices, i, sl)
return
case sl_min >= q[len(q)-1].score:
continue
default:
s.merge_slice(i, sl)
return
}
}
s.slices = append(s.slices, sl)
}
func (s *SortedResults) Apply(first, last CollectionIndex, action func(*ResultItem) (keep_going bool)) {
s.lock()
defer s.unlock()
if first.Slice >= len(s.slices) || first.Pos >= len(s.slices[first.Slice]) {
return
}
amt := utils.IfElse(first.Less(last), 1, -1)
var did_wrap bool
for {
if !action(s.slices[first.Slice][first.Pos]) {
break
}
if first.Compare(last) == 0 {
break
}
first, did_wrap = s.increment_with_wrap_around(first, amt)
if did_wrap {
break
}
}
}
func (s *SortedResults) Closest(idx CollectionIndex, matches func(*ResultItem) bool) *CollectionIndex {
s.lock()
defer s.unlock()
if idx.Slice >= len(s.slices) || idx.Pos >= len(s.slices[idx.Slice]) {
return nil
}
type result struct {
idx CollectionIndex
count int
}
var a, b result
iterate := func(idx CollectionIndex, amt int, result *result) {
var did_wrap bool
var count int
result.count = -1
for {
idx, did_wrap = s.increment_with_wrap_around(idx, amt)
if did_wrap {
break
}
count++
if matches(s.slices[idx.Slice][idx.Pos]) {
result.idx = idx
result.count = count
break
}
}
}
go func() { iterate(idx, 1, &a) }()
go func() { iterate(idx, -1, &a) }()
if a.count < 0 && b.count < 0 {
return nil
}
return utils.IfElse(a.count < b.count, &b.idx, &a.idx)
}
func (s *SortedResults) IncrementIndexWithWrapAroundAndCheck(idx CollectionIndex, amt int) (ans CollectionIndex, did_wrap bool) {
s.lock()
defer s.unlock()
return s.increment_with_wrap_around(idx, amt)
}
func (s *SortedResults) IncrementIndexWithWrapAround(idx CollectionIndex, amt int) CollectionIndex {
s.lock()
defer s.unlock()
ans, _ := s.increment_with_wrap_around(idx, amt)
return ans
}
func (s *SortedResults) increment_with_wrap_around(idx CollectionIndex, amt int) (CollectionIndex, bool) {
did_wrap := false
if amt > 0 {
for amt > 0 {
if delta := min(amt, len(s.slices[idx.Slice])-1-idx.Pos); delta > 0 {
idx.Pos += delta
amt -= delta
} else {
idx.NextSlice()
if idx.Slice >= len(s.slices) {
idx = CollectionIndex{} // wraparound
did_wrap = true
}
amt--
}
}
} else {
// we use separate code for negative increment instead of doing
// increment = len - increment as it is faster in the common case of
// increment much smaller than len
amt *= -1
for amt > 0 {
if idx.Pos > 0 {
delta := min(amt, idx.Pos)
amt -= delta
idx.Pos -= delta
} else {
if idx.Slice == 0 {
idx = CollectionIndex{Slice: len(s.slices) - 1, Pos: len(s.slices[len(s.slices)-1]) - 1}
did_wrap = true
} else {
idx.Slice--
idx.Pos = len(s.slices[idx.Slice]) - 1
}
amt--
}
}
}
return idx, did_wrap
}
// Return a - b
func (s *SortedResults) SignedDistance(a, b CollectionIndex) (ans int) {
s.lock()
defer s.unlock()
return s.signed_distance(a, b)
}
// Return a - b
func (s *SortedResults) signed_distance(a, b CollectionIndex) (ans int) {
mult := -1
if b.Less(a) {
a, b = b, a
mult = 1
}
limit := min(b.Slice, len(s.slices))
for ; a.Slice < limit; a.NextSlice() {
ans += len(s.slices[a.Slice]) - a.Pos
}
return mult * (ans + (b.Pos - a.Pos))
}
// Return |a - b|
func (s *SortedResults) distance(a, b CollectionIndex) (ans int) {
if b.Less(a) {
a, b = b, a
}
limit := min(b.Slice, len(s.slices))
for ; a.Slice < limit; a.NextSlice() {
ans += len(s.slices[a.Slice]) - a.Pos
}
return ans + (b.Pos - a.Pos)
}
func (s *SortedResults) SplitIntoColumns(calc_num_cols func(int) int, num_per_column, num_before_current int, current CollectionIndex) (ans [][]*ResultItem, num_before int, first_idx CollectionIndex) {
s.lock()
defer s.unlock()
num_cols := calc_num_cols(s.len)
total := num_cols * num_per_column
if total < 1 {
return nil, 0, CollectionIndex{}
}
num_before = min(total-1, num_before_current)
idx, did_wrap := s.increment_with_wrap_around(current, -num_before)
last_slice := s.slices[len(s.slices)-1]
last := CollectionIndex{Slice: len(s.slices) - 1, Pos: len(last_slice) - 1}
if did_wrap {
idx = CollectionIndex{}
} else if s.distance(idx, last) < total-1 {
if idx, did_wrap = s.increment_with_wrap_around(last, 1-total); did_wrap {
idx = CollectionIndex{}
}
}
first_idx = idx
num_before = s.distance(idx, current)
// fmt.Printf("111111 idx: %v current: %v num_before: %d\n", idx, current, num_before)
ans = make([][]*ResultItem, num_cols)
for colidx := range len(ans) {
col := make([]*ResultItem, 0, num_per_column)
for len(col) < num_per_column && idx.Slice < len(s.slices) {
ss := s.slices[idx.Slice]
limit := min(len(ss), idx.Pos+num_per_column-len(col))
col = append(col, ss[idx.Pos:limit]...)
idx.Pos = limit
if idx.Pos >= len(ss) {
idx.NextSlice()
if idx.Slice >= len(s.slices) {
break
}
}
}
ans[colidx] = col
}
return
}

View File

@@ -1,78 +0,0 @@
package choose_files
import (
"fmt"
"path/filepath"
"strings"
"github.com/kovidgoyal/kitty/tools/utils"
)
var _ = fmt.Print
type Filter struct {
Name, Type, Pattern string
Match func(filename string) bool
}
func (f Filter) String() string {
return fmt.Sprintf("%s:%s:%s", f.Type, f.Pattern, f.Name)
}
func (f Filter) Equal(other Filter) bool {
return f.Type == other.Type && f.Pattern == other.Pattern
}
func NewFilter(spec string) (*Filter, error) {
parts := strings.SplitN(spec, ":", 3)
if len(parts) != 3 {
return nil, fmt.Errorf("%#v is not a valid filter specifier, must have at least two colons", spec)
}
ans := &Filter{Name: parts[2], Pattern: parts[1], Type: parts[0]}
if _, err := filepath.Match(ans.Pattern, "test"); err != nil {
return nil, fmt.Errorf("%#v is not a valid glob pattern with error: %w", ans.Pattern, err)
}
if ans.Pattern != "*" && ans.Pattern != "" {
switch ans.Type {
case "glob":
ans.Match = func(filename string) bool {
m, _ := filepath.Match(ans.Pattern, filename)
return m
}
case "mime":
ans.Match = func(filename string) bool {
mime := utils.GuessMimeType(filename)
if mime == "" {
return false
}
m, _ := filepath.Match(ans.Pattern, mime)
return m
}
default:
return nil, fmt.Errorf("%#v is not a valid filter type", ans.Type)
}
}
return ans, nil
}
func CombinedFilter(filters ...Filter) Filter {
if len(filters) == 0 {
return Filter{}
}
for _, f := range filters {
if f.Match == nil {
return f
}
}
ans := filters[0]
matchers := utils.Map(func(f Filter) func(filename string) bool { return f.Match }, filters)
ans.Match = func(filename string) bool {
for _, m := range matchers {
if m(filename) {
return true
}
}
return false
}
return ans
}

View File

@@ -1,115 +0,0 @@
package choose_files
import (
"fmt"
"path/filepath"
"strings"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/style"
"github.com/kovidgoyal/kitty/tools/wcswidth"
)
var _ = fmt.Print
const HOVER_STYLE = "default fg=red"
type single_line_region struct {
x, width, y int
id string
callback func(string) error
}
func (h *Handler) draw_footer() (num_lines int, err error) {
lines := []string{}
screen_width := h.screen_size.width
sctx := style.Context{AllowEscapeCodes: true}
if h.state.screen == SAVE_FILE {
m := h.state.filter_map
h.state.filter_map = nil
defer func() { h.state.filter_map = m }()
}
if len(h.state.filter_map)+len(h.state.selections) > 0 {
buf := strings.Builder{}
pos := 0
current_style := sctx.SprintFunc("italic fg=green intense")
non_current_style := sctx.SprintFunc("dim")
var crs []single_line_region
w := func(presep, text string, sfunc func(...any) string, id string, cb func(string)) {
sz := len(presep)
if sz+pos >= screen_width {
lines = append(lines, buf.String())
pos = 0
buf.Reset()
} else {
buf.WriteString(presep)
pos += sz
}
sz = wcswidth.Stringwidth(text)
if sz+pos >= screen_width {
lines = append(lines, buf.String())
pos = 0
buf.Reset()
}
if sfunc != nil {
text = sfunc(text)
}
buf.WriteString(text)
if cb != nil {
crs = append(crs, single_line_region{x: pos, width: sz - 1, y: len(lines), id: id, callback: func(filter string) error {
cb(filter)
h.state.redraw_needed = true
return nil
}})
}
pos += sz
}
flush := func() {
if s := buf.String(); s != "" {
lines = append(lines, s)
}
pos = 0
buf.Reset()
}
if len(h.state.filter_map) > 0 {
w("", "󰈲 Filter:", nil, "", nil)
for _, name := range h.state.filter_names {
var cb func(string)
if name != h.state.current_filter {
cb = func(filter string) { h.set_filter(filter) }
}
w(" ", name, utils.IfElse(name == h.state.current_filter, current_style, non_current_style), name, cb)
}
flush()
}
if len(h.state.selections) > 0 {
before := len(lines)
w("", " Selected:", nil, "", nil)
for i, s := range h.state.selections {
text := s
if rel, rerr := filepath.Rel(h.state.CurrentDir(), s); rerr == nil {
text = rel
}
w(" ", text, nil, s, func(abspath string) { h.state.ToggleSelection(abspath) })
if len(lines)-before > 2 && len(h.state.selections)-i-1 > 3 {
w(" ", fmt.Sprintf("and %d more…", len(h.state.selections)-1-i), nil, "", nil)
break
}
}
flush()
}
offset := h.screen_size.height - len(lines)
for _, cr := range crs {
h.state.mouse_state.AddCellRegion(cr.id, cr.x, cr.y+offset, cr.x+cr.width, cr.y+offset, cr.callback).HoverStyle = HOVER_STYLE
}
}
if len(lines) > 0 {
h.lp.MoveCursorTo(1, h.screen_size.height-len(lines)+1)
if h.state.screen == SAVE_FILE {
h.lp.ClearToEndOfScreen()
}
h.lp.QueueWriteString(strings.Join(lines, "\r\n"))
}
return len(lines), err
}

View File

@@ -1,814 +0,0 @@
package choose_files
import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"sync"
"github.com/kovidgoyal/kitty/tools/cli"
"github.com/kovidgoyal/kitty/tools/config"
"github.com/kovidgoyal/kitty/tools/ignorefiles"
"github.com/kovidgoyal/kitty/tools/tty"
"github.com/kovidgoyal/kitty/tools/tui"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/tui/readline"
"github.com/kovidgoyal/kitty/tools/utils"
"golang.org/x/text/message"
)
var _ = fmt.Print
var debugprintln = tty.DebugPrintln
type Screen int
const (
NORMAL Screen = iota
SAVE_FILE
)
type Mode int
const (
SELECT_SINGLE_FILE Mode = iota
SELECT_MULTIPLE_FILES
SELECT_SAVE_FILE
SELECT_SAVE_FILES
SELECT_SAVE_DIR
SELECT_SINGLE_DIR
SELECT_MULTIPLE_DIRS
)
func (m Mode) CanSelectNonExistent() bool {
switch m {
case SELECT_SAVE_FILE, SELECT_SAVE_DIR, SELECT_SAVE_FILES:
return true
}
return false
}
func (m Mode) AllowsMultipleSelection() bool {
switch m {
case SELECT_MULTIPLE_FILES, SELECT_MULTIPLE_DIRS, SELECT_SAVE_FILES:
return true
}
return false
}
func (m Mode) OnlyDirs() bool {
switch m {
case SELECT_SINGLE_DIR, SELECT_MULTIPLE_DIRS, SELECT_SAVE_DIR:
return true
}
return false
}
func (m Mode) SelectFiles() bool {
switch m {
case SELECT_SINGLE_FILE, SELECT_MULTIPLE_FILES, SELECT_SAVE_FILE, SELECT_SAVE_FILES:
return true
}
return false
}
func (m Mode) WindowTitle() string {
switch m {
case SELECT_SINGLE_FILE:
return "Choose an existing file"
case SELECT_MULTIPLE_FILES:
return "Choose one or more existing files"
case SELECT_SAVE_FILE:
return "Choose a file to save"
case SELECT_SAVE_DIR:
return "Choose a directory to save"
case SELECT_SINGLE_DIR:
return "Choose an existing directory"
case SELECT_MULTIPLE_DIRS:
return "Choose one or more directories"
case SELECT_SAVE_FILES:
return "Choose files to save"
}
return ""
}
type render_state struct {
num_matches, num_of_slots, num_before, num_per_column, num_columns, num_shown int
first_idx CollectionIndex
}
type State struct {
base_dir string
current_dir string
select_dirs bool
multiselect bool
search_text string
mode Mode
suggested_save_file_name string
suggested_save_file_path string
window_title string
screen Screen
current_filter string
filter_map map[string]Filter
filter_names []string
show_hidden bool
respect_ignores bool
sort_by_last_modified bool
global_ignores ignorefiles.IgnoreFile
keyboard_shortcuts []*config.KeyAction
selections []string
current_idx CollectionIndex
last_render render_state
mouse_state tui.MouseState
redraw_needed bool
}
func (s State) ShowHidden() bool { return s.show_hidden }
func (s State) RespectIgnores() bool { return s.respect_ignores }
func (s State) SortByLastModified() bool { return s.sort_by_last_modified }
func (s State) GlobalIgnores() ignorefiles.IgnoreFile { return s.global_ignores }
func (s State) BaseDir() string { return utils.IfElse(s.base_dir == "", default_cwd, s.base_dir) }
func (s State) Filter() Filter { return s.filter_map[s.current_filter] }
func (s State) SelectDirs() bool { return s.select_dirs }
func (s State) Multiselect() bool { return s.multiselect }
func (s State) String() string { return utils.Repr(s) }
func (s State) SearchText() string { return s.search_text }
func (s State) OnlyDirs() bool { return s.mode.OnlyDirs() }
func (s *State) SetSearchText(val string) {
if s.search_text != val {
s.search_text = val
s.current_idx = CollectionIndex{}
}
}
func (s *State) SetCurrentDir(val string) {
if q, err := filepath.Abs(val); err == nil {
val = q
}
if s.CurrentDir() != val {
s.search_text = ""
s.current_idx = CollectionIndex{}
s.current_dir = val
}
}
func (s State) CurrentIndex() CollectionIndex { return s.current_idx }
func (s *State) SetCurrentIndex(val CollectionIndex) { s.current_idx = val }
func (s State) CurrentDir() string {
return utils.IfElse(s.current_dir == "", s.BaseDir(), s.current_dir)
}
func (s State) WindowTitle() string {
if s.window_title == "" {
return s.mode.WindowTitle()
}
return s.window_title
}
func (s *State) AddSelection(abspath string) bool {
if !slices.Contains(s.selections, abspath) {
s.selections = append(s.selections, abspath)
return true
}
return false
}
func (s *State) ToggleSelection(abspath string) (added bool) {
before := len(s.selections)
s.selections = slices.DeleteFunc(s.selections, func(x string) bool { return x == abspath })
if len(s.selections) == before {
s.selections = append(s.selections, abspath)
added = true
}
return
}
func (s *State) IsSelected(x *ResultItem) bool {
if len(s.selections) == 0 {
return false
}
q := filepath.Join(s.CurrentDir(), x.text)
return slices.Contains(s.selections, q)
}
type ScreenSize struct {
width, height, cell_width, cell_height, width_px, height_px int
}
type Handler struct {
state State
screen_size ScreenSize
result_manager *ResultManager
lp *loop.Loop
rl *readline.Readline
err_chan chan error
shortcut_tracker config.ShortcutTracker
msg_printer *message.Printer
spinner *tui.Spinner
}
func (h *Handler) draw_screen() (err error) {
h.state.redraw_needed = false
h.lp.StartAtomicUpdate()
defer func() {
h.state.mouse_state.UpdateHoveredIds()
h.state.mouse_state.ApplyHoverStyles(h.lp)
h.lp.EndAtomicUpdate()
}()
h.lp.ClearScreen()
h.state.mouse_state.ClearCellRegions()
switch h.state.screen {
case NORMAL:
matches, is_complete := h.get_results()
h.lp.SetWindowTitle(h.state.WindowTitle())
defer func() { // so that the cursor ends up in the right place
h.lp.MoveCursorTo(1, 1)
h.draw_search_bar(0)
}()
y := SEARCH_BAR_HEIGHT
footer_height, err := h.draw_footer()
if err != nil {
return err
}
y += h.draw_results(y, footer_height, matches, !is_complete)
case SAVE_FILE:
err = h.draw_save_file_name_screen()
}
return
}
func load_config(opts *Options) (ans *Config, err error) {
ans = NewConfig()
p := config.ConfigParser{LineHandler: ans.Parse}
err = p.LoadConfig("choose-files.conf", opts.Config, opts.Override)
if err != nil {
return nil, err
}
ans.KeyboardShortcuts = config.ResolveShortcuts(ans.KeyboardShortcuts)
return ans, nil
}
func (h *Handler) init_sizes(new_size loop.ScreenSize) {
h.screen_size.width = int(new_size.WidthCells)
h.screen_size.height = int(new_size.HeightCells)
h.screen_size.cell_width = int(new_size.CellWidth)
h.screen_size.cell_height = int(new_size.CellHeight)
h.screen_size.width_px = int(new_size.WidthPx)
h.screen_size.height_px = int(new_size.HeightPx)
h.rl.ClearCachedScreenSize()
}
func (h *Handler) OnInitialize() (ans string, err error) {
if sz, err := h.lp.ScreenSize(); err != nil {
return "", err
} else {
h.init_sizes(sz)
}
h.lp.AllowLineWrapping(false)
h.lp.SetCursorShape(loop.BAR_CURSOR, true)
h.lp.StartBracketedPaste()
if h.state.suggested_save_file_path != "" {
switch h.state.mode {
case SELECT_SAVE_FILE, SELECT_SAVE_DIR:
if s, err := os.Stat(h.state.suggested_save_file_path); err == nil {
if (s.IsDir() && h.state.mode != SELECT_SAVE_FILE) || (!s.IsDir() && h.state.mode == SELECT_SAVE_FILE) {
h.state.SetCurrentDir(filepath.Dir(h.state.suggested_save_file_path))
h.state.SetSearchText(filepath.Base(h.state.suggested_save_file_name))
}
}
}
}
h.result_manager.set_root_dir()
h.draw_screen()
return
}
func (h *Handler) current_abspath() string {
matches, _ := h.get_results()
if r := matches.At(h.state.CurrentIndex()); r != nil {
return filepath.Join(h.state.CurrentDir(), r.text)
}
return ""
}
func (s *State) CanSelect(r *ResultItem) bool {
return utils.IfElse(s.OnlyDirs(), r.ftype.IsDir(), !r.ftype.IsDir())
}
func (h *Handler) toggle_selection_at(idx CollectionIndex) bool {
matches, _ := h.get_results()
if r := matches.At(idx); r != nil && h.state.CanSelect(r) {
m := filepath.Join(h.state.CurrentDir(), r.text)
if added := h.state.ToggleSelection(m); added {
h.result_manager.last_click_anchor = &idx
} else {
h.result_manager.last_click_anchor = nil
if len(h.state.selections) > 0 {
x := utils.NewSetWithItems(h.state.selections...)
cdir := h.state.CurrentDir()
h.result_manager.last_click_anchor = matches.Closest(idx, func(q *ResultItem) bool { return x.Has(filepath.Join(cdir, q.text)) })
}
}
return true
}
return false
}
func (h *Handler) toggle_selection() bool {
return h.toggle_selection_at(h.state.CurrentIndex())
}
func (h *Handler) change_current_dir(dir string) {
if dir != h.state.CurrentDir() {
h.state.SetCurrentDir(dir)
h.result_manager.set_root_dir()
h.state.last_render = render_state{}
}
}
func (h *Handler) set_query(q string) {
if q != h.state.SearchText() {
h.state.SetSearchText(q)
h.result_manager.set_query()
h.state.last_render = render_state{}
}
}
func (h *Handler) set_filter(filter_name string) {
if filter_name != h.state.current_filter {
h.state.current_filter = filter_name
h.result_manager.set_filter()
h.state.last_render = render_state{}
}
}
func (h *Handler) change_to_current_dir_if_possible() error {
if m := h.current_abspath(); m != "" {
if st, err := os.Stat(m); err == nil {
if !st.IsDir() {
m = filepath.Dir(m)
}
h.change_current_dir(m)
return h.draw_screen()
}
}
h.lp.Beep()
return nil
}
func (h *Handler) finish_selection() error {
if h.state.mode.CanSelectNonExistent() && len(h.state.selections) == 0 {
return h.switch_to_save_file_name_mode()
}
h.lp.Quit(0)
return nil
}
func (h *Handler) change_filter(delta int) bool {
if len(h.state.filter_names) < 2 {
return false
}
idx := slices.Index(h.state.filter_names, h.state.current_filter)
idx += delta + len(h.state.filter_names)
idx %= len(h.state.filter_names)
h.set_filter(h.state.filter_names[idx])
return true
}
func (h *Handler) switch_to_save_file_name_mode() error {
name := h.state.suggested_save_file_name
if h.state.SearchText() != "" {
name = h.state.SearchText()
}
h.initialize_save_file_name(name)
return h.draw_screen()
}
func (h *Handler) accept_idx(idx CollectionIndex) (accepted bool, err error) {
matches, _ := h.get_results()
if r := matches.At(idx); r != nil {
m := filepath.Join(h.state.CurrentDir(), r.text)
if h.state.mode.SelectFiles() {
var s os.FileInfo
if s, err = os.Stat(m); err != nil {
return false, nil
}
if s.IsDir() {
if h.state.mode.CanSelectNonExistent() {
return true, h.switch_to_save_file_name_mode()
}
return false, nil
}
}
h.state.AddSelection(m)
h.result_manager.last_click_anchor = &idx
if len(h.state.selections) > 0 {
return true, h.finish_selection()
}
return true, h.draw_screen()
}
return
}
func (h *Handler) dispatch_action(name, args string) (err error) {
switch name {
case "quit":
h.lp.Quit(1)
case "next":
if n, nerr := strconv.Atoi(args); nerr == nil {
h.next_result(n)
} else {
switch args {
case "":
h.next_result(1)
case "left":
h.move_sideways(true)
case "right":
h.move_sideways(false)
case "first":
h.state.SetCurrentIndex(CollectionIndex{})
h.state.last_render.num_before = 0
case "last":
matches, _ := h.get_results()
h.state.SetCurrentIndex(matches.IncrementIndexWithWrapAround(CollectionIndex{}, -1))
h.state.last_render.num_before = 0
case "first_on_screen":
h.state.SetCurrentIndex(h.state.last_render.first_idx)
h.state.last_render.num_before = 0
case "last_on_screen":
matches, _ := h.get_results()
h.state.SetCurrentIndex(matches.IncrementIndexWithWrapAround(h.state.last_render.first_idx, h.state.last_render.num_shown-1))
h.state.last_render.num_before = h.state.last_render.num_shown - 1
}
}
return h.draw_screen()
case "next_filter":
if n, nerr := strconv.Atoi(args); nerr == nil {
h.change_filter(n)
return h.draw_screen()
}
h.lp.Beep()
case "select":
if !h.toggle_selection() {
h.lp.Beep()
} else {
return h.draw_screen()
}
case "accept":
accepted, aerr := h.accept_idx(h.state.CurrentIndex())
if aerr != nil {
return aerr
}
if !accepted {
h.lp.Beep()
}
case "typename":
if !h.state.mode.CanSelectNonExistent() {
h.lp.Beep()
} else {
return h.switch_to_save_file_name_mode()
}
case "toggle":
switch args {
case "dotfiles":
h.state.show_hidden = !h.state.show_hidden
h.result_manager.set_show_hidden()
return h.draw_screen()
case "ignorefiles":
h.state.respect_ignores = !h.state.respect_ignores
h.result_manager.set_respect_ignores()
return h.draw_screen()
case "sort_by_dates":
h.state.sort_by_last_modified = !h.state.sort_by_last_modified
h.result_manager.set_sort_by_last_modified()
return h.draw_screen()
default:
h.lp.Beep()
}
case "cd":
switch args {
case "current":
return h.change_to_current_dir_if_possible()
case "up":
curr := h.state.CurrentDir()
switch curr {
case "/":
case ".":
if curr, err = os.Getwd(); err == nil && curr != "/" {
h.change_current_dir(filepath.Dir(curr))
return h.draw_screen()
}
default:
h.change_current_dir(filepath.Dir(curr))
return h.draw_screen()
}
h.lp.Beep()
default:
args = utils.Expanduser(args)
if st, serr := os.Stat(args); serr != nil || !st.IsDir() {
h.lp.Beep()
return
}
if absp, err := filepath.Abs(args); err == nil {
h.change_current_dir(absp)
return h.draw_screen()
}
}
}
return
}
func (h *Handler) OnKeyEvent(ev *loop.KeyEvent) (err error) {
switch h.state.screen {
case NORMAL:
if h.handle_edit_keys(ev) {
ev.Handled = true
h.draw_screen()
}
ac := h.shortcut_tracker.Match(ev, h.state.keyboard_shortcuts)
if ac != nil {
ev.Handled = true
return h.dispatch_action(ac.Name, ac.Args)
}
case SAVE_FILE:
err = h.save_file_name_handle_key(ev)
}
return
}
func (h *Handler) OnMouseEvent(event *loop.MouseEvent) (err error) {
h.state.redraw_needed = h.state.mouse_state.UpdateState(event)
if err = h.state.mouse_state.DispatchEventToHoveredRegions(event); err != nil {
return
}
if h.state.redraw_needed {
err = h.draw_screen()
}
return
}
func (h *Handler) OnText(text string, from_key_event, in_bracketed_paste bool) (err error) {
switch h.state.screen {
case NORMAL:
h.set_query(h.state.SearchText() + text)
return h.draw_screen()
case SAVE_FILE:
if err = h.rl.OnText(text, from_key_event, in_bracketed_paste); err == nil {
err = h.draw_screen()
}
}
return
}
type CachedValues struct {
Show_hidden bool `json:"show_hidden"`
Respect_ignores bool `json:"respect_ignores"`
Sort_by_last_modified bool `json:"sort_by_last_modified"`
}
const cache_filename = "choose-files.json"
var cached_values = sync.OnceValue(func() *CachedValues {
ans := CachedValues{Respect_ignores: true}
fname := filepath.Join(utils.CacheDir(), cache_filename)
if data, err := os.ReadFile(fname); err == nil {
_ = json.Unmarshal(data, &ans)
}
return &ans
})
func (s State) save_cached_values() {
c := CachedValues{Show_hidden: s.show_hidden, Respect_ignores: s.respect_ignores, Sort_by_last_modified: s.sort_by_last_modified}
fname := filepath.Join(utils.CacheDir(), cache_filename)
if data, err := json.Marshal(c); err == nil {
_ = os.WriteFile(fname, data, 0600)
}
}
func (h *Handler) set_state_from_config(conf *Config, opts *Options) (err error) {
h.state = State{}
switch opts.Mode {
case "file":
h.state.mode = SELECT_SINGLE_FILE
case "files":
h.state.mode = SELECT_MULTIPLE_FILES
case "save-file":
h.state.mode = SELECT_SAVE_FILE
case "dir":
h.state.mode = SELECT_SINGLE_DIR
case "dirs":
h.state.mode = SELECT_MULTIPLE_DIRS
case "save-dir":
h.state.mode = SELECT_SAVE_DIR
case "save-files":
h.state.mode = SELECT_SAVE_FILES
default:
h.state.mode = SELECT_SINGLE_FILE
}
h.state.suggested_save_file_name = opts.SuggestedSaveFileName
h.state.suggested_save_file_path = opts.SuggestedSaveFilePath
h.state.filter_map = nil
h.state.current_filter = ""
if len(opts.FileFilter) > 0 {
opts.FileFilter = utils.Uniq(opts.FileFilter)
has_all_files := false
fmap := make(map[string][]Filter)
seen := utils.NewSet[string](len(opts.FileFilter))
for _, x := range opts.FileFilter {
f, ferr := NewFilter(x)
if ferr != nil {
return ferr
}
if f.Match == nil {
has_all_files = true
}
if h.state.current_filter == "" {
h.state.current_filter = f.Name
}
fmap[f.Name] = append(fmap[f.Name], *f)
if !seen.Has(f.Name) {
seen.Add(f.Name)
h.state.filter_names = append(h.state.filter_names, f.Name)
}
}
if !has_all_files {
af, _ := NewFilter("glob:*:All files")
fmap[af.Name] = append(fmap[af.Name], *af)
if !seen.Has(af.Name) {
h.state.filter_names = append(h.state.filter_names, af.Name)
}
}
h.state.filter_map = make(map[string]Filter)
for name, filters := range fmap {
h.state.filter_map[name] = CombinedFilter(filters...)
}
}
h.state.sort_by_last_modified = false
h.state.respect_ignores = true
h.state.show_hidden = false
switch conf.Show_hidden {
case Show_hidden_true, Show_hidden_y, Show_hidden_yes:
h.state.show_hidden = true
case Show_hidden_false, Show_hidden_n, Show_hidden_no:
h.state.show_hidden = false
case Show_hidden_last:
h.state.show_hidden = cached_values().Show_hidden
}
switch conf.Respect_ignores {
case Respect_ignores_true, Respect_ignores_y, Respect_ignores_yes:
h.state.respect_ignores = true
case Respect_ignores_false, Respect_ignores_n, Respect_ignores_no:
h.state.respect_ignores = false
case Respect_ignores_last:
h.state.respect_ignores = cached_values().Respect_ignores
}
switch conf.Sort_by_last_modified {
case Sort_by_last_modified_true, Sort_by_last_modified_y, Sort_by_last_modified_yes:
h.state.sort_by_last_modified = true
case Sort_by_last_modified_false, Sort_by_last_modified_n, Sort_by_last_modified_no:
h.state.sort_by_last_modified = false
case Sort_by_last_modified_last:
h.state.sort_by_last_modified = cached_values().Sort_by_last_modified
}
h.state.global_ignores = ignorefiles.NewGitignore()
if err = h.state.global_ignores.LoadLines(conf.Ignore...); err != nil {
return err
}
h.state.keyboard_shortcuts = conf.KeyboardShortcuts
return
}
var default_cwd string
func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) {
write_output := func(selections []string, interrupted bool, current_filter string) {
payload := make(map[string]any)
if err != nil {
if opts.WriteOutputTo != "" {
m := fmt.Sprint(err)
if opts.OutputFormat == "json" {
payload["error"] = m
b, _ := json.MarshalIndent(payload, "", " ")
m = string(b)
}
os.WriteFile(opts.WriteOutputTo, []byte(m), 0600)
}
return
}
if interrupted {
if opts.WriteOutputTo != "" {
if opts.OutputFormat == "json" {
payload["interrupted"] = true
b, _ := json.MarshalIndent(payload, "", " ")
os.WriteFile(opts.WriteOutputTo, b, 0600)
}
}
return
}
m := strings.Join(selections, "\n")
fmt.Print(m)
if opts.WriteOutputTo != "" {
if opts.OutputFormat == "json" {
payload["paths"] = selections
if current_filter != "" {
payload["current_filter"] = current_filter
}
b, _ := json.MarshalIndent(payload, "", " ")
m = string(b)
}
os.WriteFile(opts.WriteOutputTo, []byte(m), 0600)
}
}
conf, err := load_config(opts)
if err != nil {
return 1, err
}
lp, err := loop.New()
if err != nil {
return 1, err
}
lp.MouseTrackingMode(loop.FULL_MOUSE_TRACKING)
handler := Handler{lp: lp, err_chan: make(chan error, 8), msg_printer: message.NewPrinter(utils.LanguageTag()), spinner: tui.NewSpinner("dots")}
handler.rl = readline.New(lp, readline.RlInit{
Prompt: "> ", ContinuationPrompt: ". ", Completer: handler.complete_save_prompt,
})
if err = handler.set_state_from_config(conf, opts); err != nil {
return 1, err
}
handler.result_manager = NewResultManager(handler.err_chan, &handler.state, lp.WakeupMainThread)
switch len(args) {
case 0:
if default_cwd, err = os.Getwd(); err != nil {
return
}
case 1:
default_cwd = args[0]
default:
return 1, fmt.Errorf("Can only specify one directory to search in")
}
default_cwd = utils.Expanduser(default_cwd)
if default_cwd, err = filepath.Abs(default_cwd); err != nil {
return
}
lp.OnInitialize = func() (string, error) {
if opts.WritePidTo != "" {
if err := utils.AtomicWriteFile(opts.WritePidTo, bytes.NewReader([]byte(strconv.Itoa(os.Getpid()))), 0600); err != nil {
return "", err
}
}
if opts.Title != "" {
lp.SetWindowTitle(opts.Title)
}
return handler.OnInitialize()
}
lp.OnResize = func(old, new_size loop.ScreenSize) (err error) {
handler.init_sizes(new_size)
return handler.draw_screen()
}
lp.OnKeyEvent = handler.OnKeyEvent
lp.OnText = handler.OnText
lp.OnMouseEvent = handler.OnMouseEvent
lp.OnWakeup = func() (err error) {
select {
case err = <-handler.err_chan:
default:
err = handler.draw_screen()
}
return
}
err = lp.Run()
handler.state.save_cached_values()
if err != nil {
write_output(nil, false, "")
return 1, err
}
ds := lp.DeathSignalName()
if ds != "" {
fmt.Println("Killed by signal: ", ds)
lp.KillIfSignalled()
write_output(nil, true, "")
return 1, nil
}
rc = lp.ExitCode()
switch rc {
case 0:
write_output(handler.state.selections, false, handler.state.current_filter)
default:
write_output(nil, true, "")
}
return
}
func EntryPoint(parent *cli.Command) {
create_cmd(parent, main)
}

View File

@@ -1,169 +0,0 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2025, Kovid Goyal <kovid at kovidgoyal.net>
import sys
from kitty.conf.types import Definition
from kitty.constants import appname
from kitty.simple_cli_definitions import CONFIG_HELP, CompletionSpec
definition = Definition(
'!kittens.choose_files',
)
agr = definition.add_group
egr = definition.end_group
opt = definition.add_option
map = definition.add_map
mma = definition.add_mouse_map
agr('scanning', 'Filesystem scanning') # {{{
opt('show_hidden', 'last', choices=('last', 'yes', 'y', 'true', 'no', 'n', 'false'), long_text='''
Whether to show hidden files. The default value of :code:`last` means remember the last
used value. This setting can be toggled withing the program.''')
opt('sort_by_last_modified', 'last', choices=('last', 'yes', 'y', 'true', 'no', 'n', 'false'), long_text='''
Whether to sort the list of entries by last modified, instead of name. Note that sorting only applies
before any query is entered. Once a query is entered entries are sorted by their matching score.
The default value of :code:`last` means remember the last
used value. This setting can be toggled withing the program.''')
opt('respect_ignores', 'last', choices=('last', 'yes', 'y', 'true', 'no', 'n', 'false'), long_text='''
Whether to respect .gitignore and .ignore files and the :opt:`ignore` setting.
The default value of :code:`last` means remember the last used value.
This setting can be toggled withing the program.''')
opt('+ignore', '', add_to_default=False, long_text='''
An ignore pattern to ignore matched files. Uses the same sytax as :code:`.gitignore` files (see :code:`man gitignore`).
Anchored patterns match with respect to whatever directory is currently being displayed.
Can be specified multiple times to use multiple patterns. Note that every pattern
has to be checked against every file, so use sparingly.
''')
egr() # }}}
agr('shortcuts', 'Keyboard shortcuts') # {{{
map('Quit', 'quit esc quit')
map('Quit', 'quit ctrl+c quit')
map('Accept current result', 'accept enter accept')
map('Select current result', 'select shift+enter select', long_text='''
When selecting multiple files, this will add the current file to the list of selected files.
You can also toggle the selected status of a file by holding down the :kbd:`Ctrl` key and clicking on
it. Similarly, the :kbd:`Alt` key can be held to click and extend the range of selected files.
''')
map('Type file name', 'typename ctrl+enter typename', long_text='''
Type a file name/path rather than filtering the list of existing files.
Useful when specifying a file name for saving that does not yet exist.
Does not work when selecting files to open rather than to save.
''')
map('Next result', 'next_result down next 1')
map('Previous result', 'prev_result up next -1')
map('Left result', 'left_result left next left')
map('Right result', 'right_result right next right')
map('First result on screen', 'first_result_on_screen home next first_on_screen')
map('Last result on screen', 'last_result_on_screen end next last_on_screen')
map('First result', 'first_result_on_screen ctrl+home next first')
map('Last result', 'last_result_on_screen ctrl+end next last')
map('Change to currently selected dir', 'cd_current tab cd current')
map('Change to parent directory', 'cd_parent shift+tab cd up')
map('Change to root directory', 'cd_root ctrl+/ cd /')
map('Change to home directory', 'cd_home ctrl+~ cd ~')
map('Change to home directory', 'cd_home ctrl+` cd ~')
map('Change to home directory', 'cd_home ctrl+shift+` cd ~')
map('Change to temp directory', 'cd_tmp ctrl+t cd /tmp')
map('Next filter', 'next_filter ctrl+f 1')
map('Previous filter', 'prev_filter alt+f -1')
map('Toggle showing dotfiles', 'toggle_dotfiles alt+h toggle dotfiles')
map('Toggle showing ignored files', 'toggle_ignorefiles alt+d toggle ignorefiles')
map('Toggle sorting by dates', 'toggle_sort_by_dates alt+d toggle sort_by_dates')
egr() # }}}
def main(args: list[str]) -> None:
raise SystemExit('This must be run as kitten choose-files')
usage = '[directory to start choosing files in]'
OPTIONS = '''
--mode
type=choices
choices=file,files,save-file,dir,save-dir,dirs,save-files
default=file
The type of object(s) to select
--file-filter
type=list
A list of filters to restrict the displayed files. Can be either mimetypes, or glob style patterns. Can be specified multiple times.
The syntax is :code:`type:expression:Descriptive Name`.
For example: :code:`mime:image/png:Images` and :code:`mime:image/gif:Images` and :code:`glob:*.[tT][xX][Tt]:Text files`.
Note that glob patterns are case-sensitive. The mimetype specification is treated as a glob expressions as well, so you can,
for example, use :code:`mime:text/*` to match all text files. The first filter in the list will be applied by default. Use a filter
such as :code:`glob:*:All` to match all files. Note that filtering only appies to files, not directories.
--suggested-save-file-name
A suggested name when picking a save file.
--suggested-save-file-path
Path to an existing file to use as the save file.
--title
Window title to use for this chooser
--override -o
type=list
Override individual configuration options, can be specified multiple times.
Syntax: :italic:`name=value`.
--config
type=list
completion=type:file ext:conf group:"Config files" kwds:none,NONE
{config_help}
--write-output-to
Path to a file to which the output is written in addition to STDOUT.
--output-format
choices=text,json
default=text
The format in which to write the output.
--write-pid-to
Path to a file to which to write the process ID (PID) of this process to.
'''.format(config_help=CONFIG_HELP.format(conf_name='choose-files', appname=appname)).format
help_text = '''\
Select one or more files, quickly, using fuzzy finding, by typing just a few characters from
the file name. Browse matching files, using the arrow keys to navigate matches and press :kbd:`Enter`
to select. The :kbd:`Tab` key can be used to change to a sub-folder. See the :doc:`online docs </kittens/choose-files>`
for full details.
'''
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
cd['short_desc'] = 'Choose files, fast'
cd['args_completion'] = CompletionSpec.from_string('type:directory')
elif __name__ == '__conf__':
sys.options_definition = definition # type: ignore

View File

@@ -1,333 +0,0 @@
package choose_files
import (
"fmt"
"io/fs"
"math"
"os"
"path/filepath"
"runtime"
"strings"
"unicode/utf8"
"github.com/kovidgoyal/kitty/tools/icons"
"github.com/kovidgoyal/kitty/tools/tui"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/style"
"github.com/kovidgoyal/kitty/tools/wcswidth"
)
var _ = fmt.Print
func (h *Handler) draw_results_title() {
text := filepath.Clean(h.state.CurrentDir())
home := filepath.Clean(utils.Expanduser("~"))
if strings.HasPrefix(text, home) {
text = "~" + text[len(home):]
}
available_width := h.screen_size.width - 9
if available_width < 2 {
return
}
tt := wcswidth.TruncateToVisualLength(text, available_width)
if len(tt) < len(text) {
text = wcswidth.TruncateToVisualLength(text, available_width-1)
}
text = fmt.Sprintf(" %s %s ", h.lp.SprintStyled("fg=blue", icons.IconForFileWithMode(text, fs.ModeDir, false)+" "), h.lp.SprintStyled("fg=intense-white bold", text))
extra := available_width - wcswidth.Stringwidth(text)
x := 3
if extra > 1 {
x += extra / 2
}
h.lp.MoveCursorHorizontally(x)
h.lp.QueueWriteString(text)
}
func (h *Handler) draw_no_matches_message(in_progress bool) {
text := "Scanning filesystem, please wait…"
if !in_progress {
text = utils.IfElse(h.state.SearchText() == "", "No files present in this folder", "No matches found")
}
for _, line := range style.WrapTextAsLines(text, h.screen_size.width-2, style.WrapOptions{}) {
h.lp.QueueWriteString("\r")
h.lp.MoveCursorHorizontally(1)
h.lp.QueueWriteString(line)
h.lp.MoveCursorVertically(1)
}
}
const matching_position_style = "fg=green"
const selected_style = "fg=magenta"
const current_style = "fg=intense-white bold"
func (h *Handler) render_match_with_positions(text string, add_ellipsis bool, positions []int, is_current bool) {
prefix, suffix, _ := strings.Cut(h.lp.SprintStyled(matching_position_style, " "), " ")
if is_current {
p, s, _ := strings.Cut(h.lp.SprintStyled(current_style, " "), " ")
h.lp.QueueWriteString(p)
defer h.lp.QueueWriteString(s)
suffix += p
}
write_chunk := func(text string, emphasize bool) {
if text == "" {
return
}
if emphasize {
h.lp.QueueWriteString(prefix)
defer func() {
h.lp.QueueWriteString(suffix)
}()
}
h.lp.QueueWriteString(text)
}
at := 0
limit := len(text)
for _, p := range positions {
if p > limit || at > limit {
break
}
write_chunk(text[at:p], false)
at = p
if r, sz := utf8.DecodeRuneInString(text[p:]); r != utf8.RuneError {
write_chunk(string(r), true)
at += sz
}
}
if at < len(text) {
write_chunk(text[at:], false)
}
if add_ellipsis {
write_chunk("…", false)
}
}
var icon_cache map[string]string
func icon_for(path string, x os.FileMode) string {
if icon_cache == nil {
icon_cache = make(map[string]string, 512)
}
if ans := icon_cache[path]; ans != "" {
return ans
}
var ans string
if x&fs.ModeSymlink != 0 && x&SymlinkToDir != 0 {
ans = string(icons.SYMLINK_TO_DIR)
} else {
ans = icons.IconForFileWithMode(path, x, true)
}
if wcswidth.Stringwidth(ans) == 1 {
ans += " "
}
icon_cache[path] = ans
return ans
}
func (h *Handler) draw_column_of_matches(matches ResultsType, current_idx int, x, y, available_width, colnum int) {
root_dir := h.state.CurrentDir()
for i, m := range matches {
h.lp.QueueWriteString("\r")
h.lp.MoveCursorHorizontally(x)
is_selected := h.state.IsSelected(m)
var icon string
if is_selected {
icon = "󰗠 "
} else {
icon = icon_for(filepath.Join(root_dir, m.text), m.ftype)
}
text := m.text
add_ellipsis := false
width := wcswidth.Stringwidth(text)
if width > available_width-3 {
text = wcswidth.TruncateToVisualLength(text, available_width-4)
add_ellipsis = true
width = available_width - 3
}
is_current := i == current_idx
if is_current {
h.lp.QueueWriteString(h.lp.SprintStyled(matching_position_style, icon+" "))
} else {
if is_selected {
h.lp.QueueWriteString(h.lp.SprintStyled(selected_style, icon+" "))
} else {
h.lp.QueueWriteString(icon + " ")
}
}
h.render_match_with_positions(text, add_ellipsis, m.sorted_positions(), is_current)
h.lp.MoveCursorVertically(1)
cr := h.state.mouse_state.AddCellRegion(fmt.Sprintf("result-%d-%d", colnum, i), x, y-1+i, x+width+2, y-1+i)
cr.HoverStyle = HOVER_STYLE
var data struct {
colnum, i int
}
data.colnum, data.i = colnum, i
cr.OnClickEvent = func(id string, ev *loop.MouseEvent, cell_offset tui.Point) error {
if ev.Buttons&loop.LEFT_MOUSE_BUTTON == 0 {
return nil
}
ctrl_mod := utils.IfElse(runtime.GOOS == "darwin", loop.SUPER, loop.CTRL)
mods := ev.Mods & (ctrl_mod | loop.ALT) // shift alone and ctrl+shift are used for kitty bindings
matches, _ := h.get_results()
num_before := h.state.last_render.num_of_slots*data.colnum + data.i
idx, did_wrap := matches.IncrementIndexWithWrapAroundAndCheck(h.state.last_render.first_idx, num_before)
if did_wrap {
h.lp.Beep()
return nil
}
d := matches.SignedDistance(idx, h.state.current_idx)
h.state.SetCurrentIndex(idx)
h.state.last_render.num_before = max(0, h.state.last_render.num_before+d)
switch mods {
case 0:
h.dispatch_action("accept", "")
case ctrl_mod, ctrl_mod | loop.ALT:
h.dispatch_action("select", "")
case loop.ALT:
r := matches.At(idx)
if (r != nil && h.state.IsSelected(r)) || h.result_manager.last_click_anchor == nil {
h.dispatch_action("select", "")
return nil
}
already_selected := utils.NewSetWithItems(h.state.selections...)
cdir := h.state.CurrentDir()
matches.Apply(idx, *h.result_manager.last_click_anchor, func(r *ResultItem) bool {
m := filepath.Join(cdir, r.text)
if !already_selected.Has(m) && h.state.CanSelect(r) {
already_selected.Add(m)
h.state.selections = append(h.state.selections, m)
}
return true
})
return h.draw_screen()
}
return nil
}
}
}
func (h *Handler) draw_list_of_results(matches *SortedResults, y, height int) (num_cols, num_shown int) {
available_width := h.screen_size.width - 2
col_width := available_width
num_cols = 1
calc_num_cols := func(num_matches int) int {
if num_matches == 0 || height < 2 {
return 0
}
if num_matches > height {
col_width = 40
num_cols = available_width / col_width
for num_cols > 0 && height*(num_cols-1) >= num_matches {
num_cols--
}
col_width = available_width / num_cols
}
return num_cols
}
columns, num_before, first_idx := matches.SplitIntoColumns(calc_num_cols, height, h.state.last_render.num_before, h.state.CurrentIndex())
h.state.last_render.num_before = num_before
h.state.last_render.num_per_column = height
h.state.last_render.num_columns = num_cols
h.state.last_render.first_idx = first_idx
x := 1
for i, col := range columns {
h.lp.MoveCursorTo(x, y)
h.draw_column_of_matches(col, num_before, x, y, col_width-1, i)
num_before -= height
num_shown += len(col)
x += col_width
}
return len(columns), num_shown
}
func (h *Handler) draw_num_of_matches(num_shown, y int, in_progress bool) {
m := ""
switch h.state.last_render.num_matches {
case 0:
m = " no matches "
default:
m = fmt.Sprintf(" %d of %s matches ", min(num_shown, h.state.last_render.num_matches), h.msg_printer.Sprint(h.state.last_render.num_matches))
}
w := int(math.Ceil(float64(wcswidth.Stringwidth(m)) / 2.0))
spinner := ""
spinner_width := 0
if in_progress {
spinner = h.spinner.Tick()
spinner_width = 1 + wcswidth.Stringwidth(spinner)
}
h.lp.MoveCursorTo(h.screen_size.width-w-spinner_width-2, y)
st := loop.SizedText{Subscale_denominator: 2, Subscale_numerator: 1, Vertical_alignment: 2, Width: 1}
graphemes := wcswidth.SplitIntoGraphemes(m)
for len(graphemes) > 0 {
s := ""
for w := 0; w < 2 && len(graphemes) > 0; {
w += wcswidth.Stringwidth(graphemes[0])
s += graphemes[0]
graphemes = graphemes[1:]
}
h.lp.DrawSizedText(s, st)
}
if spinner != "" {
h.lp.QueueWriteString(spinner)
}
}
func (h *Handler) draw_results(y, bottom_margin int, matches *SortedResults, in_progress bool) (height int) {
height = h.screen_size.height - y - bottom_margin
h.lp.MoveCursorTo(1, 1+y)
h.draw_frame(h.screen_size.width, height, in_progress)
h.lp.MoveCursorTo(1, 1+y)
h.draw_results_title()
y += 2
h.lp.MoveCursorTo(1, y)
h.state.last_render.num_of_slots = height - 2
num_cols := 0
num := matches.Len()
num_shown := 0
switch num {
case 0:
h.draw_no_matches_message(in_progress)
default:
num_cols, num_shown = h.draw_list_of_results(matches, y, h.state.last_render.num_of_slots)
}
h.state.last_render.num_matches = num
h.state.last_render.num_shown = num_shown
h.draw_num_of_matches(h.state.last_render.num_of_slots*num_cols, y+height-2, in_progress)
return
}
func (h *Handler) next_result(amt int) {
if h.state.last_render.num_matches > 0 {
idx := h.state.CurrentIndex()
idx = h.result_manager.scorer.sorted_results.IncrementIndexWithWrapAround(idx, amt)
h.state.SetCurrentIndex(idx)
h.state.last_render.num_before = max(0, h.state.last_render.num_before+amt)
}
}
func (h *Handler) move_sideways(leftwards bool) {
r := h.state.last_render
if r.num_matches > 0 && r.num_per_column > 0 {
cidx := h.state.CurrentIndex()
slots := r.num_of_slots
if leftwards {
idx := h.result_manager.scorer.sorted_results.IncrementIndexWithWrapAround(cidx, -slots)
if idx.Less(cidx) {
h.state.SetCurrentIndex(idx)
if r.num_columns > 1 && r.num_before >= r.num_per_column {
h.state.last_render.num_before = max(0, h.state.last_render.num_before-slots)
}
}
} else {
idx := h.result_manager.scorer.sorted_results.IncrementIndexWithWrapAround(cidx, slots)
if cidx.Less(idx) {
h.state.SetCurrentIndex(idx)
if r.num_columns > 1 && r.num_before < (r.num_columns-1)*r.num_per_column {
h.state.last_render.num_before = max(0, h.state.last_render.num_before+slots)
}
}
}
}
}

View File

@@ -1,140 +0,0 @@
package choose_files
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/kovidgoyal/kitty/tools/cli"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/utils"
)
var _ = fmt.Print
func (h *Handler) complete_save_prompt(before_cursor, after_cursor string) *cli.Completions {
path := before_cursor
prefix := ""
if idx := strings.Index(path, string(os.PathSeparator)); idx > -1 {
prefix = filepath.Dir(path) + string(os.PathSeparator)
}
dir := ""
if path == "" {
path = h.state.CurrentDir()
dir = path
} else {
if !filepath.IsAbs(path) {
path = filepath.Join(h.state.CurrentDir(), path)
}
dir = filepath.Dir(path)
if strings.HasSuffix(before_cursor, string(os.PathSeparator)) {
dir = path
}
}
entries, err := os.ReadDir(dir)
if err != nil {
return nil
}
ans := cli.NewCompletions()
dirs := ans.AddMatchGroup("Directories")
dirs.IsFiles = true
dirs.NoTrailingSpace = true
files := ans.AddMatchGroup("Files")
files.IsFiles = true
files.NoTrailingSpace = true
leading, _ := filepath.Rel(dir, path)
if leading == "." {
leading = ""
}
for _, e := range entries {
word := e.Name()
if leading == "" || strings.HasPrefix(word, leading) {
collection := utils.IfElse(e.Type().IsDir(), dirs, files)
if prefix != "" {
word = prefix + word
}
collection.Matches = append(collection.Matches, &cli.Match{Word: word})
}
}
return ans
}
func (h *Handler) current_save_file_path() string {
ans := h.rl.AllText()
if ans != "" {
ans = utils.Expanduser(ans)
if !filepath.IsAbs(ans) {
ans = filepath.Join(h.state.CurrentDir(), ans)
}
}
return ans
}
func (h *Handler) save_file_name_handle_key(ev *loop.KeyEvent) (err error) {
ac := h.shortcut_tracker.Match(ev, h.state.keyboard_shortcuts)
if ac != nil {
switch ac.Name {
case "accept":
if p := h.current_save_file_path(); p != "" {
h.state.selections = append(h.state.selections, p)
h.lp.Quit(0)
} else {
h.lp.Beep()
}
ev.Handled = true
return nil
case "select":
if p := h.current_save_file_path(); p != "" {
h.state.selections = append(h.state.selections, p)
}
ev.Handled = true
h.rl.ResetText()
return h.draw_screen()
case "quit":
h.state.screen = NORMAL
ev.Handled = true
return h.draw_screen()
}
}
if err = h.rl.OnKeyEvent(ev); err == nil {
err = h.draw_screen()
}
return
}
func (h *Handler) initialize_save_file_name(fname string) {
h.state.screen = SAVE_FILE
h.rl.ResetText()
if len(h.state.selections) > 0 && fname == "" {
if q, err := filepath.Abs(h.state.selections[0]); err == nil {
if s, err := os.Stat(q); err == nil {
if s.IsDir() == h.state.mode.OnlyDirs() {
if fname, err = filepath.Rel(h.state.CurrentDir(), q); err != nil {
fname = q
}
}
}
}
}
h.rl.SetText(fname)
}
func (h *Handler) draw_save_file_name_screen() (err error) {
h.lp.AllowLineWrapping(true)
desc := utils.IfElse(h.state.mode == SELECT_SAVE_FILE, "file", "directory")
h.lp.Println("Enter the name of the", desc, "below, relative to:")
h.lp.Println(h.lp.SprintStyled("fg=green", h.state.CurrentDir()))
if h.state.mode.AllowsMultipleSelection() {
h.lp.Println("Use shift+enter (or whatever you mapped the select action to) to enter multiple filenames")
}
h.lp.Println()
h.rl.RedrawNonAtomic()
h.lp.AllowLineWrapping(false)
if len(h.state.selections) > 0 {
h.lp.SaveCursorPosition()
h.draw_footer()
h.lp.RestoreCursorPosition()
}
return
}

View File

@@ -1,746 +0,0 @@
package choose_files
import (
"bytes"
"cmp"
"encoding/binary"
"fmt"
"io/fs"
"math"
"os"
"path/filepath"
"slices"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
"unicode"
"unicode/utf8"
"github.com/kovidgoyal/kitty/tools/fzf"
"github.com/kovidgoyal/kitty/tools/ignorefiles"
"github.com/kovidgoyal/kitty/tools/utils"
"golang.org/x/sys/unix"
)
var _ = fmt.Print
func (c CombinedScore) String() string {
return fmt.Sprintf("{score: %d length: %d index: %d}", c.Score(), c.Length(), c.Index())
}
type ignore_file_with_prefix struct {
impl ignorefiles.IgnoreFile
prefix string
}
func (i *ignore_file_with_prefix) is_ignored(name string, ftype fs.FileMode) (ans bool, was_match bool) {
ans, linenum, _ := i.impl.IsIgnored(i.prefix+name, ftype)
was_match = linenum > -1
return
}
type ResultItem struct {
text string
ftype fs.FileMode
positions []int // may be nil
score CombinedScore
ignore_files []ignore_file_with_prefix
}
type ResultsType []*ResultItem
func (r *ResultItem) SetScoreResult(x fzf.Result) {
r.positions = x.Positions
r.score.Set_score(uint16(math.MaxUint16 - uint16(x.Score)))
}
func (r ResultItem) IsMatching() bool {
return r.score.Score() < uint16(math.MaxUint16)
}
func (r ResultItem) String() string {
return fmt.Sprintf("{text: %#v, %s, positions: %#v}", r.text, r.score, r.positions)
}
func (r *ResultItem) sorted_positions() []int {
if len(r.positions) > 1 {
sort.Ints(r.positions)
}
return r.positions
}
type FileSystemScanner struct {
listeners []chan bool
in_progress, keep_going atomic.Bool
root_dir string
mutex sync.Mutex
collection *ResultCollection
dir_reader func(path string) ([]fs.DirEntry, error)
file_reader func(path string) ([]byte, error)
filter_func func(filename string) bool
global_gitignore ignorefiles.IgnoreFile
global_ignore ignorefiles.IgnoreFile
respect_ignores, show_hidden bool
sort_by_last_modified bool
err error
}
func new_filesystem_scanner(root_dir string, notify chan bool, filter_func func(string) bool) (fss *FileSystemScanner) {
ans := &FileSystemScanner{root_dir: root_dir, listeners: []chan bool{notify}, collection: NewResultCollection(4096)}
ans.in_progress.Store(true)
ans.keep_going.Store(true)
ans.dir_reader = os.ReadDir
ans.file_reader = os.ReadFile
ans.filter_func = utils.IfElse(filter_func == nil, accept_all, filter_func)
ans.global_gitignore = ignorefiles.NewGitignore()
ans.global_ignore = ignorefiles.NewGitignore()
ans.respect_ignores = true
ans.show_hidden = false
return ans
}
type Scanner interface {
Start()
Cancel()
AddListener(chan bool)
Len() int
Batch(offset *CollectionIndex) []ResultItem
Finished() bool
Error() error
}
func (fss *FileSystemScanner) lock() { fss.mutex.Lock() }
func (fss *FileSystemScanner) unlock() { fss.mutex.Unlock() }
func (fss *FileSystemScanner) Error() error {
fss.lock()
defer fss.unlock()
return fss.err
}
func (fss *FileSystemScanner) Start() {
go fss.worker()
}
func (fss *FileSystemScanner) Cancel() {
fss.keep_going.Store(false)
}
func (fss *FileSystemScanner) AddListener(x chan bool) {
fss.lock()
defer fss.unlock()
if !fss.in_progress.Load() {
close(x)
} else {
fss.listeners = append(fss.listeners, x)
}
}
func (fss *FileSystemScanner) Len() int {
fss.lock()
defer fss.unlock()
return fss.collection.Len()
}
func (fss *FileSystemScanner) Batch(offset *CollectionIndex) []ResultItem {
fss.lock()
defer fss.unlock()
return fss.collection.Batch(offset)
}
func (fss *FileSystemScanner) Finished() bool {
return !fss.in_progress.Load()
}
type sortable_dir_entry struct {
name string
ftype fs.FileMode
sort_key []byte
buf [unix.NAME_MAX + 1]byte
}
const SymlinkToDir = 1
// lowercase a string into a pre-existing byte buffer with speedups for ASCII
func as_lower(s string, output []byte) int {
limit := min(len(s), len(output))
found_non_ascii := false
pos := 0
for i := range limit {
c := s[i]
if 'A' <= c && c <= 'Z' {
c += 'a' - 'A'
if pos < i {
copy(output[pos:i], s[pos:i])
}
output[i] = c
pos = i + 1
} else if c >= utf8.RuneSelf {
if pos < i {
copy(output[pos:i], s[pos:i])
}
found_non_ascii = true
pos = i
break
}
}
if !found_non_ascii {
if pos < limit {
copy(output[pos:limit], s[pos:limit])
}
return limit
}
buf := [4]byte{}
var n int
for _, r := range s[pos:] {
o := output[pos:]
r = unicode.ToLower(r)
if len(o) > 3 {
n = utf8.EncodeRune(o, r)
} else {
n = utf8.EncodeRune(buf[:], r)
n = copy(o, buf[:n])
}
pos += n
if pos >= len(output) {
break
}
}
return pos
}
func accept_all(filename string) bool { return true }
func (fss *FileSystemScanner) worker() {
defer func() {
fss.lock()
defer fss.unlock()
fss.in_progress.Store(false)
if r := recover(); r != nil {
st, qerr := utils.Format_stacktrace_on_panic(r)
fss.err = fmt.Errorf("%w\n%s", qerr, st)
}
for _, l := range fss.listeners {
close(l)
}
}()
root_dir, _ := filepath.Abs(fss.root_dir)
if !strings.HasSuffix(root_dir, string(os.PathSeparator)) {
root_dir += string(os.PathSeparator)
}
dir := root_dir
var ignore_files []ignore_file_with_prefix
base := ""
pos := &CollectionIndex{}
var arena []sortable_dir_entry
var sortable []*sortable_dir_entry
var ignoreable []*sortable_dir_entry
var idx uint32
dot_git := os.Getenv("GIT_DIR")
if dot_git == "" {
dot_git = ".git"
}
// do a breadth first traversal of the filesystem
is_root := true
for dir != "" {
if !fss.keep_going.Load() {
break
}
entries, err := fss.dir_reader(dir)
if err != nil {
if is_root {
fss.keep_going.Store(false)
fss.lock()
fss.err = err
fss.unlock()
}
entries = nil
}
if cap(arena) < len(entries) {
arena = make([]sortable_dir_entry, 0, max(1024, len(entries), 2*cap(arena)))
sortable = make([]*sortable_dir_entry, 0, cap(arena))
ignoreable = make([]*sortable_dir_entry, 0, cap(arena))
}
arena = arena[:len(entries)]
sortable = sortable[:0]
ignoreable = ignoreable[:0]
ignore_files_copied := false
add_ignore_file_from_impl := func(impl ignorefiles.IgnoreFile) {
// we want ignore_files to be a copy as we dont want to
// change the underlying array of ignore_files as it is
// referenced by multiple ResultItems
if !ignore_files_copied {
ignore_files_copied = true
n := make([]ignore_file_with_prefix, len(ignore_files), len(ignore_files)+4)
copy(n, ignore_files)
ignore_files = n
}
ignore_files = append(ignore_files, ignore_file_with_prefix{impl: impl})
}
add_ignore_file := func(name string) {
if data, rerr := fss.file_reader(dir + name); rerr == nil {
impl := ignorefiles.NewGitignore()
if rerr = impl.LoadString(utils.UnsafeBytesToString(data)); rerr == nil && impl.Len() > 0 {
add_ignore_file_from_impl(impl)
}
}
}
entry_is_ignored := func(name string, ftype fs.FileMode) (is_ignored bool) {
for _, ignore_file := range ignore_files {
if iig, was_match := ignore_file.is_ignored(name, ftype); was_match {
is_ignored = iig
}
}
return
}
has_git_ignore, has_dot_git, has_dot_ignore := false, false, false
for i, e := range entries {
name := e.Name()
ftype := e.Type()
is_dir := ftype&fs.ModeDir != 0
if !is_dir {
switch name {
case ".ignore":
has_dot_ignore = true
case ".gitignore":
has_git_ignore = true
}
if !fss.filter_func(name) {
continue
}
} else {
if name == dot_git {
has_dot_git = true
}
}
if !fss.show_hidden && name[0] == '.' {
continue
}
arena[i].name = name
if ftype&fs.ModeSymlink != 0 {
if st, serr := os.Stat(dir + arena[i].name); serr == nil && st.IsDir() {
ftype |= SymlinkToDir
}
}
arena[i].ftype = ftype
if is_dir {
arena[i].buf[0] = '0'
} else {
arena[i].buf[0] = '1'
}
if fss.sort_by_last_modified {
var ts time.Time
if info, err := e.Info(); err == nil {
ts = info.ModTime()
}
binary.BigEndian.PutUint64(arena[i].buf[1:], uint64(ts.UnixNano()))
arena[i].sort_key = arena[i].buf[:1+8]
} else {
n := as_lower(arena[i].name, arena[i].buf[1:])
arena[i].sort_key = arena[i].buf[:1+n]
}
sortable = append(sortable, &arena[i])
}
if fss.respect_ignores {
if is_root && fss.global_ignore.Len() > 0 {
add_ignore_file_from_impl(fss.global_ignore)
}
if has_dot_git {
if fss.global_gitignore.Len() > 0 {
add_ignore_file_from_impl(fss.global_gitignore)
}
add_ignore_file(filepath.Join(dot_git, "info", "exclude"))
if has_git_ignore {
add_ignore_file(".gitignore")
}
}
if has_dot_ignore {
add_ignore_file(".ignore")
}
}
final_entries := sortable
if len(ignore_files) > 0 {
for _, e := range sortable {
if !entry_is_ignored(e.name, e.ftype) {
ignoreable = append(ignoreable, e)
}
}
final_entries = ignoreable
}
slices.SortFunc(final_entries, func(a, b *sortable_dir_entry) int { return bytes.Compare(a.sort_key, b.sort_key) })
fss.lock()
for _, e := range final_entries {
i := fss.collection.NextAppendPointer()
i.ftype = e.ftype
i.text = base + e.name
i.score.Set_index(idx)
i.ignore_files = ignore_files
idx++
}
listeners := fss.listeners
fss.unlock()
for _, l := range listeners {
select {
case l <- true:
default:
}
}
ignore_files = nil
if relpath, ignf := fss.collection.NextDir(pos); relpath != "" {
base = relpath + string(os.PathSeparator)
dir = root_dir + base
if len(ignf) != 0 {
name := filepath.Base(relpath) + string(os.PathSeparator)
ignore_files = utils.Map(func(ignore_file ignore_file_with_prefix) ignore_file_with_prefix {
return ignore_file_with_prefix{impl: ignore_file.impl, prefix: ignore_file.prefix + name}
}, ignf)
}
} else {
dir = ""
}
is_root = false
}
}
type FileSystemScorer struct {
scanner Scanner
keep_going, is_complete atomic.Bool
root_dir, query string
filter Filter
only_dirs bool
mutex sync.Mutex
sorted_results *SortedResults
on_results func(error, bool)
current_worker_wait *sync.WaitGroup
scorer *fzf.FuzzyMatcher
dir_reader func(path string) ([]fs.DirEntry, error)
file_reader func(path string) ([]byte, error)
global_gitignore, global_ignore ignorefiles.IgnoreFile
respect_ignores, show_hidden bool
sort_by_last_modified bool
}
func NewFileSystemScorer(root_dir, query string, filter Filter, only_dirs bool, on_results func(error, bool)) (ans *FileSystemScorer) {
return &FileSystemScorer{
query: query, root_dir: root_dir, only_dirs: only_dirs, filter: filter, on_results: on_results,
scorer: fzf.NewFuzzyMatcher(fzf.PATH_SCHEME), sorted_results: NewSortedResults(), respect_ignores: true,
}
}
func (fss *FileSystemScorer) lock() { fss.mutex.Lock() }
func (fss *FileSystemScorer) unlock() { fss.mutex.Unlock() }
func (fss *FileSystemScorer) Start() {
on_results := make(chan bool)
fss.is_complete.Store(false)
fss.keep_going.Store(true)
if fss.scanner == nil {
sc := new_filesystem_scanner(fss.root_dir, on_results, fss.filter.Match)
if fss.dir_reader != nil {
sc.dir_reader = fss.dir_reader
}
if fss.file_reader != nil {
sc.file_reader = fss.file_reader
}
if fss.global_gitignore != nil {
sc.global_gitignore = fss.global_gitignore
} else if ignore := ignorefiles.GlobalGitignore(); ignore != nil {
sc.global_gitignore = ignore
}
if fss.global_ignore != nil {
sc.global_ignore = fss.global_ignore
}
sc.show_hidden, sc.respect_ignores = fss.show_hidden, fss.respect_ignores
sc.sort_by_last_modified = fss.sort_by_last_modified
fss.scanner = sc
fss.scanner.Start()
} else {
fss.scanner.AddListener(on_results)
}
fss.current_worker_wait = &sync.WaitGroup{}
fss.current_worker_wait.Add(1)
go fss.worker(on_results, fss.current_worker_wait)
}
func (fss *FileSystemScorer) Change_query(query string) {
if fss.query == query {
return
}
fss.keep_going.Store(false)
if fss.current_worker_wait != nil {
if fss.scanner != nil {
fss.scanner.Cancel()
}
fss.current_worker_wait.Wait()
}
fss.lock()
fss.query = query
fss.sorted_results.Clear()
fss.unlock()
fss.Start()
}
func (fss *FileSystemScorer) change_scanner_setting(callback func()) {
fss.keep_going.Store(false)
if fss.current_worker_wait != nil {
if fss.scanner != nil {
fss.scanner.Cancel()
}
fss.current_worker_wait.Wait()
}
fss.lock()
callback()
fss.sorted_results.Clear()
fss.scanner = nil
fss.unlock()
fss.Start()
}
func (fss *FileSystemScorer) Change_filter(filter Filter) {
if !fss.filter.Equal(filter) {
fss.change_scanner_setting(func() { fss.filter = filter })
}
}
func (fss *FileSystemScorer) Change_show_hidden(val bool) {
if fss.show_hidden != val {
fss.change_scanner_setting(func() { fss.show_hidden = val })
}
}
func (fss *FileSystemScorer) Change_respect_ignores(val bool) {
if fss.respect_ignores != val {
fss.change_scanner_setting(func() { fss.respect_ignores = val })
}
}
func (fss *FileSystemScorer) Change_sort_by_last_modified(val bool) {
if fss.sort_by_last_modified != val {
fss.change_scanner_setting(func() { fss.sort_by_last_modified = val })
}
}
func (fss *FileSystemScorer) worker(on_results chan bool, worker_wait *sync.WaitGroup) {
defer func() {
fss.is_complete.Store(true)
defer worker_wait.Done()
if r := recover(); r != nil {
if fss.keep_going.Load() {
st, qerr := utils.Format_stacktrace_on_panic(r)
fss.on_results(fmt.Errorf("%w\n%s", qerr, st), true)
}
} else {
if fss.keep_going.Load() {
fss.on_results(fss.scanner.Error(), true)
}
}
}()
handle_batch := func(results []ResultItem) (err error) {
if err = fss.scanner.Error(); err != nil {
return
}
var rp []*ResultItem
if fss.only_dirs {
rp = make([]*ResultItem, 0, len(results))
for i, r := range results {
if r.ftype.IsDir() {
rp = append(rp, &results[i])
}
}
} else {
if fss.filter.Match == nil {
rp = make([]*ResultItem, len(results))
for i := range len(rp) {
rp[i] = &results[i]
}
} else {
rp = make([]*ResultItem, 0, len(results))
for i, r := range results {
if r.ftype.IsDir() || fss.filter.Match(filepath.Base(r.text)) {
rp = append(rp, &results[i])
}
}
}
}
if len(rp) > 0 {
if fss.query != "" {
scores, err := fss.scorer.Score(utils.Map(func(r *ResultItem) string { return r.text }, rp), fss.query)
if err != nil {
return err
}
for i, r := range rp {
r.SetScoreResult(scores[i])
r.score.Set_length(uint16(len(r.text)))
}
rp = utils.Filter(rp, func(r *ResultItem) bool { return r.IsMatching() })
} else {
for _, r := range rp {
r.score &= 0b11111111111111111111111111111111 // only preserve index
r.positions = nil
}
}
}
if len(rp) > 0 {
slices.SortFunc(rp, func(a, b *ResultItem) int { return cmp.Compare(a.score, b.score) })
}
fss.sorted_results.AddSortedSlice(rp)
return
}
offset := &CollectionIndex{}
for range on_results {
if !fss.keep_going.Load() {
break
}
results := fss.scanner.Batch(offset)
if len(results) > 0 || fss.scanner.Error() != nil {
fss.on_results(handle_batch(results), false)
}
}
for fss.keep_going.Load() {
b := fss.scanner.Batch(offset)
if len(b) == 0 {
break
}
fss.on_results(handle_batch(b), false)
}
}
func (fss *FileSystemScorer) Results() (ans *SortedResults, is_finished bool) {
fss.lock()
defer fss.unlock()
return fss.sorted_results, fss.is_complete.Load()
}
func (fss *FileSystemScorer) Cancel() {
fss.keep_going.Store(false)
fss.scanner.Cancel()
}
type Settings interface {
OnlyDirs() bool
CurrentDir() string
SearchText() string
ShowHidden() bool
RespectIgnores() bool
SortByLastModified() bool
Filter() Filter
GlobalIgnores() ignorefiles.IgnoreFile
}
type ResultManager struct {
report_errors chan error
WakeupMainThread func() bool
settings Settings
scorer *FileSystemScorer
mutex sync.Mutex
last_wakeup_at time.Time
last_click_anchor *CollectionIndex
}
func NewResultManager(err_chan chan error, settings Settings, WakeupMainThread func() bool) *ResultManager {
ans := &ResultManager{
report_errors: err_chan,
settings: settings,
WakeupMainThread: WakeupMainThread,
}
return ans
}
func (m *ResultManager) new_scorer() {
root_dir := m.current_root_dir()
query := m.settings.SearchText()
m.scorer = NewFileSystemScorer(root_dir, query, m.settings.Filter(), m.settings.OnlyDirs(), m.on_results)
m.scorer.respect_ignores = m.settings.RespectIgnores()
m.scorer.show_hidden = m.settings.ShowHidden()
m.scorer.global_ignore = m.settings.GlobalIgnores()
m.last_click_anchor = nil
}
func (m *ResultManager) on_results(err error, is_finished bool) {
if err != nil {
m.report_errors <- err
m.WakeupMainThread()
return
}
m.mutex.Lock()
defer m.mutex.Unlock()
if is_finished || time.Since(m.last_wakeup_at) > time.Millisecond*50 {
m.WakeupMainThread()
m.last_wakeup_at = time.Now()
}
}
func (m *ResultManager) current_root_dir() string {
var err error
root_dir := m.settings.CurrentDir()
if root_dir == "" || root_dir == "." {
if root_dir, err = os.Getwd(); err != nil {
return "/"
}
}
root_dir = utils.Expanduser(root_dir)
if root_dir, err = filepath.Abs(root_dir); err != nil {
return "/"
}
return root_dir
}
func (m *ResultManager) set_root_dir() {
if m.scorer != nil {
m.scorer.Cancel()
}
_ = os.Chdir(m.current_root_dir()) // this is so the terminal emulator can read the wd for launch --directory=current
m.new_scorer()
m.mutex.Lock()
m.last_wakeup_at = time.Time{}
m.mutex.Unlock()
m.scorer.Start()
}
func (m *ResultManager) set_something(callback func()) {
m.mutex.Lock()
m.last_wakeup_at = time.Time{}
m.mutex.Unlock()
if m.scorer == nil {
m.new_scorer()
m.scorer.Start()
} else {
m.last_click_anchor = nil
callback()
}
}
func (m *ResultManager) set_query() {
m.set_something(func() { m.scorer.Change_query(m.settings.SearchText()) })
}
func (m *ResultManager) set_filter() {
m.set_something(func() { m.scorer.Change_filter(m.settings.Filter()) })
}
func (m *ResultManager) set_show_hidden() {
m.set_something(func() { m.scorer.Change_show_hidden(m.settings.ShowHidden()) })
}
func (m *ResultManager) set_respect_ignores() {
m.set_something(func() { m.scorer.Change_respect_ignores(m.settings.RespectIgnores()) })
}
func (m *ResultManager) set_sort_by_last_modified() {
m.set_something(func() { m.scorer.Change_sort_by_last_modified(m.settings.SortByLastModified()) })
}
func (h *Handler) get_results() (ans *SortedResults, is_complete bool) {
if h.result_manager.scorer == nil {
return
}
return h.result_manager.scorer.Results()
}

View File

@@ -1,371 +0,0 @@
package choose_files
import (
"fmt"
"io/fs"
"math/rand"
"os"
"strconv"
"strings"
"sync"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/kovidgoyal/kitty/tools/ignorefiles"
"github.com/kovidgoyal/kitty/tools/utils"
)
var _ = fmt.Print
func TestAsLower(t *testing.T) {
buf := [512]byte{}
for _, q := range []string{
"abc", "aBc", "aBCCf83Dx", "mOoseÇa", "89ÇĞxxA", "", "23", "aIİBc",
} {
n := as_lower(q, buf[:])
actual := utils.UnsafeBytesToString(buf[:n])
if diff := cmp.Diff(strings.ToLower(q), actual); diff != "" {
t.Fatalf("Failed to lowercase: %#v\n%s", q, diff)
}
}
}
type node struct {
name string
children map[string]*node
data string
}
func (n node) Name() string {
return n.name
}
func (n node) IsDir() bool {
return n.children != nil
}
func (n node) String() string {
return fmt.Sprintf("{name: %s num_children: %d}", n.name, len(n.children))
}
func (n node) Type() fs.FileMode {
if n.children == nil {
return 0
}
return fs.ModeDir
}
func (n node) Info() (fs.FileInfo, error) {
return nil, fmt.Errorf("Info() not implemented")
}
func random_name(r *rand.Rand) string {
length := 3 + r.Intn(23)
bytes := make([]byte, length)
for i := range length {
bytes[i] = byte(r.Intn(26) + 'a')
}
return string(bytes)
}
func (n *node) generate_random_tree(depth, breadth int) {
r := rand.New(rand.NewSource(111))
n.children = make(map[string]*node)
for range breadth {
c := &node{name: random_name(r)}
n.children[c.name] = c
if depth > 0 && r.Intn(10) < 3 {
c.generate_random_tree(depth-1, breadth)
}
}
}
func (n node) dir_entries() []fs.DirEntry {
entries := make([]fs.DirEntry, 0, len(n.children))
for _, v := range n.children {
entries = append(entries, v)
}
return entries
}
func (n node) ReadFile(name string) ([]byte, error) {
if name == string(os.PathSeparator) {
return nil, fs.ErrNotExist
}
p := &n
for _, x := range strings.Split(strings.Trim(name, string(os.PathSeparator)), string(os.PathSeparator)) {
c, found := p.children[x]
if !found {
return nil, fs.ErrNotExist
}
p = c
}
return []byte(p.data), nil
}
func (n node) ReadDir(name string) ([]fs.DirEntry, error) {
if name == string(os.PathSeparator) {
return n.dir_entries(), nil
}
p := &n
for _, x := range strings.Split(strings.Trim(name, string(os.PathSeparator)), string(os.PathSeparator)) {
c, found := p.children[x]
if !found {
return nil, fs.ErrNotExist
}
if !c.IsDir() {
return nil, fs.ErrExist
}
p = c
}
return p.dir_entries(), nil
}
func TestChooseFilesIgnore(t *testing.T) {
root := node{name: string(os.PathSeparator), children: map[string]*node{
"a": {name: "a"},
"b": {name: "b"},
"c.png": {name: "c.png"},
".ignore": {name: ".ignore", data: "a\nx/s/n"},
".gitignore": {name: ".gitignore", data: "b"},
"x": {name: "x", children: map[string]*node{
"1": {name: "1"}, "2": {name: "2"}, "3": {name: "3"},
"s": {name: "s", children: map[string]*node{
"m": {name: "m"}, "n": {name: "n"},
}},
}},
"y": {name: "y", children: map[string]*node{
"3": {name: "3"}, "4": {name: "4"}, "5": {name: "5"},
"s": {name: "s", children: map[string]*node{
"3": {name: "3"}, "4": {name: "4"}, "5": {name: "5"}, "6": {name: "6"},
}},
".gitignore": {name: ".gitignore", data: "/s/5"},
".git": {name: ".git", children: map[string]*node{
"info": {name: "info", children: map[string]*node{
"exclude": {name: "exclude", data: "s/4"},
}},
}},
}},
}}
r := func(respect_ignores bool, expected string) {
ch := make(chan bool)
s := new_filesystem_scanner("/", ch, nil)
s.dir_reader = root.ReadDir
s.file_reader = root.ReadFile
s.global_gitignore = ignorefiles.NewGitignore()
s.global_ignore = ignorefiles.NewGitignore()
s.respect_ignores = respect_ignores
if err := s.global_gitignore.LoadLines("*.png", "s/3"); err != nil {
t.Fatal(err)
}
if err := s.global_ignore.LoadLines("x/3"); err != nil {
t.Fatal(err)
}
s.Start()
for range ch {
}
if s.err != nil {
t.Fatal(s.err)
}
ci := CollectionIndex{}
actual := utils.Map(func(x ResultItem) string { return x.text }, s.Batch(&ci))
if diff := cmp.Diff(strings.Split(expected, ` `), actual); diff != "" {
t.Fatalf("Incorrect ignoring:\n%s", diff)
}
}
r(true, `x y b c.png x/s x/1 x/2 y/s y/3 y/4 y/5 x/s/m y/s/6`)
r(false, `x y a b c.png x/s x/1 x/2 x/3 y/s y/3 y/4 y/5 x/s/m x/s/n y/s/3 y/s/4 y/s/5 y/s/6`)
}
func TestChooseFilesScoring(t *testing.T) {
root := node{name: string(os.PathSeparator), children: map[string]*node{
"b": {name: "b"},
"a": {name: "a"},
"c.png": {name: "c.png"},
"x": {name: "x", children: map[string]*node{
"1": {name: "1"}, "2": {name: "2"}, "3": {name: "3"},
"s": {name: "s", children: map[string]*node{
"m": {name: "m"}, "n": {name: "n"},
}},
}},
"y": {name: "y", children: map[string]*node{
"3": {name: "3"}, "4": {name: "4"}, "5": {name: "5"},
}},
}}
wg := sync.WaitGroup{}
wg.Add(1)
s := NewFileSystemScorer(string(os.PathSeparator), "", Filter{}, false, func(err error, is_complete bool) {
if is_complete {
wg.Done()
}
})
s.dir_reader = root.ReadDir
s.global_gitignore = ignorefiles.NewGitignore()
s.Start()
wg.Wait()
results := func() (ans []string) {
rr, _ := s.Results()
for _, r := range rr.RenderedMatches(CollectionIndex{}, -1) {
ans = append(ans, r.text)
}
return
}
ae := func(query string, expected ...string) {
wg.Add(1)
s.Change_query(query)
wg.Wait()
if diff := cmp.Diff(expected, results()); diff != "" {
t.Fatalf("Query less scoring failed\n%s", diff)
}
}
ae("a", "a")
ae("3", "x/3", "y/3")
ae("s", "x/s", "x/s/m", "x/s/n")
ae("sn", "x/s/n")
ae("", "x", "y", "a", "b", "c.png", "x/s", "x/1", "x/2", "x/3", "y/3", "y/4", "y/5", "x/s/m", "x/s/n")
af := func(filter string, expected ...string) {
f, _ := NewFilter(filter)
wg.Add(1)
s.Change_filter(*f)
wg.Wait()
if diff := cmp.Diff(expected, results()); diff != "" {
t.Fatalf("filter %s failed\n%s", filter, diff)
}
}
af("glob:a:A", "x", "y", "a", "x/s")
af("glob:[ab]:A", "x", "y", "a", "b", "x/s")
af("mime:image/png:A", "x", "y", "c.png", "x/s")
af("mime:image/*:A", "x", "y", "c.png", "x/s")
af("glob:*:All", "x", "y", "a", "b", "c.png", "x/s", "x/1", "x/2", "x/3", "y/3", "y/4", "y/5", "x/s/m", "x/s/n")
}
func TestSortedResults(t *testing.T) {
r := NewSortedResults()
idx := CollectionIndex{}
m := func(items ...int) []*ResultItem {
ans := make([]*ResultItem, len(items))
for i, x := range items {
ans[i] = &ResultItem{text: strconv.Itoa(x), score: CombinedScore(x)}
}
return ans
}
v := func(slice, pos, num int) []int {
if num == 0 {
num = r.Len()
}
return utils.Map(func(r *ResultItem) int { return int(r.score) }, r.RenderedMatches(CollectionIndex{slice, pos}, num))
}
tv := func(slice, pos, num int, expected ...int) {
if diff := cmp.Diff(expected, v(slice, pos, num)); diff != "" {
t.Fatalf("view failed for %v num:%d\n%s", CollectionIndex{slice, pos}, num, diff)
}
}
tci := func(increment int, expected int) {
orig := idx
idx = r.IncrementIndexWithWrapAround(idx, increment)
actual := int(r.At(idx).score)
if actual != expected {
t.Fatalf("increment: %d on %v failed\nexpected: %d actual: %d idx: %v", increment, orig, expected, actual, idx)
}
}
dt := func(a, b CollectionIndex, expected int) {
actual := r.distance(a, b)
if expected != actual {
t.Fatalf("distance on %v and %v failed\nexpected: %d actual: %d ", a, b, expected, actual)
}
if r.distance(b, a) != actual {
t.Fatalf("distance on %v and %v not commutative %d != %d", a, b, actual, r.distance(b, a))
}
}
tc := func(num_before, expected_new_before int, ci CollectionIndex, expected ...[]int) {
ac, new_num_before, _ := r.SplitIntoColumns(func(int) int { return 2 }, 2, num_before, ci)
actual := make([][]int, len(ac))
for i, x := range ac {
actual[i] = utils.Map(func(r *ResultItem) int { return int(r.score) }, x)
}
if expected_new_before != new_num_before {
t.Fatalf("new_num_before not as expected for num_before: %d ci: %v\n%d != %d", num_before, ci, expected_new_before, new_num_before)
}
if diff := cmp.Diff(expected, actual); diff != "" {
t.Fatalf("wrong columns for num_before: %d ci: %v\n%s", num_before, ci, diff)
}
}
r.AddSortedSlice(m(10, 20, 30))
r.AddSortedSlice(m(40, 50, 60))
r.AddSortedSlice(m(70, 80, 90))
tc(0, 0, CollectionIndex{}, []int{10, 20}, []int{30, 40})
tc(1, 1, CollectionIndex{Pos: 1}, []int{10, 20}, []int{30, 40})
tc(1, 1, CollectionIndex{Pos: 2}, []int{20, 30}, []int{40, 50})
tc(2, 2, CollectionIndex{Pos: 2}, []int{10, 20}, []int{30, 40})
tc(20, 2, CollectionIndex{Pos: 2}, []int{10, 20}, []int{30, 40})
for num_before := range 4 {
tc(num_before, 3, CollectionIndex{2, 2}, []int{60, 70}, []int{80, 90})
}
tc(1, 1, CollectionIndex{1, 1}, []int{40, 50}, []int{60, 70})
dt(CollectionIndex{Pos: 0}, CollectionIndex{Pos: 2}, 2)
dt(CollectionIndex{Pos: 0}, CollectionIndex{Slice: 1}, 3)
dt(CollectionIndex{Pos: 0}, CollectionIndex{Slice: 1, Pos: 1}, 4)
tv(0, 0, 0, 10, 20, 30, 40, 50, 60, 70, 80, 90)
tv(0, 2, 3, 30, 40, 50)
tv(0, 3, 3, 40, 50, 60)
tv(1, 0, 4, 40, 50, 60, 70)
tci(1, 20)
tci(3, 50)
tci(-1, 40)
tci(-3, 10)
tci(-2, 80)
tci(3, 20)
tci(9, 20)
tci(-9, 20)
r.AddSortedSlice(m(100, 110, 120))
r.AddSortedSlice(m(41, 61, 71, 99))
tv(0, 0, 0, 10, 20, 30, 40, 41, 50, 60, 61, 70, 71, 80, 90, 99, 100, 110, 120)
r.AddSortedSlice(m(1, 2, 3))
tv(0, 0, 0, 1, 2, 3, 10, 20, 30, 40, 41, 50, 60, 61, 70, 71, 80, 90, 99, 100, 110, 120)
r.AddSortedSlice(m(1000, 2000))
tv(0, 0, 0, 1, 2, 3, 10, 20, 30, 40, 41, 50, 60, 61, 70, 71, 80, 90, 99, 100, 110, 120, 1000, 2000)
}
func run_scoring(b *testing.B, depth, breadth int, query string) {
b.StopTimer()
root := node{name: string(os.PathSeparator)}
root.generate_random_tree(depth, breadth)
b.StartTimer()
for range b.N {
b.StopTimer()
wg := sync.WaitGroup{}
wg.Add(1)
s := NewFileSystemScorer(string(os.PathSeparator), query, Filter{}, false, func(err error, is_complete bool) {
if is_complete {
wg.Done()
}
})
s.dir_reader = root.ReadDir
s.global_gitignore = ignorefiles.NewGitignore()
b.StartTimer()
s.scanner.Start()
s.Start()
wg.Wait()
}
fmt.Println("\nnumber of iterations: ", b.N)
fmt.Println("time per iteration:", b.Elapsed()/time.Duration(b.N))
}
// To run this benchmark with profiling use:
// go test -bench=FileNameScoringWithoutQuery -benchmem -cpuprofile=/tmp/cpu.prof -memprofile=/tmp/mem.prof github.com/kovidgoyal/kitty/kittens/choose_files -o /tmp/cfexe
func BenchmarkFileNameScoringWithoutQuery(b *testing.B) {
run_scoring(b, 5, 20, "")
}
// To run this benchmark with profiling use:
// go test -bench=FileNameScoringWithQuery -benchmem -cpuprofile=/tmp/cpu.prof -memprofile=/tmp/mem.prof github.com/kovidgoyal/kitty/kittens/choose_files -o /tmp/cfexe
func BenchmarkFileNameScoringWithQuery(b *testing.B) {
run_scoring(b, 5, 20, "abc")
}

View File

@@ -1,127 +0,0 @@
package choose_files
import (
"fmt"
"strconv"
"strings"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/wcswidth"
)
var _ = fmt.Print
func (h *Handler) draw_frame(width, height int, in_progress bool) {
lp := h.lp
prefix, suffix := "", ""
if in_progress {
x := h.lp.SprintStyled("fg=cyan", " ")
prefix, suffix, _ = strings.Cut(x, " ")
lp.QueueWriteString(prefix)
}
for i := range height {
lp.SaveCursorPosition()
switch i {
case 0:
lp.QueueWriteString("╭")
lp.QueueWriteString(strings.Repeat("─", width-2))
lp.QueueWriteString("╮")
case height - 1:
lp.QueueWriteString("╰")
lp.QueueWriteString(strings.Repeat("─", width-2))
lp.QueueWriteString("╯")
default:
lp.QueueWriteString("│")
lp.MoveCursorHorizontally(width - 2)
lp.QueueWriteString("│")
}
lp.RestoreCursorPosition()
lp.MoveCursorVertically(1)
}
if suffix != "" {
lp.QueueWriteString(suffix)
}
}
func (h *Handler) draw_search_text(available_width int) {
text := h.state.SearchText()
available_width /= 2
if wcswidth.Stringwidth(text) > available_width {
g := wcswidth.SplitIntoGraphemes(text)
available_width -= 2
g = g[len(g)-available_width:]
text = "…" + strings.Join(g, "")
}
h.lp.DrawSizedText(text+" ", loop.SizedText{Scale: 2})
h.lp.MoveCursorHorizontally(-2)
}
const SEARCH_BAR_HEIGHT = 4
func (h *Handler) draw_controls(y int) (max_width int) {
type entry struct {
text string
callback func()
width int
}
lines := make([]entry, 0, SEARCH_BAR_HEIGHT)
add_control := func(icon, text string, callback func()) {
line := icon + " " + text
width := wcswidth.Stringwidth(line)
max_width = max(max_width, width)
lines = append(lines, entry{line, callback, width})
}
add_control(utils.IfElse(h.state.ShowHidden(), " ", " "), utils.IfElse(h.state.ShowHidden(), "hide dotfiles", "show dotfiles"), func() {
h.state.show_hidden = !h.state.show_hidden
h.result_manager.set_show_hidden()
})
add_control("󰑑 ", utils.IfElse(h.state.RespectIgnores(), "show ignored", "hide ignored"), func() {
h.state.respect_ignores = !h.state.respect_ignores
h.result_manager.set_respect_ignores()
})
add_control(utils.IfElse(h.state.SortByLastModified(), " ", " "), utils.IfElse(h.state.SortByLastModified(), "sort names", "sort dates"), func() {
h.state.sort_by_last_modified = !h.state.sort_by_last_modified
h.result_manager.set_sort_by_last_modified()
})
x := h.screen_size.width - max_width
for i, e := range lines {
h.lp.MoveCursorTo(x+1, y+i+1)
h.lp.QueueWriteString(e.text)
cb := e.callback
h.state.mouse_state.AddCellRegion("rcontrol-"+strconv.Itoa(i), x, y+i, x+e.width, y+i, func(_ string) error {
cb()
h.state.redraw_needed = true
return nil
}).HoverStyle = HOVER_STYLE
}
return max_width + 1
}
func (h *Handler) draw_search_bar(y int) {
left_margin, right_margin := 0, h.draw_controls(y)
h.lp.MoveCursorTo(1+left_margin, 1+y)
available_width := h.screen_size.width - left_margin - right_margin
h.draw_frame(available_width, SEARCH_BAR_HEIGHT, false)
for y1 := y; y1 < y+4; y1++ {
cr := h.state.mouse_state.AddCellRegion("search-bar", left_margin, y1, left_margin+available_width, y1)
cr.PointerShape = loop.TEXT_POINTER
cr.HoverStyle = "none"
}
h.lp.MoveCursorTo(1+left_margin+1, 2+y)
h.draw_search_text(available_width - 2)
}
func (h *Handler) handle_edit_keys(ev *loop.KeyEvent) bool {
switch {
case ev.MatchesPressOrRepeat("backspace"):
if h.state.SearchText() == "" {
h.lp.Beep()
} else {
g := wcswidth.SplitIntoGraphemes(h.state.search_text)
h.set_query(strings.Join(g[:len(g)-1], ""))
return true
}
}
return false
}

View File

@@ -10,7 +10,7 @@ import (
"sync"
"time"
"github.com/kovidgoyal/kitty/tools/utils"
"kitty/tools/utils"
)
var _ = fmt.Print

View File

@@ -9,10 +9,10 @@ import (
"strings"
"sync"
"github.com/kovidgoyal/kitty/tools/tui"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/wcswidth"
"kitty/tools/tui"
"kitty/tools/tui/loop"
"kitty/tools/utils"
"kitty/tools/wcswidth"
)
var _ = fmt.Print

View File

@@ -5,8 +5,8 @@ import (
"math"
"sync"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/utils"
"kitty/tools/tui/loop"
"kitty/tools/utils"
)
var _ = fmt.Print

View File

@@ -2,10 +2,10 @@ package choose_fonts
import (
"fmt"
"github.com/kovidgoyal/kitty/tools/tui"
"github.com/kovidgoyal/kitty/tools/tui/subseq"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/wcswidth"
"kitty/tools/tui"
"kitty/tools/tui/subseq"
"kitty/tools/utils"
"kitty/tools/wcswidth"
)
var _ = fmt.Print

View File

@@ -5,9 +5,9 @@ import (
"path/filepath"
"strings"
"github.com/kovidgoyal/kitty/tools/config"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/utils"
"kitty/tools/config"
"kitty/tools/tui/loop"
"kitty/tools/utils"
)
var _ = fmt.Print

View File

@@ -4,9 +4,9 @@ import (
"fmt"
"strings"
"github.com/kovidgoyal/kitty/tools/tui/graphics"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/utils"
"kitty/tools/tui/graphics"
"kitty/tools/tui/loop"
"kitty/tools/utils"
)
var _ = fmt.Print
@@ -83,7 +83,7 @@ func (g *graphics_manager) display_image(slot int, path string, img_width, img_h
func (g *graphics_manager) on_response(gc *graphics.GraphicsCommand) (err error) {
if gc.ResponseMessage() != "OK" {
return fmt.Errorf("Failed to load image with error: %s\n\nNote that the choose-fonts kitten does not work over SSH as it is meant to select a locally available font to use in kitty.", gc.ResponseMessage())
return fmt.Errorf("Failed to load image with error: %s", gc.ResponseMessage())
}
for _, img := range g.images {
if img.image_number == gc.ImageNumber() {

View File

@@ -5,10 +5,10 @@ import (
"strconv"
"strings"
"github.com/kovidgoyal/kitty/tools/tui"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/tui/readline"
"github.com/kovidgoyal/kitty/tools/utils"
"kitty/tools/tui"
"kitty/tools/tui/loop"
"kitty/tools/tui/readline"
"kitty/tools/utils"
)
var _ = fmt.Print

View File

@@ -5,11 +5,11 @@ import (
"strings"
"sync"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/tui/readline"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/style"
"github.com/kovidgoyal/kitty/tools/wcswidth"
"kitty/tools/tui/loop"
"kitty/tools/tui/readline"
"kitty/tools/utils"
"kitty/tools/utils/style"
"kitty/tools/wcswidth"
)
var _ = fmt.Print

View File

@@ -4,9 +4,9 @@ import (
"fmt"
"os"
"github.com/kovidgoyal/kitty/tools/cli"
"github.com/kovidgoyal/kitty/tools/tty"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"kitty/tools/cli"
"kitty/tools/tty"
"kitty/tools/tui/loop"
)
var _ = fmt.Print

View File

@@ -2,7 +2,7 @@ package choose_fonts
import (
"fmt"
"github.com/kovidgoyal/kitty/tools/utils"
"kitty/tools/utils"
"slices"
"strings"
)

View File

@@ -7,8 +7,8 @@ import (
"strings"
"sync"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/shlex"
"kitty/tools/utils"
"kitty/tools/utils/shlex"
)
var _ = fmt.Print

View File

@@ -6,10 +6,10 @@ import (
"strconv"
"sync"
"github.com/kovidgoyal/kitty/tools/tui"
"github.com/kovidgoyal/kitty/tools/tui/graphics"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/utils"
"kitty/tools/tui"
"kitty/tools/tui/graphics"
"kitty/tools/tui/loop"
"kitty/tools/utils"
)
var _ = fmt.Print

View File

@@ -11,9 +11,9 @@ import (
"os"
"strings"
"github.com/kovidgoyal/kitty/tools/tty"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/utils"
"kitty/tools/tty"
"kitty/tools/tui/loop"
"kitty/tools/utils"
)
var _ = fmt.Print

View File

@@ -3,14 +3,9 @@
package clipboard
import (
"fmt"
"io"
"os"
"strconv"
"strings"
"unicode"
"github.com/kovidgoyal/kitty/tools/cli"
"kitty/tools/cli"
)
func run_mime_loop(opts *Options, args []string) (err error) {
@@ -25,47 +20,10 @@ func run_mime_loop(opts *Options, args []string) (err error) {
}
func clipboard_main(cmd *cli.Command, opts *Options, args []string) (rc int, err error) {
if opts.Password != "" {
if opts.HumanName == "" {
return 1, fmt.Errorf("must specify --human-name when using a password")
}
ptype, val, found := strings.Cut(opts.Password, ":")
if !found {
return 1, fmt.Errorf("invalid password: %#v no password type specified", opts.Password)
}
switch ptype {
case "text":
opts.Password = val
case "fd":
if fd, err := strconv.Atoi(val); err == nil {
if f := os.NewFile(uintptr(fd), "password-fd"); f == nil {
return 1, fmt.Errorf("invalid file descriptor: %d", fd)
} else {
data, err := io.ReadAll(f)
f.Close()
if err != nil {
return 1, fmt.Errorf("failed to read from file descriptor: %d with error: %w", fd, err)
}
opts.Password = strings.TrimRightFunc(string(data), unicode.IsSpace)
}
} else {
return 1, fmt.Errorf("not a valid file descriptor number: %#v", val)
}
case "file":
if data, err := os.ReadFile(val); err == nil {
opts.Password = strings.TrimRightFunc(string(data), unicode.IsSpace)
} else {
return 1, fmt.Errorf("failed to read from file: %#v with error: %w", val, err)
}
}
}
if len(args) > 0 {
return 0, run_mime_loop(opts, args)
}
if opts.Password != "" || opts.HumanName != "" {
return 1, fmt.Errorf("cannot use --human-name or --password in filter mode")
}
return 0, run_plain_text_loop(opts)
}

View File

@@ -44,18 +44,6 @@ other :code:`text/*` MIME is present.
type=bool-set
Wait till the copy to clipboard is complete before exiting. Useful if running
the kitten in a dedicated, ephemeral window. Only needed in filter mode.
--password
A password to use when accessing the clipboard. If the user chooses to accept the password
future invocations of the kitten will not have a permission prompt in this tty session. Does not
work in filter mode. Must be of the form: text:actual-password or fd:integer (a file descriptor
number to read the password from) or file:path-to-file (a file from which to read the password).
Note that you must also specify a human friendly name using the :option:`--human-name` flag.
--human-name
A human friendly name to show the user when asking for permission to access the clipboard.
'''.format
help_text = '''\
Read or write to the system clipboard.

View File

@@ -14,10 +14,10 @@ import (
"strings"
"sync"
"github.com/kovidgoyal/kitty/tools/tty"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/images"
"kitty/tools/tty"
"kitty/tools/tui/loop"
"kitty/tools/utils"
"kitty/tools/utils/images"
)
var _ = fmt.Print
@@ -201,7 +201,7 @@ func unescape_metadata_value(k, x string) (ans string) {
return x
}
func Encode_bytes(metadata map[string]string, payload []byte) string {
func encode_bytes(metadata map[string]string, payload []byte) string {
ans := strings.Builder{}
enc_payload := ""
if len(payload) > 0 {
@@ -228,7 +228,7 @@ func Encode_bytes(metadata map[string]string, payload []byte) string {
}
func encode(metadata map[string]string, payload string) string {
return Encode_bytes(metadata, utils.UnsafeStringToBytes(payload))
return encode_bytes(metadata, utils.UnsafeStringToBytes(payload))
}
func error_from_status(status string) error {
@@ -329,14 +329,9 @@ func run_get_loop(opts *Options, args []string) (err error) {
if opts.UsePrimary {
basic_metadata["loc"] = "primary"
}
lp.OnInitialize = func() (string, error) {
lp.QueueWriteString(encode(basic_metadata, "."))
if opts.Password != "" {
basic_metadata["pw"] = base64.StdEncoding.EncodeToString(utils.UnsafeStringToBytes(opts.Password))
}
if opts.HumanName != "" {
basic_metadata["name"] = base64.StdEncoding.EncodeToString(utils.UnsafeStringToBytes(opts.HumanName))
}
return "", nil
}

View File

@@ -3,17 +3,15 @@
package clipboard
import (
"encoding/base64"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"slices"
"strings"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/utils"
"kitty/tools/tui/loop"
"kitty/tools/utils"
)
var _ = fmt.Print
@@ -39,7 +37,12 @@ func (self *Input) has_mime_matching(predicate func(string) bool) bool {
if predicate(self.mime_type) {
return true
}
return slices.ContainsFunc(self.extra_mime_types, predicate)
for _, i := range self.extra_mime_types {
if predicate(i) {
return true
}
}
return false
}
func write_loop(inputs []*Input, opts *Options) (err error) {
@@ -81,14 +84,6 @@ func write_loop(inputs []*Input, opts *Options) (err error) {
if mime != "" {
ans["mime"] = mime
}
if ptype == "write" {
if opts.Password != "" {
ans["pw"] = base64.StdEncoding.EncodeToString(utils.UnsafeStringToBytes(opts.Password))
}
if opts.HumanName != "" {
ans["name"] = base64.StdEncoding.EncodeToString(utils.UnsafeStringToBytes(opts.HumanName))
}
}
return ans
}
@@ -104,7 +99,7 @@ func write_loop(inputs []*Input, opts *Options) (err error) {
i := inputs[0]
n, err := i.src.Read(buf[:])
if n > 0 {
waiting_for_write = lp.QueueWriteString(Encode_bytes(make_metadata("wdata", i.mime_type), buf[:n]))
waiting_for_write = lp.QueueWriteString(encode_bytes(make_metadata("wdata", i.mime_type), buf[:n]))
}
if err != nil {
if errors.Is(err, io.EOF) {

View File

@@ -1,206 +0,0 @@
package desktop_ui
import (
"fmt"
"strings"
"github.com/kovidgoyal/dbus"
"github.com/kovidgoyal/kitty"
"github.com/kovidgoyal/kitty/tools/cli"
"github.com/kovidgoyal/kitty/tools/config"
"github.com/kovidgoyal/kitty/tools/utils"
)
var _ = fmt.Print
type ServerOptions struct {
Config []string
Override []string
}
const server_conf_name = "desktop-ui-portal"
func load_server_config(opts *ServerOptions) (ans *Config, err error) {
ans = NewConfig()
p := config.ConfigParser{LineHandler: ans.Parse}
err = p.LoadConfig(server_conf_name+".conf", opts.Config, opts.Override)
return
}
func run_server(opts *ServerOptions) (err error) {
config, err := load_server_config(opts)
if err == nil {
portal, err := NewPortal(config)
if err == nil {
err = portal.Start()
if err == nil {
defer portal.Cleanup()
}
}
}
if err != nil {
return
}
c := make(chan string)
<-c
return
}
func specialize_command(parent *cli.Command) {
parent.Run = func(cmd *cli.Command, args []string) (int, error) {
cmd.ShowHelp()
return 1, nil
}
parent.ShortDescription = "Implement various desktop components for use with lightweight compositors/window managers on Linux"
rs := parent.AddSubCommand(&cli.Command{
Name: "run-server",
ShortDescription: "Start the various servers used to integrate with the Linux desktop",
HelpText: "This should be run very early in the startup sequence of your window manager, before any other programs are run.",
Run: func(cmd *cli.Command, args []string) (rc int, err error) {
opts := ServerOptions{}
err = cmd.GetOptionValues(&opts)
if err == nil {
err = run_server(&opts)
}
return utils.IfElse(err == nil, 0, 1), err
},
})
rs.Add(cli.OptionSpec{
Name: `--override -o`, Type: "list", Dest: `Override`,
Help: "Override individual configuration options, can be specified multiple times. Syntax: :italic:`name=value`. For example: :italic:`-o color_scheme=dark`",
})
rs.Add(cli.OptionSpec{
Name: `--config -c`, Type: "list", Dest: `Config`,
Help: strings.ReplaceAll(strings.ReplaceAll(kitty.ConfigHelp, "{appname}", "kitty"), "{conf_name}", server_conf_name),
})
parent.AddSubCommand(&cli.Command{
Name: "enable-portal",
ShortDescription: "This will create or edit the various files needed so that the portal from this kitten is used by xdg-desktop-portal",
HelpText: "Once you run this command, add :code:`kitten desktop-ui run-server` to your window manager startup sequence and reboot your computer (or logout and restart your session) and hopefully xdg-desktop-portal should now delegate to kitty for the portals implemented here. If it doesn't try running :code:`/usr/lib/xdg-desktop-portal -r -v` it will provide a lot of logging about why it is choosing different portal backends. That combined with a careful reading of :code:`man portals.conf` should be enough to learn how to convince xdg-desktop-portal to use kitty.\n\nYou can change the system color-scheme dynamically by running::\n\n:code:`kitten desktop-ui set-color-scheme dark`",
Run: func(cmd *cli.Command, args []string) (rc int, err error) {
err = enable_portal()
return utils.IfElse(err == nil, 0, 1), err
},
})
parent.AddSubCommand(&cli.Command{
Name: "set-color-scheme",
ShortDescription: "Change the color scheme",
ArgCompleter: cli.NamesCompleter("Choices for color-scheme", "no-preference", "light", "dark", "toggle"),
Usage: " light|dark|no-preference|toggle",
Run: func(cmd *cli.Command, args []string) (rc int, err error) {
if len(args) != 1 {
cmd.ShowHelp()
return 1, fmt.Errorf("must specify the new color scheme value")
}
err = set_color_scheme(args[0])
return utils.IfElse(err == nil, 0, 1), err
},
})
parent.AddSubCommand(&cli.Command{
Name: "set-accent-color",
ShortDescription: "Change the accent color",
Usage: " color_as_hex_or_name",
Run: func(cmd *cli.Command, args []string) (rc int, err error) {
if len(args) != 1 {
cmd.ShowHelp()
return 1, fmt.Errorf("must specify the new accent color value")
}
var v dbus.Variant
if v, err = to_color(args[0]); err == nil {
err = set_variant_setting(PORTAL_APPEARANCE_NAMESPACE, PORTAL_ACCENT_COLOR_KEY, v, false)
}
return utils.IfElse(err == nil, 0, 1), err
},
})
parent.AddSubCommand(&cli.Command{
Name: "set-contrast",
ShortDescription: "Change the contrast. Can be high or normal.",
Usage: " high|normal",
Run: func(cmd *cli.Command, args []string) (rc int, err error) {
if len(args) != 1 {
cmd.ShowHelp()
return 1, fmt.Errorf("must specify the new contrast value")
}
var v dbus.Variant
switch args[0] {
case "normal":
v = dbus.MakeVariant(uint32(0))
case "high":
v = dbus.MakeVariant(uint32(1))
default:
return 1, fmt.Errorf("%s is not a valid contrast value", args[0])
}
err = set_variant_setting(PORTAL_APPEARANCE_NAMESPACE, PORTAL_CONTRAST_KEY, v, false)
return utils.IfElse(err == nil, 0, 1), err
},
})
st := parent.AddSubCommand(&cli.Command{
Name: "set-setting",
ShortDescription: "Change an arbitrary setting",
Usage: " key [value]",
HelpText: "Set an arbitrary setting. If you want to set the color-scheme use the dedicated command for it. Use this command with care as it does no validation for the type of value. The syntax for specifying values is described at: :link:`the glib docs <https://docs.gtk.org/glib/gvariant-text-format.html>`. Leaving out the value or specifying an empty value, will delete the setting.",
Run: func(cmd *cli.Command, args []string) (rc int, err error) {
val := ""
if len(args) < 1 {
cmd.ShowHelp()
return 1, fmt.Errorf("must specify the key")
}
if len(args) > 1 {
val = args[1]
}
opts := SetOptions{}
if err = cmd.GetOptionValues(&opts); err == nil {
err = set_setting(args[0], val, &opts)
}
return utils.IfElse(err == nil, 0, 1), err
},
})
st.Add(cli.OptionSpec{
Name: "--namespace -n",
Help: "The namespace in which to change the setting.",
Default: PORTAL_APPEARANCE_NAMESPACE,
})
st.Add(cli.OptionSpec{
Name: "--data-type",
Help: "The DBUS data type signature of the value. The default is to guess from the textual representation, see :link:`the glib docs <https://docs.gtk.org/glib/gvariant-text-format.html>` for details.",
})
ss := parent.AddSubCommand(&cli.Command{
Name: "show-settings",
ShortDescription: "Print the current values of the desktop settings",
Run: func(cmd *cli.Command, args []string) (rc int, err error) {
if len(args) != 0 {
cmd.ShowHelp()
return 1, fmt.Errorf("no arguments allowed")
}
opts := ShowSettingsOptions{}
err = cmd.GetOptionValues(&opts)
if err == nil {
err = show_settings(&opts)
}
return utils.IfElse(err == nil, 0, 1), err
},
})
ss.Add(cli.OptionSpec{
Name: "--as-json",
Help: "Show the settings as JSON for machine consumption",
Type: "bool-set",
})
ss.Add(cli.OptionSpec{
Name: "--in-namespace",
Help: "Show only settings in the specified names. Can be specified multiple times. When unspecified all namespaces are returned.",
Type: "list",
})
ss.Add(cli.OptionSpec{
Name: "--allow-other-backends",
Help: "Normally, after printing the settings, if the settings did not come from the desktop-ui kitten the command prints an error and exits. This prevents that.",
Type: "bool-set",
})
}
func EntryPoint(root *cli.Command) {
create_cmd(root, nil)
}

View File

@@ -1,44 +0,0 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2025, Kovid Goyal <kovid at kovidgoyal.net>
import sys
from kitty.conf.types import Definition
definition = Definition(
'!kittens.choose_files',
)
agr = definition.add_group
egr = definition.end_group
opt = definition.add_option
map = definition.add_map
mma = definition.add_mouse_map
agr('Appearance')
opt('color_scheme', 'no-preference', choices=('no-preference', 'dark', 'light'), long_text='''\
The color scheme for your system. This sets the initial value of the color scheme. It can be changed subsequently
by using :code:`kitten desktop-ui color-scheme`.
''')
opt('accent_color', 'cyan', long_text='The RGB accent color for your system, can be specified as a color name or in hex a decimal format.')
opt('contrast', 'normal', choices=('normal', 'high'), long_text='The preferred contrast level.')
opt('+file_chooser_kitty_conf', '',
long_text='Path to config file to use for kitty when drawing the file chooser window. Can be specified multiple times. By default, the'
' normal kitty.conf is used. Relative paths are resolved with respect to the kitty config directory.'
)
opt('+file_chooser_kitty_override', '', long_text='Override individual kitty configuration options, for the file chooser window.'
' Can be specified multiple times. Syntax: :italic:`name=value`. For example: :code:`font_size=20`.'
)
egr()
def main(args: list[str]) -> None:
raise SystemExit('This must be run as kitten desktop-ui')
if __name__ == '__main__':
main(sys.argv)
elif __name__ == '__conf__':
sys.options_definition = definition # type: ignore

View File

@@ -1,903 +0,0 @@
package desktop_ui
import (
"bytes"
"encoding/json"
"fmt"
"log"
"maps"
"net/url"
"os"
"os/exec"
"path/filepath"
"runtime"
"slices"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/kovidgoyal/dbus"
"github.com/kovidgoyal/dbus/introspect"
"github.com/kovidgoyal/dbus/prop"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/style"
"golang.org/x/sys/unix"
)
var _ = fmt.Print
const PORTAL_APPEARANCE_NAMESPACE = "org.freedesktop.appearance"
const PORTAL_COLOR_SCHEME_KEY = "color-scheme"
const PORTAL_ACCENT_COLOR_KEY = "accent-color"
const PORTAL_CONTRAST_KEY = "contrast"
const PORTAL_BUS_NAME = "org.freedesktop.impl.portal.desktop.kitty"
const DESKTOP_OBJECT_PATH = "/org/freedesktop/portal/desktop"
const SETTINGS_INTERFACE = "org.freedesktop.impl.portal.Settings"
const FILE_CHOOSER_INTERFACE = "org.freedesktop.impl.portal.FileChooser"
const KITTY_OBJECT_PATH = "/net/kovidgoyal/kitty/portal"
const CHANGE_SETTINGS_INTERFACE = "net.kovidgoyal.kitty.settings"
const DESKTOP_PORTAL_NAME = "org.freedesktop.portal.Desktop"
const REQUEST_INTERFACE = "org.freedesktop.impl.portal.Request"
// Special portal setting used to check if we are being called by xdg-desktop-portal
const SETTINGS_CANARY_NAMESPACE = "net.kovidgoyal.kitty"
const SETTINGS_CANARY_KEY = "status"
type ColorScheme uint32
const (
NO_PREFERENCE ColorScheme = iota
DARK
LIGHT
)
const (
RESPONSE_SUCCESS uint32 = iota
RESPONSE_CANCELED
RESPONSE_ENDED
)
type SettingsMap map[string]map[string]dbus.Variant
type Portal struct {
bus *dbus.Conn
settings SettingsMap
lock sync.Mutex
opts *Config
file_chooser_first_instance *exec.Cmd
}
func to_color(spec string) (v dbus.Variant, err error) {
if col, err := style.ParseColor(spec); err == nil {
return dbus.MakeVariant([]float64{float64(col.Red) / 255., float64(col.Green) / 255., float64(col.Blue) / 255.}), nil
}
return
}
func NewPortal(opts *Config) (p *Portal, err error) {
ans := Portal{opts: opts}
ans.settings = SettingsMap{
SETTINGS_CANARY_NAMESPACE: map[string]dbus.Variant{
SETTINGS_CANARY_KEY: dbus.MakeVariant("running"),
},
}
ans.settings[PORTAL_APPEARANCE_NAMESPACE] = map[string]dbus.Variant{}
switch opts.Color_scheme {
case Color_scheme_dark:
ans.settings[PORTAL_APPEARANCE_NAMESPACE][PORTAL_COLOR_SCHEME_KEY] = dbus.MakeVariant(uint32(DARK))
case Color_scheme_light:
ans.settings[PORTAL_APPEARANCE_NAMESPACE][PORTAL_COLOR_SCHEME_KEY] = dbus.MakeVariant(uint32(LIGHT))
default:
ans.settings[PORTAL_APPEARANCE_NAMESPACE][PORTAL_COLOR_SCHEME_KEY] = dbus.MakeVariant(uint32(NO_PREFERENCE))
}
ans.settings[PORTAL_APPEARANCE_NAMESPACE][PORTAL_ACCENT_COLOR_KEY], err = to_color(opts.Accent_color)
var contrast uint32
if opts.Contrast == Contrast_high {
contrast = 1
}
ans.settings[PORTAL_APPEARANCE_NAMESPACE][PORTAL_CONTRAST_KEY] = dbus.MakeVariant(contrast)
return &ans, nil
}
type PropSpec map[string]*prop.Prop
type SignalSpec map[string][]struct {
Name, Type string
}
type MethodSpec map[string][]struct {
Name, Type string
Out bool
}
func ExportInterface(conn *dbus.Conn, object any, interface_name, object_path string, method_spec MethodSpec, prop_spec PropSpec, signal_spec SignalSpec) (err error) {
op := dbus.ObjectPath(object_path)
method_map := make(map[string]string, len(method_spec))
methods := []introspect.Method{}
if len(method_spec) > 0 {
for method_name, args := range method_spec {
method_map[method_name] = method_name
meth_args := make([]introspect.Arg, len(args))
for i, a := range args {
meth_args[i] = introspect.Arg{
Name: a.Name,
Type: a.Type,
Direction: utils.IfElse(a.Out, "out", "in"),
}
}
methods = append(methods, introspect.Method{
Name: method_name,
Args: meth_args,
})
}
}
if err = conn.ExportWithMap(object, method_map, op, interface_name); err != nil {
return fmt.Errorf("failed to export interface: %s at object path: %s with error: %w", interface_name, object_path, err)
}
var properties []introspect.Property
p := prop.Map{interface_name: prop_spec}
if len(prop_spec) > 0 {
if props, err := prop.Export(conn, op, p); err != nil {
return fmt.Errorf("failed to export properties with error: %w", err)
} else {
properties = props.Introspection(interface_name)
}
}
var signals []introspect.Signal
if len(signal_spec) > 0 {
for signal_name, args := range signal_spec {
sig_args := make([]introspect.Arg, len(args))
for i, a := range args {
sig_args[i] = introspect.Arg{
Name: a.Name,
Type: a.Type,
Direction: "out",
}
}
signals = append(signals, introspect.Signal{
Name: signal_name,
Args: sig_args,
})
}
}
interface_data := introspect.Interface{
Name: interface_name,
Methods: methods,
Properties: properties,
Signals: signals,
}
interfaces := []introspect.Interface{
introspect.IntrospectData, interface_data,
}
if len(properties) > 0 {
interfaces = append(interfaces, prop.IntrospectData)
}
n := &introspect.Node{Name: object_path, Interfaces: interfaces}
if err = conn.Export(introspect.NewIntrospectable(n), op, introspect.IntrospectData.Name); err != nil {
return fmt.Errorf("failed to export introspected methods with error: %w", err)
}
return
}
func (self *Portal) Start() (err error) {
if self.bus, err = dbus.SessionBus(); err != nil {
return fmt.Errorf("could not connect to session D-Bus: %s", err)
}
reply, err := self.bus.RequestName(PORTAL_BUS_NAME, dbus.NameFlagDoNotQueue)
if err != nil {
return fmt.Errorf("failed to register dbus name: %v", err)
}
if reply != dbus.RequestNameReplyPrimaryOwner {
return fmt.Errorf("can't register D-Bus name: name already taken")
}
props := PropSpec{
"version": {Value: uint32(1), Writable: false, Emit: prop.EmitFalse},
}
signals := SignalSpec{
"SettingChanged": {{"namespace", "s"}, {"key", "s"}, {"value", "v"}},
}
methods := MethodSpec{
"Read": {{"namespace", "s", false}, {"key", "s", false}, {"value", "v", true}},
"ReadAll": {{"namespaces", "as", false}, {"value", "a{sa{sv}}", true}},
}
if err = ExportInterface(self.bus, self, SETTINGS_INTERFACE, DESKTOP_OBJECT_PATH, methods, props, signals); err != nil {
return
}
methods = MethodSpec{
"OpenFile": {{"handle", "o", false}, {"app_id", "s", false}, {"parent_window", "s", false}, {"title", "s", false}, {"options", "a{sv}", false},
{"response", "u", true}, {"results", "a{sv}", false},
},
"SaveFile": {{"handle", "o", false}, {"app_id", "s", false}, {"parent_window", "s", false}, {"title", "s", false}, {"options", "a{sv}", false},
{"response", "u", true}, {"results", "a{sv}", false},
},
}
if err = ExportInterface(self.bus, self, FILE_CHOOSER_INTERFACE, DESKTOP_OBJECT_PATH, methods, nil, nil); err != nil {
return
}
methods = MethodSpec{
"ChangeSetting": {{"namespace", "s", false}, {"key", "s", false}, {"value", "v", false}},
"RemoveSetting": {{"namespace", "s", false}, {"key", "s", false}},
}
props["version"].Value = uint32(1)
if err = ExportInterface(self.bus, self, CHANGE_SETTINGS_INTERFACE, KITTY_OBJECT_PATH, methods, props, nil); err != nil {
return
}
return
}
func ParseValueWithSignature(value, value_type_signature string) (v dbus.Variant, err error) {
var s dbus.Signature
if value_type_signature != "" {
if value_type_signature[0] == '@' {
value_type_signature = value_type_signature[1:]
}
s, err = dbus.ParseSignature(value_type_signature)
if err != nil {
return dbus.Variant{}, fmt.Errorf("%s is not a valid type signature: %w", value_type_signature, err)
}
}
v, err = dbus.ParseVariant(value, s)
if err != nil {
if value_type_signature == "" {
return dbus.Variant{}, fmt.Errorf("could not guess the data type of: %s with error: %w", value, err)
}
return dbus.Variant{}, fmt.Errorf("%s is not a valid value for signature: %#v with error: %w", value, value_type_signature, err)
}
return v, nil
}
func ParseValue(value string) (dbus.Variant, error) {
return ParseValueWithSignature(value, "")
}
type ShowSettingsOptions struct {
AsJson bool
AllowOtherBackends bool
InNamespace []string
}
func fetch_settings(conn *dbus.Conn, namespaces ...string) (ans ReadAllType, err error) {
path := "/" + strings.ToLower(strings.ReplaceAll(DESKTOP_PORTAL_NAME, ".", "/"))
obj := conn.Object(DESKTOP_PORTAL_NAME, dbus.ObjectPath(path))
interface_name := strings.ReplaceAll(DESKTOP_PORTAL_NAME, "Desktop", "Settings")
if len(namespaces) == 0 {
namespaces = append(namespaces, "")
}
call := obj.Call(interface_name+".ReadAll", dbus.FlagNoAutoStart, namespaces)
if err = call.Store(&ans); err != nil {
return nil, fmt.Errorf("Failed to read response from ReadAll with error: %w", err)
}
return
}
func show_settings(opts *ShowSettingsOptions) (err error) {
conn, err := dbus.SessionBus()
if err != nil {
return fmt.Errorf("failed to connect to system bus with error: %w", err)
}
defer conn.Close()
var response ReadAllType
response, err = fetch_settings(conn, opts.InNamespace...)
if opts.AsJson {
unwrapped := make(map[string]map[string]any, len(response))
for ns, m := range response {
w := make(map[string]any, len(m))
for k, a := range m {
w[k] = a.Value()
}
unwrapped[ns] = w
}
j, err := json.MarshalIndent(unwrapped, "", " ")
if err != nil {
return fmt.Errorf("Failed to format the response as JSON: %w", err)
}
fmt.Println(string(j))
} else {
for ns, m := range response {
fmt.Println(ns + ":")
for key, v := range m {
fmt.Printf("\t%s: %s\n", key, v)
}
}
}
if !opts.AllowOtherBackends {
is_running_self := false
if m, found := response[SETTINGS_CANARY_NAMESPACE]; found {
_, is_running_self = m[SETTINGS_CANARY_KEY]
}
if !is_running_self {
err = fmt.Errorf("the settings did not come from the desktop-ui kitten. Some other portal backend is providing the service.")
}
}
return
}
var DataDirs = sync.OnceValue(func() (ans []string) {
d := os.Getenv("XDG_DATA_DIRS")
if d == "" {
d = "/usr/local/share/:/usr/share/"
}
all := []string{os.Getenv("XDG_DATA_HOME")}
all = append(all, strings.Split(d, ":")...)
seen := map[string]bool{}
for _, x := range all {
if !seen[x] {
seen[x] = true
ans = append(ans, x)
}
}
return
})
func IsDir(x string) bool {
s, err := os.Stat(x)
return err == nil && s.IsDir()
}
var WritableDataDirs = sync.OnceValue(func() (ans []string) {
for _, x := range DataDirs() {
if err := os.MkdirAll(x, 0o755); err == nil && unix.Access(x, unix.W_OK) == nil {
ans = append(ans, x)
}
}
return
})
var AllPortalInterfaces = sync.OnceValue(func() (ans []string) {
return []string{SETTINGS_INTERFACE, FILE_CHOOSER_INTERFACE}
})
// enable-portal {{{
func patch_portals_conf(text []byte) []byte {
lines := []string{}
in_preferred := false
for _, line := range utils.Splitlines(utils.UnsafeBytesToString(text)) {
sl := strings.TrimSpace(line)
if strings.HasPrefix(sl, "[") {
in_preferred = sl == "[preferred]"
lines = append(lines, line)
for _, iface := range AllPortalInterfaces() {
lines = append(lines, iface+"=kitty")
}
} else if in_preferred {
remove := false
for _, iface := range AllPortalInterfaces() {
if strings.HasPrefix(sl, iface) {
remove = true
break
}
}
if !remove {
lines = append(lines, line)
}
}
}
return utils.UnsafeStringToBytes(strings.Join(lines, "\n"))
}
func enable_portal() (err error) {
if len(WritableDataDirs()) == 0 {
return fmt.Errorf("Could not find any writable data directories. Make sure XDG_DATA_DIRS is set and contains at least one directory for which you have write permission")
}
portals_dir := ""
for _, x := range WritableDataDirs() {
q := filepath.Join(x, "xdg-desktop-portal", "portals")
if unix.Access(q, unix.W_OK) == nil && IsDir(q) {
portals_dir = q
break
}
}
if portals_dir == "" {
for _, x := range WritableDataDirs() {
q := filepath.Join(x, "xdg-desktop-portal", "portals")
if err := os.MkdirAll(q, 0o755); err == nil {
portals_dir = q
break
}
}
}
if portals_dir == "" {
return fmt.Errorf("Could not find any writable portals directories. Make sure XDG_DATA_HOME is set and point to a directory for which you have write permission.")
}
portals_defn := filepath.Join(portals_dir, "kitty.portal")
if err = os.WriteFile(portals_defn, utils.UnsafeStringToBytes(fmt.Sprintf(
`[portal]
DBusName=%s
Interfaces=%s;
`, PORTAL_BUS_NAME, strings.Join(AllPortalInterfaces(), ";"))), 0o644); err != nil {
return err
}
fmt.Println("Wrote kitty portal definition to:", portals_defn)
dbus_service_dir := ""
for _, x := range WritableDataDirs() {
q := filepath.Join(x, "dbus-1", "services")
if err := os.MkdirAll(q, 0o755); err == nil {
dbus_service_dir = q
break
}
}
if dbus_service_dir == "" {
return fmt.Errorf("Could not find any writable portals directories. Make sure XDG_DATA_HOME is set and point to a directory for which you have write permission.")
}
dbus_service_defn := filepath.Join(dbus_service_dir, PORTAL_BUS_NAME+".service")
exe_path, eerr := os.Executable()
if eerr != nil {
exe_path = utils.Which("kitten")
}
if exe_path, err = filepath.Abs(exe_path); eerr != nil {
return fmt.Errorf("failed to get path to kitten executable with error: %w", err)
}
if err = os.WriteFile(dbus_service_defn, utils.UnsafeStringToBytes(fmt.Sprintf(
`[D-BUS Service]
Name=%s
Exec=%s desktop-ui run-server
`, PORTAL_BUS_NAME, exe_path)), 0o644); err != nil {
return err
}
fmt.Println("Wrote kitty DBUS activation service file to:", dbus_service_defn)
d := os.Getenv("XDG_CURRENT_DESKTOP")
cf := os.Getenv("XDG_CONFIG_HOME")
if cf == "" {
cf = utils.Expanduser("~/.config")
}
cf = filepath.Join(cf, "xdg-desktop-portal")
if err = os.MkdirAll(cf, 0o755); err != nil {
return fmt.Errorf("failed to create %s to store the portals.conf file with error: %w", cf, err)
}
patched_file := ""
desktops := utils.Filter(strings.Split(d, ":"), func(x string) bool { return x != "" })
desktops = append(desktops, "")
for _, x := range strings.Split(d, ":") {
q := filepath.Join(cf, utils.IfElse(x == "", "portals.conf", fmt.Sprintf("%s-portals.conf", strings.ToLower(x))))
if text, err := os.ReadFile(q); err == nil {
text := patch_portals_conf(text)
if err = os.WriteFile(q, text, 0o644); err == nil {
patched_file = q
break
}
}
}
if patched_file == "" {
x := desktops[0]
q := filepath.Join(cf, utils.IfElse(x == "", "portals.conf", fmt.Sprintf("%s-portals.conf", strings.ToLower(x))))
text := patch_portals_conf([]byte{})
if err = os.WriteFile(q, text, 0o644); err != nil {
return err
}
patched_file = q
}
fmt.Printf("Patched %s to use the kitty portals\n", patched_file)
return
}
// }}}
type SetOptions struct {
Namespace, DataType string
}
func set_variant_setting(namespace, key string, v dbus.Variant, remove_setting bool) (err error) {
conn, err := dbus.SessionBus()
if err != nil {
return fmt.Errorf("failed to connect to system bus with error: %w", err)
}
defer conn.Close()
method := "ChangeSetting"
var vals = []any{namespace, key}
if remove_setting {
method = "RemoveSetting"
} else {
vals = append(vals, v)
}
obj := conn.Object(PORTAL_BUS_NAME, dbus.ObjectPath(KITTY_OBJECT_PATH))
call := obj.Call(CHANGE_SETTINGS_INTERFACE+"."+method, dbus.FlagNoAutoStart, vals...)
if err = call.Store(); err != nil {
return fmt.Errorf("failed to call %s with error: %w", method, err)
}
return
}
func set_setting(key, value string, opts *SetOptions) (err error) {
remove_setting := false
var v dbus.Variant
if value == "" {
remove_setting = true
} else {
if v, err = ParseValueWithSignature(value, opts.DataType); err != nil {
return err
}
}
return set_variant_setting(opts.Namespace, key, v, remove_setting)
}
func set_color_scheme(which string) (err error) {
conn, err := dbus.SessionBus()
if err != nil {
return fmt.Errorf("failed to connect to system bus with error: %w", err)
}
defer conn.Close()
val := NO_PREFERENCE
var res ReadAllType
if res, err = fetch_settings(conn, PORTAL_APPEARANCE_NAMESPACE); err != nil {
return fmt.Errorf("failed to read existing color scheme setting with error: %w", err)
}
if m, found := res[PORTAL_APPEARANCE_NAMESPACE]; found {
if v, found := m[PORTAL_COLOR_SCHEME_KEY]; found {
v.Store(&val)
}
}
nval := val
switch which {
case "toggle":
switch val {
case LIGHT:
nval = DARK
case DARK:
nval = LIGHT
}
case "no-preference":
nval = NO_PREFERENCE
case "light":
nval = LIGHT
case "dark":
nval = DARK
default:
return fmt.Errorf("%s is not a valid value of the color-scheme", which)
}
if val == nval {
return
}
obj := conn.Object(PORTAL_BUS_NAME, dbus.ObjectPath(KITTY_OBJECT_PATH))
call := obj.Call(CHANGE_SETTINGS_INTERFACE+".ChangeSetting", dbus.FlagNoAutoStart, PORTAL_APPEARANCE_NAMESPACE, PORTAL_COLOR_SCHEME_KEY, dbus.MakeVariant(nval))
if err = call.Store(); err != nil {
return fmt.Errorf("failed to call ChangeSetting with error: %w", err)
}
return
}
func (self *Portal) ChangeSetting(namespace, key string, value dbus.Variant) *dbus.Error {
self.lock.Lock()
defer self.lock.Unlock()
if self.settings[namespace] == nil {
self.settings[namespace] = map[string]dbus.Variant{}
}
self.settings[namespace][key] = value
if e := self.bus.Emit(
DESKTOP_OBJECT_PATH,
SETTINGS_INTERFACE+".SettingChanged",
namespace,
key,
value,
); e != nil {
log.Println("Couldn't emit signal:", e)
}
return nil
}
func (self *Portal) RemoveSetting(namespace, key string) *dbus.Error {
self.lock.Lock()
defer self.lock.Unlock()
existed := false
if m := self.settings[namespace]; m != nil {
_, existed = m[key]
}
if !existed {
return nil
}
delete(self.settings[namespace], key)
return nil
}
func (self *Portal) Read(namespace, key string) (dbus.Variant, *dbus.Error) {
self.lock.Lock()
defer self.lock.Unlock()
if m, found := self.settings[namespace]; found {
if v, found := m[key]; found {
return v, nil
}
}
return dbus.Variant{}, dbus.NewError("org.freedesktop.portal.Error.NotFound", []any{fmt.Sprintf("the setting %s in the namespace %s is not supported", key, namespace)})
}
type ReadAllType map[string]map[string]dbus.Variant
func (self *Portal) ReadAll(namespaces []string) (ReadAllType, *dbus.Error) {
self.lock.Lock()
defer self.lock.Unlock()
var matched_namespaces = SettingsMap{}
if len(namespaces) == 0 {
matched_namespaces = self.settings
} else {
for _, namespace := range namespaces {
if namespace == "" {
matched_namespaces = self.settings
break
} else {
if strings.HasSuffix(namespace, ".*") {
namespace = namespace[:len(namespace)-1]
for candidate := range self.settings {
if strings.HasPrefix(candidate, namespace) {
matched_namespaces[candidate] = map[string]dbus.Variant{}
}
}
} else if _, found := self.settings[namespace]; found {
matched_namespaces[namespace] = map[string]dbus.Variant{}
}
}
}
}
values := map[string]map[string]dbus.Variant{}
for namespace := range matched_namespaces {
values[namespace] = make(map[string]dbus.Variant, len(self.settings[namespace]))
maps.Copy(values[namespace], self.settings[namespace])
}
return values, nil
}
type vmap map[string]dbus.Variant
type Filter_expression struct {
Ftype uint32
Val string
}
type Filter struct {
Name string
Expressions []Filter_expression
}
func (f Filter) Equal(o Filter) bool {
return f.Name == o.Name && slices.Equal(f.Expressions, o.Expressions)
}
type ChooseFilesData struct {
Title string
Mode string
Cwd string
SuggestedSaveFileName, SuggestedSaveFilePath string
Handle dbus.ObjectPath
Filters []Filter
}
func (c *ChooseFilesData) set_filters(options vmap) {
if v, found := options["filters"]; found {
v.Store(&c.Filters)
}
if v, found := options["current_filter"]; found {
var x Filter
if err := v.Store(&x); err == nil {
idx := slices.IndexFunc(c.Filters, func(q Filter) bool { return x.Equal(q) })
if idx > -1 {
c.Filters = slices.Delete(c.Filters, idx, idx+1)
}
c.Filters = slices.Insert(c.Filters, 0, x)
}
}
}
func get_matching_filter(name string, all_filters []Filter) (dbus.Variant, bool) {
for _, x := range all_filters {
if x.Name == name {
return dbus.MakeVariant(x), true
}
}
return dbus.Variant{}, false
}
func (options vmap) get_bool(name string, defval bool) (ans bool) {
if v, found := options[name]; found {
if v.Store(&ans) == nil {
return
}
}
return defval
}
func (self *Portal) Cleanup() {
self.lock.Lock()
defer self.lock.Unlock()
if self.file_chooser_first_instance != nil {
self.file_chooser_first_instance.Process.Signal(unix.SIGTERM)
ch := make(chan int)
go func() {
self.file_chooser_first_instance.Wait()
ch <- 0
}()
select {
case <-ch:
case <-time.After(time.Second):
self.file_chooser_first_instance.Process.Kill()
self.file_chooser_first_instance.Wait()
}
self.file_chooser_first_instance = nil
}
}
type ChooserResponse struct {
Paths []string `json:"paths"`
Error string `json:"error"`
Interrupted bool `json:"interrupted"`
Current_filter string `json:"current_filter"`
}
func (self *Portal) run_file_chooser(cfd ChooseFilesData) (response uint32, result_dict vmap) {
response = RESPONSE_ENDED
tdir, err := os.MkdirTemp("", "kitty-cfd")
if err != nil {
log.Println("cannot run file chooser as failed to create a temporary directory with error: ", err)
return
}
pid_path := filepath.Join(tdir, "pid")
var close_requested, child_killed atomic.Bool
Close := func() *dbus.Error {
close_requested.Store(true)
if !child_killed.Load() {
if raw, err := os.ReadFile(pid_path); err == nil {
if pid, err := strconv.Atoi(string(raw)); err == nil {
child_killed.Store(true)
unix.Kill(pid, unix.SIGTERM)
}
}
}
return nil
}
self.bus.ExportMethodTable(map[string]any{"Close": Close}, cfd.Handle, REQUEST_INTERFACE)
defer func() {
self.bus.ExportMethodTable(nil, cfd.Handle, REQUEST_INTERFACE)
_ = os.RemoveAll(tdir)
}()
output_path := filepath.Join(tdir, "output.json")
cmd := func() *exec.Cmd {
self.lock.Lock()
defer self.lock.Unlock()
args := []string{
"+kitten", "panel", "--layer=overlay", "--edge=center", "--focus-policy=exclusive",
"-o", "background_opacity=0.85", "--wait-for-single-instance-window-close",
"--single-instance", "--instance-group", "cfp-" + strconv.Itoa(os.Getpid()),
}
for _, x := range self.opts.File_chooser_kitty_conf {
args = append(args, `-c`, x)
}
for _, x := range self.opts.File_chooser_kitty_override {
args = append(args, `-o`, x)
}
if self.file_chooser_first_instance == nil {
fifo_path := filepath.Join(tdir, "fifo")
if err := unix.Mkfifo(fifo_path, 0600); err != nil {
log.Println("cannot run file chooser as failed to create a fifo directory with error: ", err)
return nil
}
fa := slices.Clone(args)
fa = append(fa, "--start-as-hidden", "sh", "-c", "echo a > '"+fifo_path+"'; read")
cmd := exec.Command(utils.KittyExe(), fa...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Start()
ch := make(chan int)
go func() {
f, err := os.OpenFile(fifo_path, os.O_RDONLY, os.ModeNamedPipe)
if err != nil {
log.Println("cannot run file chooser as failed to open fifo for read with error: ", err)
}
b := []byte{'a', 'b', 'c', 'd'}
f.Read(b)
ch <- 0
}()
select {
case <-ch:
self.file_chooser_first_instance = cmd
case <-time.After(5 * time.Second):
log.Println("cannot run file chooser as panel script timed out writing to fifo")
return nil
}
}
args = append(args, "kitten", `choose-files`, `--mode`, cfd.Mode, `--write-output-to`, output_path, `--output-format=json`)
if cfd.SuggestedSaveFileName != "" {
args = append(args, `--suggested-save-file-name`, cfd.SuggestedSaveFileName)
}
if cfd.SuggestedSaveFilePath != "" {
args = append(args, `--suggested-save-file-path`, cfd.SuggestedSaveFilePath)
}
if cfd.Title != "" {
args = append(args, "--title", cfd.Title)
}
for _, fs := range cfd.Filters {
for _, exp := range fs.Expressions {
args = append(args, "--file-filter", fmt.Sprintf("%s:%s:%s", utils.IfElse(exp.Ftype == 0, "glob", "mime"), exp.Val, fs.Name))
}
}
args = append(args, "--write-pid-to", pid_path)
args = append(args, utils.IfElse(cfd.Cwd == "", "~", cfd.Cwd))
cmd := exec.Command(utils.KittyExe(), args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd
}()
if cmd == nil || close_requested.Load() {
return
}
log.Println("running file chooser with args:", cmd.Path, utils.Repr(cmd.Args))
if err := cmd.Run(); err != nil {
log.Println("running file chooser failed with error: ", err)
return
}
if close_requested.Load() {
return
}
raw, err := os.ReadFile(output_path)
if err != nil {
log.Println("running file chooser failed, could not read from output file with error: ", err)
return
}
if close_requested.Load() {
return
}
var result ChooserResponse
if err = json.Unmarshal(raw, &result); err != nil {
log.Println("running file chooser failed, invalid JSON response with error: ", err)
return
}
if result.Error != "" {
log.Println("running file chooser failed, with error: ", result.Error)
return
}
if result.Interrupted {
response = RESPONSE_CANCELED
log.Println("running file chooser failed, interrupted by user.")
return
}
response = RESPONSE_SUCCESS
prefix := "file://" + utils.IfElse(runtime.GOOS == "windows", "/", "")
uris := utils.Map(func(path string) string {
path = filepath.ToSlash(path)
u := url.URL{Path: path}
return prefix + u.EscapedPath()
}, result.Paths)
result_dict = vmap{"uris": dbus.MakeVariant(uris)}
if result.Current_filter != "" {
if v, found := get_matching_filter(result.Current_filter, cfd.Filters); found {
result_dict["current_filter"] = v
}
}
return
}
func (options vmap) get_bytearray(name string) string {
if v, found := options[name]; found {
var b []byte
if v.Store(&b) == nil {
// the FileChooser spec requires paths and filenames to be null
// terminated, so remove trailing nulls.
return string(bytes.TrimRight(b, "\x00"))
}
}
return ""
}
func (self *Portal) OpenFile(handle dbus.ObjectPath, app_id string, parent_window string, title string, options vmap) (uint32, vmap, *dbus.Error) {
cfd := ChooseFilesData{Title: title, Cwd: options.get_bytearray("current_folder"), Handle: handle}
cfd.set_filters(options)
dir_only := options.get_bool("directory", false)
multiple := options.get_bool("multiple", false)
if dir_only {
cfd.Mode = utils.IfElse(multiple, "dirs", "dir")
} else {
cfd.Mode = utils.IfElse(multiple, "files", "file")
}
response, result := self.run_file_chooser(cfd)
return response, result, nil
}
func (self *Portal) SaveFile(handle dbus.ObjectPath, app_id string, parent_window string, title string, options vmap) (uint32, vmap, *dbus.Error) {
cfd := ChooseFilesData{
Title: title, Cwd: options.get_bytearray("current_folder"), Handle: handle,
SuggestedSaveFileName: options.get_bytearray("current_name"),
SuggestedSaveFilePath: options.get_bytearray("current_file")}
multiple := options.get_bool("multiple", false)
cfd.set_filters(options)
cfd.Mode = utils.IfElse(multiple, "save-files", "save-file")
response, result := self.run_file_chooser(cfd)
return response, result, nil
}

View File

@@ -11,7 +11,7 @@ import (
"strings"
"unicode/utf8"
"github.com/kovidgoyal/kitty/tools/utils"
"kitty/tools/utils"
)
var _ = fmt.Print

View File

@@ -9,7 +9,7 @@ import (
"strings"
"testing"
"github.com/kovidgoyal/kitty/tools/utils"
"kitty/tools/utils"
"github.com/google/go-cmp/cmp"
)

View File

@@ -11,8 +11,8 @@ import (
"strings"
"sync"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/images"
"kitty/tools/utils"
"kitty/tools/utils/images"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/lexers"

View File

@@ -6,18 +6,17 @@ import (
"archive/tar"
"bytes"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/kovidgoyal/kitty/kittens/ssh"
"github.com/kovidgoyal/kitty/tools/cli"
"github.com/kovidgoyal/kitty/tools/config"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/utils"
"kitty/kittens/ssh"
"kitty/tools/cli"
"kitty/tools/config"
"kitty/tools/tui/loop"
"kitty/tools/utils"
)
var _ = fmt.Print
@@ -37,33 +36,16 @@ var conf *Config
var opts *Options
var lp *loop.Loop
var temp_files []string
func resolve_path(path string) (ans string, is_dir bool, err error) {
var s fs.FileInfo
if s, err = os.Stat(path); err != nil {
return
} else {
if s.Mode()&fs.ModeNamedPipe != 0 {
var src, dest *os.File
if src, err = os.Open(path); err != nil {
return
}
defer src.Close()
if dest, err = os.CreateTemp("", fmt.Sprintf("*-pipe-%s", filepath.Base(path))); err != nil {
return
}
defer dest.Close()
temp_files = append(temp_files, dest.Name())
if _, err = io.Copy(dest, src); err != nil {
return
}
return dest.Name(), false, nil
} else {
return path, s.IsDir(), nil
}
func isdir(path string) bool {
if s, err := os.Stat(path); err == nil {
return s.IsDir()
}
return false
}
func exists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func get_ssh_file(hostname, rpath string) (string, error) {
@@ -151,22 +133,15 @@ func main(_ *cli.Command, opts_ *Options, args []string) (rc int, err error) {
if err != nil {
return 1, err
}
defer func() {
for _, path := range temp_files {
os.Remove(path)
}
}()
var left_is_dir, right_is_dir bool
if left, left_is_dir, err = resolve_path(left); err != nil {
return 1, err
}
if right, right_is_dir, err = resolve_path(right); err != nil {
return 1, err
}
if left_is_dir != right_is_dir {
if isdir(left) != isdir(right) {
return 1, fmt.Errorf("The items to be diffed should both be either directories or files. Comparing a directory to a file is not valid.'")
}
if !exists(left) {
return 1, fmt.Errorf("%s does not exist", left)
}
if !exists(right) {
return 1, fmt.Errorf("%s does not exist", right)
}
lp, err = loop.New()
loop.MouseTrackingMode(lp, loop.BUTTONS_AND_DRAG_MOUSE_TRACKING)
if err != nil {

View File

@@ -8,13 +8,13 @@ import (
"strings"
"sync"
"github.com/kovidgoyal/kitty"
"github.com/kovidgoyal/kitty/tools/config"
"github.com/kovidgoyal/kitty/tools/tty"
"github.com/kovidgoyal/kitty/tools/tui"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/wcswidth"
"kitty"
"kitty/tools/config"
"kitty/tools/tty"
"kitty/tools/tui"
"kitty/tools/tui/loop"
"kitty/tools/utils"
"kitty/tools/wcswidth"
)
var _ = fmt.Print

View File

@@ -6,9 +6,9 @@ import (
"bytes"
"errors"
"fmt"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/images"
"github.com/kovidgoyal/kitty/tools/utils/shlex"
"kitty/tools/utils"
"kitty/tools/utils/images"
"kitty/tools/utils/shlex"
"os/exec"
"path/filepath"
"strconv"

View File

@@ -10,12 +10,12 @@ import (
"strconv"
"strings"
"github.com/kovidgoyal/kitty/tools/tui/graphics"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/tui/sgr"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/style"
"github.com/kovidgoyal/kitty/tools/wcswidth"
"kitty/tools/tui/graphics"
"kitty/tools/tui/loop"
"kitty/tools/tui/sgr"
"kitty/tools/utils"
"kitty/tools/utils/style"
"kitty/tools/wcswidth"
)
var _ = fmt.Print

View File

@@ -9,10 +9,10 @@ import (
"strings"
"sync"
"github.com/kovidgoyal/kitty/tools/tui"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/images"
"github.com/kovidgoyal/kitty/tools/wcswidth"
"kitty/tools/tui"
"kitty/tools/utils"
"kitty/tools/utils/images"
"kitty/tools/wcswidth"
)
var _ = fmt.Print

Some files were not shown because too many files have changed in this diff Show More