Compare commits

..

3 Commits
v0.42.2 ... f2

Author SHA1 Message Date
Kovid Goyal
715645e69c More work on floats 2025-05-11 22:06:12 +05:30
Kovid Goyal
4e697abb34 more work on floats 2025-05-11 22:06:12 +05:30
Kovid Goyal
96b0c463e8 Start work on floating windows 2025-05-11 22:06:11 +05:30
304 changed files with 13871 additions and 11387 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

@@ -106,69 +106,6 @@ consumption to do the same tasks.
Detailed list of changes
-------------------------------------
0.42.2 [2025-07-16]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- 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`)
- 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]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

@@ -643,10 +643,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 +686,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 +700,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 +710,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

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

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

@@ -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,7 +118,7 @@ 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.

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

@@ -114,12 +114,11 @@ shell.
The Linux dock panel was::
wm bar
kitten panel kitty +launch my-panel.py
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
This creates the panel window and runs the ``my-panel.py`` script inside it
using the Python interpreter that comes bundled with kitty. Unfortunately the
actual script is not public, but there are :ref:`public projects implementing
general purpose panels using kitty <panel_projects>`.
@@ -147,23 +146,29 @@ Compatibility with various platforms
🟢 **Hyprland**
Fully working, no known issues
🟢 **labwc**
🟢 **KDE** (kwin)
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.
Partially working. Issues include:
* Renders its configured background over the background window instead of
under it. This is likely because it uses the wlr protocol for
backgrounds itself.
* Hiding a dock panel (unmapping the window) does not release the space
used by the dock.
🟠 **niri**
Hiding a dock panel (unmapping the window) does not release the space used
by the dock.
Breaks when hiding (unmapping) layer shell windows. This means the quick
access terminal is non-functional, but background and dock panels work.
More technically, keyboard focus gets stuck in the hidden window and when trying
to remap the hidden window niri never sends configure events for the remapped surface.
🟠 **labwc**
Breaks when hiding (unmapping) layer shell windows. This means the quick
access terminal is non-functional, but background and dock panels work.
More technically, when unmapping the surface (attaching a NULL buffer to
it) labwc continues to send configure events to the unmapped surface,
leading to Wayland protocol errors and a crash of labwc.
🔴 **GNOME** (mutter)
Does not implement the wlr protocol at all, nothing works.

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

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

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) {')
@@ -459,7 +458,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 +508,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{')
@@ -615,7 +614,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 +646,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 +723,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 +751,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

@@ -1940,22 +1940,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 +1970,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 +2011,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 +2020,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 +2201,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)

View File

@@ -325,6 +325,7 @@ def generate_wrappers(glfw_header: str) -> None:
void glfwWaylandRedrawCSDWindowTitle(GLFWwindow *handle)
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)

13
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,19 +1302,14 @@ 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;
@@ -2386,7 +2380,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 +2874,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 +4239,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.
*

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;

8
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;
@@ -719,7 +719,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);
@@ -884,7 +883,6 @@ void _glfwPlatformUpdateTimer(unsigned long long timer_id, monotonic_t interval,
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);

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;

46
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,7 +971,7 @@ 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;
}
@@ -1040,13 +1028,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;
}
}
@@ -1156,7 +1143,7 @@ create_layer_shell_surface(_GLFWwindow *window) {
struct wl_output *wl_output = find_output_by_name(window->wl.layer_shell.config.output_name);
#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, get_layer_shell_layer(window), "kitty");
if (!ls) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: layer-surface creation failed");
return false;
@@ -1210,10 +1197,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 +1466,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);
@@ -2834,16 +2817,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,9 +2892,8 @@ 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; }

24
glfw/x11_window.c vendored
View File

@@ -606,11 +606,6 @@ calculate_layer_geometry(_GLFWwindow *window) {
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;
@@ -2097,13 +2092,7 @@ 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) {
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);
@@ -3384,17 +3373,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 //////
//////////////////////////////////////////////////////////////////////////

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.2
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.2 h1:Rm81SCZ2mPoH+Q8ZCc/9YvzPUN/E7HgPiPJD8SLV6GI=
github.com/alecthomas/chroma/v2 v2.17.2/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

@@ -13,11 +13,11 @@ import (
"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

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

View File

@@ -8,13 +8,13 @@ import (
"strconv"
"strings"
"github.com/kovidgoyal/kitty/tools/config"
"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/tui/readline"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/wcswidth"
"kitty/tools/config"
"kitty/tools/tui"
"kitty/tools/tui/graphics"
"kitty/tools/tui/loop"
"kitty/tools/tui/readline"
"kitty/tools/utils"
"kitty/tools/wcswidth"
)
var _ = fmt.Print
@@ -171,7 +171,6 @@ func (self *Handler) initialize() {
self.original_context_count = self.current_context_count
self.async_results = make(chan AsyncResult, 32)
go func() {
self.lp.RecoverFromPanicInGoRoutine()
r := AsyncResult{}
r.collection, r.err = create_collection(self.left, self.right)
self.async_results <- r
@@ -192,7 +191,6 @@ func (self *Handler) generate_diff() {
return nil
})
go func() {
self.lp.RecoverFromPanicInGoRoutine()
r := AsyncResult{rtype: DIFF}
r.diff_map, r.err = diff(jobs, self.current_context_count)
self.async_results <- r
@@ -232,7 +230,6 @@ func (self *Handler) highlight_all() {
}
text_files := utils.Filter(self.collection.paths_to_highlight.AsSlice(), is_path_text)
go func() {
self.lp.RecoverFromPanicInGoRoutine()
r := AsyncResult{rtype: HIGHLIGHT}
highlight_all(text_files, use_light_colors)
self.async_results <- r
@@ -255,7 +252,6 @@ func (self *Handler) load_all_images() {
if self.image_count > 0 {
image_collection.Initialize(self.lp)
go func() {
self.lp.RecoverFromPanicInGoRoutine()
r := AsyncResult{rtype: IMAGE_LOAD}
image_collection.LoadAll()
self.async_results <- r
@@ -277,7 +273,6 @@ func (self *Handler) resize_all_images_if_needed() {
}
if sz != self.images_resized_to && self.image_count > 0 {
go func() {
self.lp.RecoverFromPanicInGoRoutine()
image_collection.ResizeForPageSize(sz.Width, sz.Height)
r := AsyncResult{rtype: IMAGE_RESIZE, page_size: sz}
self.async_results <- r

View File

@@ -11,13 +11,13 @@ import (
"strings"
"unicode"
"github.com/kovidgoyal/kitty/tools/cli"
"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/utils/style"
"github.com/kovidgoyal/kitty/tools/wcswidth"
"kitty/tools/cli"
"kitty/tools/tty"
"kitty/tools/tui"
"kitty/tools/tui/loop"
"kitty/tools/utils"
"kitty/tools/utils/style"
"kitty/tools/wcswidth"
)
var _ = fmt.Print
@@ -33,7 +33,7 @@ func convert_text(text string, cols int) string {
continue
}
if strings.TrimRight(full_line, "\r") == "" {
for range len(full_line) {
for i := 0; i < len(full_line); i++ {
lines = append(lines, empty_line)
}
continue
@@ -203,27 +203,6 @@ func main(_ *cli.Command, o *Options, args []string) (rc int, err error) {
if len(mark_text) <= len(hint) {
mark_text = ""
} else {
replaced_text := mark_text[:len(hint)]
replaced_text = strings.ReplaceAll(replaced_text, "\r", "\n")
if strings.Contains(replaced_text, "\n") {
buf := strings.Builder{}
buf.Grow(2 * len(hint))
h := hint
parts := strings.Split(replaced_text, "\n")
for i, x := range parts {
if x != "" {
buf.WriteString(h[:len(x)])
h = h[len(x):]
}
if i != len(parts)-1 {
buf.WriteString("\n")
}
}
if h != "" {
buf.WriteString(h)
}
hint = buf.String()
}
mark_text = mark_text[len(hint):]
}
ans := hint_style(hint) + text_style(mark_text)

View File

@@ -117,7 +117,7 @@ controls where to display the selected error message, other options are ignored.
default={default_regex}
The regular expression to use when option :option:`--type` is set to
:code:`regex`, in Perl 5 syntax. If you specify a numbered group in the regular
expression, only the group will be matched. This allows you to match text
expression, only the group will be matched. This allow you to match text
ignoring a prefix/suffix, as needed. The default expression matches lines. To
match text over multiple lines, things get a little tricky, as line endings
are a sequence of zero or more null bytes followed by either a carriage return

View File

@@ -19,10 +19,10 @@ import (
"github.com/dlclark/regexp2"
"github.com/seancfoley/ipaddress-go/ipaddr"
"github.com/kovidgoyal/kitty"
"github.com/kovidgoyal/kitty/tools/config"
"github.com/kovidgoyal/kitty/tools/tty"
"github.com/kovidgoyal/kitty/tools/utils"
"kitty"
"kitty/tools/config"
"kitty/tools/tty"
"kitty/tools/utils"
)
var _ = fmt.Print

View File

@@ -5,8 +5,8 @@ package hints
import (
"errors"
"fmt"
"github.com/kovidgoyal/kitty"
"github.com/kovidgoyal/kitty/tools/utils"
"kitty"
"kitty/tools/utils"
"os"
"path/filepath"
"strconv"

View File

@@ -15,8 +15,8 @@ import (
"sync"
"unicode"
"github.com/kovidgoyal/kitty/tools/cli"
"github.com/kovidgoyal/kitty/tools/utils"
"kitty/tools/cli"
"kitty/tools/utils"
"golang.org/x/sys/unix"
)

View File

@@ -4,7 +4,7 @@ package hyperlinked_grep
import (
"fmt"
"github.com/kovidgoyal/kitty/tools/utils/shlex"
"kitty/tools/utils/shlex"
"testing"
"github.com/google/go-cmp/cmp"

View File

@@ -8,11 +8,11 @@ import (
"os"
"time"
"github.com/kovidgoyal/kitty/tools/tui/graphics"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/images"
"github.com/kovidgoyal/kitty/tools/utils/shm"
"kitty/tools/tui/graphics"
"kitty/tools/tui/loop"
"kitty/tools/utils"
"kitty/tools/utils/images"
"kitty/tools/utils/shm"
)
var _ = fmt.Print

View File

@@ -5,8 +5,8 @@ package icat
import (
"fmt"
"github.com/kovidgoyal/kitty/tools/tui/graphics"
"github.com/kovidgoyal/kitty/tools/utils/images"
"kitty/tools/tui/graphics"
"kitty/tools/utils/images"
)
var _ = fmt.Print

View File

@@ -11,13 +11,13 @@ import (
"sync/atomic"
"time"
"github.com/kovidgoyal/kitty/tools/cli"
"github.com/kovidgoyal/kitty/tools/tty"
"github.com/kovidgoyal/kitty/tools/tui"
"github.com/kovidgoyal/kitty/tools/tui/graphics"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/images"
"github.com/kovidgoyal/kitty/tools/utils/style"
"kitty/tools/cli"
"kitty/tools/tty"
"kitty/tools/tui"
"kitty/tools/tui/graphics"
"kitty/tools/utils"
"kitty/tools/utils/images"
"kitty/tools/utils/style"
"golang.org/x/sys/unix"
)

View File

@@ -4,13 +4,13 @@ package icat
import (
"fmt"
"github.com/kovidgoyal/kitty/tools/tty"
"github.com/kovidgoyal/kitty/tools/tui/graphics"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/images"
"github.com/kovidgoyal/kitty/tools/utils/shm"
"image"
"image/gif"
"kitty/tools/tty"
"kitty/tools/tui/graphics"
"kitty/tools/utils"
"kitty/tools/utils/images"
"kitty/tools/utils/shm"
"github.com/edwvee/exiffix"
"github.com/kovidgoyal/imaging"

View File

@@ -15,11 +15,11 @@ import (
"path/filepath"
"strings"
"github.com/kovidgoyal/kitty/tools/tty"
"github.com/kovidgoyal/kitty/tools/tui/graphics"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/images"
"github.com/kovidgoyal/kitty/tools/utils/shm"
"kitty/tools/tty"
"kitty/tools/tui/graphics"
"kitty/tools/utils"
"kitty/tools/utils/images"
"kitty/tools/utils/shm"
)
var _ = fmt.Print

View File

@@ -8,20 +8,20 @@ import (
"encoding/binary"
"errors"
"fmt"
"github.com/kovidgoyal/kitty"
"io"
"kitty"
"math"
not_rand "math/rand/v2"
"os"
"path/filepath"
"strings"
"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"
"github.com/kovidgoyal/kitty/tools/utils/images"
"github.com/kovidgoyal/kitty/tools/utils/shm"
"kitty/tools/tui"
"kitty/tools/tui/graphics"
"kitty/tools/tui/loop"
"kitty/tools/utils"
"kitty/tools/utils/images"
"kitty/tools/utils/shm"
)
var _ = fmt.Print

View File

@@ -12,10 +12,10 @@ import (
"strings"
"time"
"github.com/kovidgoyal/kitty/tools/cli"
"github.com/kovidgoyal/kitty/tools/tty"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/utils"
"kitty/tools/cli"
"kitty/tools/tty"
"kitty/tools/tui/loop"
"kitty/tools/utils"
)
var _ = fmt.Print

View File

@@ -13,7 +13,7 @@ import (
"golang.org/x/sys/unix"
"github.com/kovidgoyal/kitty/tools/simdstring"
"kitty/tools/simdstring"
)
var _ = fmt.Print

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