Compare commits

..

1 Commits
rect ... curve

Author SHA1 Message Date
Kovid Goyal
4e6b26dd7e Finish up implementation of drawing curve with derivative
Fixes #8299
2025-05-10 08:37:17 +05:30
367 changed files with 15368 additions and 14833 deletions

4
.gitattributes vendored
View File

@@ -1,7 +1,3 @@
kitty/terminfo.h linguist-generated=true
terminfo/kitty.termcap linguist-generated=true
terminfo/kitty.terminfo linguist-generated=true
terminfo/x/xterm-kitty linguist-generated=true
kitty/char-props-data.h linguist-generated=true
kitty_tests/GraphemeBreakTest.json linguist-generated=true
kitty/charsets.c linguist-generated=true

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,99 +106,12 @@ consumption to do the same tasks.
Detailed list of changes
-------------------------------------
0.43.0 [2025-07-16]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- A new :opt:`cursor_trail_color` setting to independently control the color of
cursor trails (:pull:`8830`)
- macOS: Add the default :kbd:`Cmd+L` mapping from Terminal.app to erase the
last command and its output (:disc:`6040`)
- Fix :opt:`background_opacity` being non-linear with light color themes
(:iss:`8869`)
- Wayland: Fix incorrect window size calculation when transitioning from
full screen to non-full screen with client side decorations (:iss:`8826`)
- macOS: Fix hiding quick access terminal window not restoring focus to
previously active application (:disc:`8840`)
- Allow using backspace to move the cursor onto the previous line in cooked mode. This is indicated by the `bw` property in kitty's terminfo (:iss:`8841`)
- Watchers: A new event for global watchers corresponding to the tab bar being changed (:disc:`8842`)
- Fix a regression in 0.40.0 that broke handling of the VS16 variation selector when it caused a character to flow to the next line (:iss:`8848`)
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]
0.42.0 [future]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- A new kitten: :doc:`quick-access-terminal </kittens/quick-access-terminal>` to :ref:`quake`
- The :doc:`panel kitten </kittens/panel>` works on macOS and X11 as well as Wayland (:iss:`2590`)
- The :doc:`panel kitten </kittens/panel>` now works on macOS as well as Wayland (:iss:`2590`)
- **Behavior change**: Now kitty does full grapheme segmentation following the
Unicode 16 spec when splitting text into cells (:iss:`8533`)
@@ -210,7 +123,7 @@ Detailed list of changes
- launch: Allow creating desktop panels such as those created by the :doc:`panel kitten </kittens/panel>` (:iss:`8549`)
- Remote control: Allow modifying desktop panels and showing/hiding OS Windows
using the ``kitten @ resize-os-window`` command (:iss:`8550`)
using the `kitten @ resize-os-window` command (:iss:`8550`)
- Remote control launch: Allow waiting for a program launched in a new window
to exit and get the exit code via the `kitty +launch
@@ -247,7 +160,7 @@ Detailed list of changes
- :ac:`change_font_size` allow multiplying/dividing the current font size in addition to incrementing it (:iss:`8616`)
- Box drawing: Improve appearance of rounder corners, giving them a uniform line width (:iss:`8299`)
- Box drawing: Improve appearance of rounder corners giving them a uniform line width (:iss:`8299`)
0.41.1 [2025-04-03]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

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

View File

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

View File

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

@@ -416,20 +416,13 @@ also set it with the following command:
You can also change the icon manually by following the steps:
.. tab:: macOS
#. Find :file:`kitty.app` in the Applications folder, select it and press :kbd:`⌘+I`
#. Drag :file:`kitty.icns` onto the application icon in the kitty info pane
#. Delete the icon cache and restart Dock:
#. Find :file:`kitty.app` in the Applications folder, select it and press :kbd:`⌘+I`
#. Drag :file:`kitty.icns` onto the application icon in the kitty info pane
#. Delete the icon cache and restart Dock::
.. code-block:: sh
rm /var/folders/*/*/*/com.apple.dock.iconcache; killall Dock
.. tab:: Linux
#. Copy :file:`kitty.desktop` from the installation location (usually
:file:`/usr/share/applications` to :file:`~/.local/share/applications`
#. Edit the copied desktop file changing the ``Icon`` line to have
the absolute path to your desired icon.
rm /var/folders/*/*/*/com.apple.dock.iconcache; killall Dock
How do I map key presses in kitty to different keys in the terminal program?

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>`_
@@ -333,30 +329,9 @@ A system panel for Kitty terminal that displays real-time system metrics using t
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
A kitten-panel based desktop panel for your desktop
Password managers
---------------------
`1password <https://github.com/mm-zacharydavison/kitty-kitten-1password>`__
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Allow injecting passwords from 1Password into kitty.
`BitWarden <https://github.com/dnanhkhoa/kitty-password-manager>`__
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Inject passwords from ButWarden into kitty
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,109 +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 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.
Type a few letters from the filename and once it becomes the top selection,
press :kbd:`Enter`. You can change the current directory by instead selecting a
directory and pressing the :kbd:`Tab` key. :kbd:`Shift+Tab` goes up one
directory level.
Creating shortcuts to favorite/frequently used directories
------------------------------------------------------------
You can create keyboard shortcuts to quickly switch to any directory in
:file:`choose-files.conf`. For example:
.. code-block:: conf
map ctrl+t cd /tmp
map alt+p cd ~/my/project
Selecting multiple files
-----------------------------
When you wish to select multiple files, start the kitten with :option:`--mode
<kitty +kitten choose_files --mode>`:code:`=files`. Then instead of pressing
:kbd:`Enter`, press :kbd:`Shift+Enter` instead and the file will be added to the list
of selections. You can also hold the :kbd:`Ctrl` key and click on files to add
them to the selections. Similarly, you can hold the :kbd:`Alt` key and click to
select ranges of files (similar to using :kbd:`Shift+click` in a GUI app).
Press :kbd:`Enter` on the last selected file to finish. The list of selected
files is displayed at the bottom of the kitten and you can click on them
to deselect a file. Similarly, pressing :kbd:`Shift+Enter` will un-select a
previously selected file.
Hidden and ignored files
--------------------------
By default, the kitten does not process hidden files and directories (whose
names start with a period). This can be :opt:`changed in the configuration <kitten-choose_files.show_hidden>`
and also at runtime via the clickable link to the right of the search input.
Similarly, the kitten respects both :file:`.gitignore` and :file:`.ignore`
files, by default. This can also be changed both :opt:`in configuration
<kitten-choose_files.respect_ignores>` or at runtime. Note that
:file:`.gitignore` files are only respected if there is also a :file:`.git`
directory present. The kitten also supports the global :file:`.gitignore` file,
though it applies only inside git working trees. You can specify :opt:`global ignore
patterns <kitten-choose_files.ignore>`, that apply everywhere in :file:`choose-files.conf`.
Selecting non-existent files (save file names)
-------------------------------------------------
This kitten can also be used to select non-existent files, that is a new file
for a :guilabel:`Save file` type of dialog using :option:`--mode <kitty +kitten
choose_files --mode>`:code:`=save-file`. Once you have changed to the directory
you want the file to be in (using the :kbd:`Tab` key),
press :kbd:`Ctrl+Enter` and you will be able to type in the file name.
Selecting directories
---------------------------
This kitten can also be used to select directories,
for an :guilabel:`Open directory` type of dialog using :option:`--mode <kitty +kitten
choose_files --mode>`:code:`=dir`. Once you have changed to the directory
you want, press :kbd:`Ctrl+Enter` to accept it. Or if you are in a parent
directory you can select a descendant directory by pressing :kbd:`Enter`, the
same as you would for selecting a file to open.
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

@@ -418,8 +418,6 @@ Kittens created by kitty users
`smart-scroll <https://github.com/yurikhan/kitty-smart-scroll>`_
Makes the kitty scroll bindings work in full screen applications
`kitty-tab-switcher <https://github.com/OsiPog/kitty-tab-switcher>`__
Fuzzy finder for kitty tabs with previews
`gattino <https://github.com/salvozappa/gattino>`__
Integrate kitty with an LLM to convert plain language prompts into shell commands.

View File

@@ -1,126 +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.
How to install
-------------------
.. note::
This kitten relies on the :doc:`panel kitten </kittens/panel>`
under the hood to supply UI components. Check :ref:`the documentation <panel_compat>`
of that kitten to see if your window manager works with it.
First, run::
kitten desktop-ui enable-portal
Then, set the following two environment variables, *system wide*, that means in
:file:`/etc/environment` or the equivalent for your distribution::
QT_QPA_PLATFORMTHEME=xdgdesktopportal
GTK_USE_PORTAL=1
Finally, reboot. Now, when you open a file dialog in most GUI applications, it
should open the :doc:`choose-files kitten </kittens/choose-files>` instead
of a normal file open dialog. You can change the current light/dark mode of
your desktop by running::
kitten desktop-ui set-color-scheme dark
kitten desktop-ui set-color-scheme light
Check the current value using::
dbus-send --session --print-reply --dest=org.freedesktop.portal.Desktop /org/freedesktop/portal/desktop org.freedesktop.portal.Settings.Read string:org.freedesktop.appearance string:color-scheme
How it works
----------------
Modern Linux desktops have so called `portals
<https://flatpak.github.io/xdg-desktop-portal/docs/index.html>`__ that were
invented for sandboxed applications and provide various facilities to such
applications over DBUS, including file open dialogs, common desktop settings,
etc. This kitten works by implementing a backend for some of these services.
Normal GUI applications can then be told to make use of these services, thereby
allowing us to replace parts of the desktop experience as needed.
There are multiple competing implementations of the backends. Each desktop
environment like KDE or GNOME has it's own backend and many window managers
provide implementations for some backends as well. Service discovery and
configuring which backend to use happens via the :file:`xdg-desktop-portal`
program, usually found at :file:`/usr/lib/xdg-desktop-portal`.
It can be configured by files in :file:`~/.local/share/xdg-desktop-portal`. See
`man portals.conf <https://man.archlinux.org/man/portals.conf.5>`__. The
``kitten desktop-ui enable-portal`` command takes care of the setup for you
automatically. If you want to customize exactly which services to use this
kitten for, run the command and then edit the conf file that the command says
it has patched.
Troubleshooting
-------------------
First, ensure that DBUS is able to auto-start the kitten when it is needed. If
the kitten is not already running, try the following command::
dbus-send --session --print-reply --dest=org.freedesktop.impl.portal.desktop.kitty \
/net/kovidgoyal/kitty/portal org.freedesktop.DBus.Properties.GetAll \
string:net.kovidgoyal.kitty.settings
If DBUS is able to start the kitten or if it is already running it will print
out the version property, otherwise it will fail with an error. If it fails,
check the file
:file:`~/.local/share/dbus-1/services/org.freedesktop.impl.portal.desktop.kitty.service`
that should have been created by the ``enable-portal`` command. It's ``Exec``
key must point to the full path to the kitten executable.
Next, check that the XDG portal system is actually using this kitten for its
settings backend. Run::
dbus-send --session --print-reply --dest=org.freedesktop.portal.Desktop \
/org/freedesktop/portal/desktop org.freedesktop.portal.Settings.Read \
string:net.kovidgoyal.kitty string:status
If this returns a reply then the kitten is being used, as expected. If it
returns a not found error, then some other backend is being used for settings.
Read the ``portals.conf`` man page and run::
/usr/lib/xdg-desktop-portal -r v
this will output a lot of debug information, which should tell you which
backend is chosen for which service. Read the debug output carefully to
determine why the kitten is not being selected.
If some GUI applications are not using the choose-files kitten for their file
select dialogs, then make sure the environment variables mentioned above are
set, you can also try running the the GUI application with them set explicitly,
as::
QT_QPA_PLATFORMTHEME=xdgdesktopportal GTK_USE_PORTAL=1 my-gui-app
Note that not all applications use portals, so if some particular application
is failing to use the portal but others work, report the issue to that
applications' developers.

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

@@ -175,17 +175,6 @@ create :file:`~/.config/kitty/mywatcher.py` and use :option:`launch --watcher` =
# code received from the program running in the window
...
def on_tab_bar_dirty(boss: Boss, window: Window, data: dict[str, Any]) -> None:
# called when any changes happen to the tab bar, such a new tabs being
# created, tab titles changing, tabs moving, etc. Useful to display the
# tab bar externally to kitty. This is called even if the tab bar is
# hidden. Note that this is called only in *global watchers*, that is
# watchers defined in kitty.conf or using the --watcher command line
# flag. data contains tab_manager which is the object responsible for
# managing all tabs in a single OS Window.
...
Every callback is passed a reference to the global ``Boss`` object as well as
the ``Window`` object the action is occurring on. The ``data`` object is a dict
that contains event dependent data. You have full access to kitty internals in

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
@@ -357,11 +357,11 @@ The algorithm for splitting text into cells
<https://www.unicode.org/Public/UCD/latest/ucd/auxiliary/GraphemeBreakTest.txt>`__.
.. warning::
This algorithm is under public discussion in :iss:`8533`. If serious issues
are brought to light in that discussion, there may be small changes to the
algorithm to address them. Additionally, in the future if the Unicode standard
changes in ways that affect this algorithm, it will be updated. Currently the
algorithm is based on Unicode version 16.
This algorithm is under public discussion in :iss:`8533`. Until that issue
is closed, it is subject to change based on feedback from the community.
Additionally, in the future if the Unicode standard changes in ways that
affect this algorithm, it will be updated. Currently the algorithm is based
on Unicode version 16.
Here, we specify how a terminal must split up text into cells, where a cell is
a width one unit in the character grid the terminal displays.

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,13 +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('_', ' ')
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():
@@ -455,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:
@@ -468,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

@@ -10,8 +10,7 @@ import subprocess
cmdline = (
'glad --out-path {dest} --api gl:core=3.1 '
' --extensions GL_ARB_texture_storage,GL_ARB_copy_image,GL_ARB_multisample,GL_ARB_robustness,'
'GL_ARB_instanced_arrays,GL_KHR_debug,GL_ARB_framebuffer_sRGB,GL_EXT_framebuffer_sRGB '
' --extensions GL_ARB_texture_storage,GL_ARB_copy_image,GL_ARB_multisample,GL_ARB_robustness,GL_ARB_instanced_arrays,GL_KHR_debug '
'c --header-only --debug'
)

View File

@@ -301,14 +301,6 @@ static NSDictionary<NSString*,NSNumber*> *global_shortcuts = nil;
@implementation GLFWApplicationDelegate
- (void)applicationDidActivate:(NSNotification *)notification {
NSRunningApplication *app = notification.userInfo[NSWorkspaceApplicationKey];
if (app && app.processIdentifier != getpid()) {
_glfw.ns.previous_front_most_application = app.processIdentifier;
debug_rendering("Front most application changed to: %s pid: %d\n", app.bundleIdentifier.UTF8String, app.processIdentifier)
}
}
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
{
(void)sender;
@@ -460,7 +452,6 @@ static GLFWapplicationwillfinishlaunchingfun finish_launching_callback = NULL;
CGDirectDisplayID displayID = [(NSNumber*)displayIDAsID unsignedIntValue];
_glfwDispatchRenderFrame(displayID);
}
@end
@@ -825,11 +816,6 @@ int _glfwPlatformInit(bool *supports_window_occlusion)
}
[NSApp setDelegate:_glfw.ns.delegate];
[[[NSWorkspace sharedWorkspace] notificationCenter]
addObserver:_glfw.ns.delegate
selector:@selector(applicationDidActivate:)
name:NSWorkspaceDidActivateApplicationNotification
object:nil];
static struct {
unsigned short virtual_key_code;
NSEventModifierFlags input_source_switch_modifiers;
@@ -842,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");
@@ -1158,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

@@ -188,7 +188,6 @@ typedef struct _GLFWlibraryNS
double restoreCursorPosX, restoreCursorPosY;
// The window whose disabled cursor mode is active
_GLFWwindow* disabledCursorWindow;
pid_t previous_front_most_application;
struct {
CFBundleRef bundle;

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];
@@ -2236,26 +2209,6 @@ void _glfwPlatformShowWindow(_GLFWwindow* window)
void _glfwPlatformHideWindow(_GLFWwindow* window)
{
[window->ns.object orderOut:nil];
pid_t prev_app_pid = _glfw.ns.previous_front_most_application; _glfw.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,11 +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",
"staging/color-management/color-management-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);

View File

@@ -570,11 +570,6 @@ window_is_csd_capable(_GLFWwindow *window) {
return window->decorated && !decs.serverSide && window->wl.xdg.toplevel;
}
bool
csd_should_window_be_decorated(_GLFWwindow *window) {
return window_is_csd_capable(window) && window->monitor == NULL && (window->wl.current.toplevel_states & TOPLEVEL_STATE_FULLSCREEN) == 0;
}
static bool
ensure_csd_resources(_GLFWwindow *window) {
if (!window_is_csd_capable(window)) return false;
@@ -658,18 +653,18 @@ csd_change_title(_GLFWwindow *window) {
void
csd_set_window_geometry(_GLFWwindow *window, int32_t *width, int32_t *height) {
const bool include_space_for_csd = csd_should_window_be_decorated(window);
bool has_csd = window_is_csd_capable(window) && decs.titlebar.surface && !(window->wl.current.toplevel_states & TOPLEVEL_STATE_FULLSCREEN);
bool size_specified_by_compositor = *width > 0 && *height > 0;
if (!size_specified_by_compositor) {
*width = window->wl.user_requested_content_size.width;
*height = window->wl.user_requested_content_size.height;
if (window->wl.xdg.top_level_bounds.width > 0) *width = MIN(*width, window->wl.xdg.top_level_bounds.width);
if (window->wl.xdg.top_level_bounds.height > 0) *height = MIN(*height, window->wl.xdg.top_level_bounds.height);
if (include_space_for_csd) *height += decs.metrics.visible_titlebar_height;
if (has_csd) *height += decs.metrics.visible_titlebar_height;
}
decs.geometry.x = 0; decs.geometry.y = 0;
decs.geometry.width = *width; decs.geometry.height = *height;
if (include_space_for_csd) {
if (has_csd) {
decs.geometry.y = -decs.metrics.visible_titlebar_height;
*height -= decs.metrics.visible_titlebar_height;
}

View File

@@ -13,6 +13,5 @@ void csd_free_all_resources(_GLFWwindow *window);
bool csd_change_title(_GLFWwindow *window);
void csd_set_window_geometry(_GLFWwindow *window, int32_t *width, int32_t *height);
bool csd_set_titlebar_color(_GLFWwindow *window, uint32_t color, bool use_system_color);
bool csd_should_window_be_decorated(_GLFWwindow *window);
void csd_set_visible(_GLFWwindow *window, bool visible);
void csd_handle_pointer_event(_GLFWwindow *window, int button, int state, struct wl_surface* surface);

108
glfw/wl_init.c vendored
View File

@@ -454,52 +454,6 @@ static const struct wl_seat_listener seatListener = {
seatHandleName,
};
static void
ignored_color_manager_event(void *data UNUSED, struct wp_color_manager_v1 *wp_color_manager_v1 UNUSED, uint32_t x UNUSED) {}
static void
on_color_manger_features_done(void *data UNUSED, struct wp_color_manager_v1 *wp_color_manager_v1 UNUSED) {
_glfw.wl.color_manager.capabilities_reported = true;
}
static void
on_supported_color_primaries(void *data UNUSED, struct wp_color_manager_v1 *wp_color_manager_v1 UNUSED, uint32_t x) {
switch(x) {
case WP_COLOR_MANAGER_V1_PRIMARIES_SRGB:
_glfw.wl.color_manager.supported_primaries.srgb = true; break;
}
}
static void
on_supported_color_feature(void *data UNUSED, struct wp_color_manager_v1 *wp_color_manager_v1 UNUSED, uint32_t x) {
switch(x) {
case WP_COLOR_MANAGER_V1_FEATURE_PARAMETRIC:
_glfw.wl.color_manager.supported_features.parametric = true; break;
case WP_COLOR_MANAGER_V1_FEATURE_SET_PRIMARIES:
_glfw.wl.color_manager.supported_features.set_primaries = true; break;
}
}
static void
on_supported_color_transfer_function(void *data UNUSED, struct wp_color_manager_v1 *wp_color_manager_v1 UNUSED, uint32_t x) {
switch(x) {
case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_SRGB:
_glfw.wl.color_manager.supported_transfer_functions.srgb = true; break;
case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_GAMMA22:
_glfw.wl.color_manager.supported_transfer_functions.gamma22 = true; break;
case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR:
_glfw.wl.color_manager.supported_transfer_functions.ext_linear = true; break;
}
}
static const struct wp_color_manager_v1_listener color_manager_listener = {
.supported_intent = ignored_color_manager_event,
.supported_feature = on_supported_color_feature,
.supported_primaries_named = on_supported_color_primaries,
.supported_tf_named = on_supported_color_transfer_function,
.done = on_color_manger_features_done,
};
static void wmBaseHandlePing(void* data UNUSED,
struct xdg_wm_base* wmBase,
uint32_t serial)
@@ -646,19 +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);
} else if (is(wp_color_manager_v1)) {
_glfw.wl.wp_color_manager_v1 = wl_registry_bind(registry, name, &wp_color_manager_v1_interface, 1);
wp_color_manager_v1_add_listener(_glfw.wl.wp_color_manager_v1, &color_manager_listener, NULL);
}
#undef is
}
@@ -693,6 +639,7 @@ static const struct wl_registry_listener registryListener = {
registryHandleGlobalRemove
};
GLFWAPI GLFWColorScheme glfwGetCurrentSystemColorTheme(bool query_if_unintialized) {
return glfw_current_system_color_theme(query_if_unintialized);
}
@@ -769,12 +716,8 @@ 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);
#define P(x) p += snprintf(p, sizeof(buf) - (p - buf), "%s ", x);
if (_glfw.wl.xdg_wm_base_version < 6) P("window-state-suspended");
if (_glfw.wl.xdg_wm_base_version < 5) P("window-capabilities");
if (!_glfw.wl.color_manager.has_needed_capabilities) P("color-manager");
#undef P
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
while (p > buf && (p - 1)[0] == ' ') { p--; *p = 0; }
return buf;
@@ -782,25 +725,6 @@ get_compositor_missing_capabilities(void) {
GLFWAPI const char* glfwWaylandMissingCapabilities(void) { return get_compositor_missing_capabilities(); }
static void
image_description_failed(void *data UNUSED, struct wp_image_description_v1 *d, uint32_t cause, const char *msg) {
wp_image_description_v1_destroy(d);
_glfwInputError(GLFW_PLATFORM_ERROR, "Failed to create color mamagement profile description with cause: %d and error: %s", cause, msg);
_glfw.wl.color_manager.image_description_done = true;
}
static void
image_description_ready(void *data UNUSED, struct wp_image_description_v1 *d, uint32_t identity UNUSED) {
_glfw.wl.color_manager.image_description_done = true;
_glfw.wl.color_manager.image_description = d;
}
static const struct wp_image_description_v1_listener image_description_listener = {
.failed = image_description_failed,
.ready = image_description_ready,
};
int _glfwPlatformInit(bool *supports_window_occlusion)
{
int i;
@@ -859,23 +783,6 @@ int _glfwPlatformInit(bool *supports_window_occlusion)
// Sync so we got all initial output events
wl_display_roundtrip(_glfw.wl.display);
// Sync so we get all color manager capabilities
if (_glfw.wl.wp_color_manager_v1) {
while (!_glfw.wl.color_manager.capabilities_reported) wl_display_roundtrip(_glfw.wl.display);
_glfw.wl.color_manager.has_needed_capabilities = \
_glfw.wl.color_manager.supported_transfer_functions.srgb &&
_glfw.wl.color_manager.supported_features.parametric &&
_glfw.wl.color_manager.supported_features.set_primaries;
if (_glfw.wl.color_manager.has_needed_capabilities) {
struct wp_image_description_creator_params_v1 *c = wp_color_manager_v1_create_parametric_creator(
_glfw.wl.wp_color_manager_v1);
wp_image_description_creator_params_v1_set_tf_named(c, WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_SRGB);
wp_image_description_creator_params_v1_set_primaries_named(c, WP_COLOR_MANAGER_V1_PRIMARIES_SRGB);
wp_image_description_v1_add_listener(wp_image_description_creator_params_v1_create(c),
&image_description_listener, NULL);
}
}
for (i = 0; i < _glfw.monitorCount; ++i)
{
monitor = _glfw.monitors[i];
@@ -987,13 +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_color_manager_v1) {
if (_glfw.wl.color_manager.image_description)
wp_image_description_v1_destroy(_glfw.wl.color_manager.image_description);
wp_color_manager_v1_destroy(_glfw.wl.wp_color_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)
@@ -1008,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,

19
glfw/wl_platform.h vendored
View File

@@ -66,11 +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"
#include "wayland-color-management-v1-client-protocol.h"
#define _glfw_dlopen(name) dlopen(name, RTLD_LAZY | RTLD_LOCAL)
#define _glfw_dlclose(handle) dlclose(handle)
@@ -172,7 +169,6 @@ typedef struct _GLFWwindowWayland
bool hovered;
bool transparent;
struct wl_surface* surface;
struct wp_color_management_surface_v1 *color_management;
bool waiting_for_swap_to_commit;
struct wl_egl_window* native;
struct wl_callback* callback;
@@ -214,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.
@@ -293,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
@@ -345,8 +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_color_manager_v1* wp_color_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;
@@ -355,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;
@@ -396,14 +388,6 @@ typedef struct _GLFWlibraryWayland
PFN_wl_egl_window_resize window_resize;
} egl;
struct {
struct { bool gamma22, ext_linear, srgb; } supported_transfer_functions;
struct { bool srgb; } supported_primaries;
struct { bool parametric, set_primaries; } supported_features;
bool capabilities_reported, image_description_done, has_needed_capabilities;
struct wp_image_description_v1 *image_description;
} color_manager;
struct {
glfw_wl_xdg_activation_request *array;
size_t capacity, sz;
@@ -422,6 +406,7 @@ typedef struct _GLFWmonitorWayland
{
struct wl_output* output;
uint32_t name;
char friendly_name[64], description[64];
int currentMode;
int x;

88
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++) {
@@ -424,7 +412,7 @@ apply_scale_changes(_GLFWwindow *window, bool resize_framebuffer, bool update_cs
double scale = _glfwWaylandWindowScale(window);
if (resize_framebuffer) resizeFramebuffer(window);
_glfwInputWindowContentScale(window, (float)scale, (float)scale);
if (update_csd) csd_set_visible(window, csd_should_window_be_decorated(window)); // resize the csd iff the window currently has CSD
if (update_csd) csd_set_visible(window, true); // resize the csd iff the window currently has CSD
int buffer_scale = window->wl.fractional_scale ? 1 : (int)scale;
wl_surface_set_buffer_scale(window->wl.surface, buffer_scale);
}
@@ -571,27 +559,18 @@ static const struct wp_fractional_scale_v1_listener fractional_scale_listener =
.preferred_scale = &fractional_scale_preferred_scale,
};
static void
ensure_color_manager_ready(void) {
if (_glfw.wl.wp_color_manager_v1 && !_glfw.wl.color_manager.image_description_done) {
while (!_glfw.wl.color_manager.image_description_done) wl_display_roundtrip(_glfw.wl.display);
}
}
static bool
create_surface(_GLFWwindow* window, const _GLFWwndconfig* wndconfig) {
static bool createSurface(_GLFWwindow* window,
const _GLFWwndconfig* wndconfig)
{
window->wl.surface = wl_compositor_create_surface(_glfw.wl.compositor);
if (!window->wl.surface) return false;
wl_surface_add_listener(window->wl.surface, &surfaceListener, window);
wl_surface_set_user_data(window->wl.surface, window);
if (!window->wl.surface)
return false;
if (_glfw.wl.color_manager.has_needed_capabilities) {
ensure_color_manager_ready();
if (_glfw.wl.color_manager.image_description) {
window->wl.color_management = wp_color_manager_v1_get_surface(_glfw.wl.wp_color_manager_v1, window->wl.surface);
wp_color_management_surface_v1_set_image_description(window->wl.color_management, _glfw.wl.color_manager.image_description, WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL);
}
}
wl_surface_add_listener(window->wl.surface,
&surfaceListener,
window);
wl_surface_set_user_data(window->wl.surface, window);
// If we already have been notified of the primary monitor scale, assume
// the window will be created on it and so avoid a rescale roundtrip in the common
@@ -637,7 +616,6 @@ create_surface(_GLFWwindow* window, const _GLFWwndconfig* wndconfig) {
update_regions(window);
wl_surface_set_buffer_scale(window->wl.surface, scale);
if (_glfw.keyboard_grabbed) inhibit_shortcuts_for(window, true);
return true;
}
@@ -818,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);
}
}
@@ -833,7 +812,7 @@ apply_xdg_configure_changes(_GLFWwindow *window) {
int width = window->wl.pending.width, height = window->wl.pending.height;
csd_set_window_geometry(window, &width, &height);
bool resized = dispatchChangesAfterConfigure(window, width, height);
csd_set_visible(window, csd_should_window_be_decorated(window));
csd_set_visible(window, !(window->wl.decorations.serverSide || window->monitor || window->wl.current.toplevel_states & TOPLEVEL_STATE_FULLSCREEN));
debug("Final window %llu content size: %dx%d resized: %d\n", window->id, width, height, resized);
}
@@ -976,7 +955,7 @@ setXdgDecorations(_GLFWwindow* window)
zxdg_toplevel_decoration_v1_set_mode(window->wl.xdg.decoration, window->decorated ? ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE: ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE);
} else {
window->wl.decorations.serverSide = false;
csd_set_visible(window, csd_should_window_be_decorated(window));
csd_set_visible(window, window->decorated);
}
}
@@ -992,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;
}
@@ -1049,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;
}
}
@@ -1165,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;
@@ -1219,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);
@@ -1446,7 +1422,7 @@ int _glfwPlatformCreateWindow(
strncpy(window->wl.appId, wndconfig->wl.appId, sizeof(window->wl.appId));
window->swaps_disallowed = true;
if (!create_surface(window, wndconfig)) return false;
if (!createSurface(window, wndconfig)) return false;
if (wndconfig->title) window->wl.title = _glfw_strdup(wndconfig->title);
if (wndconfig->maximized) window->wl.maximize_on_first_show = true;
if (wndconfig->visible) {
@@ -1490,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);
@@ -1522,9 +1496,6 @@ void _glfwPlatformDestroyWindow(_GLFWwindow* window)
if (window->wl.layer_shell.zwlr_layer_surface_v1)
zwlr_layer_surface_v1_destroy(window->wl.layer_shell.zwlr_layer_surface_v1);
if (window->wl.color_management)
wp_color_management_surface_v1_destroy(window->wl.color_management);
if (window->wl.surface)
wl_surface_destroy(window->wl.surface);
@@ -1644,7 +1615,7 @@ void _glfwPlatformSetWindowSize(_GLFWwindow* window, int width, int height)
csd_set_window_geometry(window, &w, &h);
window->wl.width = w; window->wl.height = h;
resizeFramebuffer(window);
csd_set_visible(window, csd_should_window_be_decorated(window)); // resizes the csd iff the window currently has csd
csd_set_visible(window, true); // resizes the csd iff the window currently has csd
commit_window_surface_if_safe(window);
inform_compositor_of_window_geometry(window, "SetWindowSize");
}
@@ -2846,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 //////
//////////////////////////////////////////////////////////////////////////
@@ -2931,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.20.0
github.com/bmatcuk/doublestar/v4 v4.9.1
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.30.0
golang.org/x/sys v0.35.0
golang.org/x/text v0.28.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

24
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.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
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.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.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
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.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
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,6 +0,0 @@
def syntax_aliases(x: str) -> dict[str, str]:
ans = {}
for x in x.split():
k, _, v = x.partition(':')
ans[k] = v
return ans

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,862 +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, preview_width int
first_idx CollectionIndex
}
type State struct {
base_dir string
current_dir string
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
show_preview bool
respect_ignores bool
sort_by_last_modified bool
global_ignores ignorefiles.IgnoreFile
keyboard_shortcuts []*config.KeyAction
display_title bool
pygments_style, dark_pygments_style string
syntax_aliases map[string]string
selections []string
current_idx CollectionIndex
last_render render_state
mouse_state tui.MouseState
redraw_needed bool
}
func (s State) HighlightStyles() (string, string) { return s.pygments_style, s.dark_pygments_style }
func (s State) SyntaxAliases() map[string]string { return s.syntax_aliases }
func (s State) DisplayTitle() bool { return s.display_title }
func (s State) ShowHidden() bool { return s.show_hidden }
func (s State) ShowPreview() bool { return s.show_preview }
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) 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
preview_manager *PreviewManager
}
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)
if h.state.DisplayTitle() {
h.lp.Println(h.state.WindowTitle())
h.draw_search_bar(1)
} else {
h.draw_search_bar(0)
}
}()
y := SEARCH_BAR_HEIGHT + utils.IfElse(h.state.DisplayTitle(), 1, 0)
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() {
if h.state.mode.OnlyDirs() {
h.state.AddSelection(h.state.CurrentDir())
return h.finish_selection()
}
h.lp.Beep()
} else {
return h.switch_to_save_file_name_mode()
}
case "toggle":
switch args {
case "preview":
h.state.show_preview = !h.state.show_preview
return h.draw_screen()
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 ".":
return h.change_to_current_dir_if_possible()
case "..":
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()
} else {
h.lp.Beep()
return nil
}
}
}
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"`
Hide_preview bool `json:"hide_preview"`
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, Hide_preview: !s.show_preview}
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
h.state.show_preview = true
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
}
switch conf.Show_preview {
case Show_preview_true, Show_preview_y, Show_preview_yes:
h.state.show_preview = true
case Show_preview_false, Show_preview_n, Show_preview_no:
h.state.show_preview = false
case Show_preview_last:
h.state.show_preview = !cached_values().Hide_preview
}
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
h.state.display_title = opts.DisplayTitle
h.state.pygments_style = conf.Pygments_style
h.state.dark_pygments_style = conf.Dark_pygments_style
h.state.syntax_aliases = conf.Syntax_aliases
return
}
var default_cwd string
var use_light_colors bool
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)
lp.ColorSchemeChangeNotifications()
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)
handler.preview_manager = NewPreviewManager(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)
}
lp.RequestCurrentColorScheme()
return handler.OnInitialize()
}
lp.OnResize = func(old, new_size loop.ScreenSize) (err error) {
handler.init_sizes(new_size)
return handler.draw_screen()
}
lp.OnColorSchemeChange = func(p loop.ColorPreference) (err error) {
new_val := p == loop.LIGHT_COLOR_PREFERENCE
if new_val != use_light_colors {
use_light_colors = new_val
handler.preview_manager.invalidate_color_scheme_based_cached_items()
return handler.draw_screen()
}
return
}
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,206 +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('appearance', 'Appearance') # {{{
opt('show_preview', 'last', choices=('last', 'yes', 'y', 'true', 'no', 'n', 'false'), long_text='''
Whether to show a preview of the current file/directory. The default value of :code:`last` means remember the last
used value. This setting can be toggled withing the program.''')
opt('pygments_style', 'default', long_text='''
The pygments color scheme to use for syntax highlighting of file previews. See :link:`pygments
builtin styles <https://pygments.org/styles/>` for a list of schemes.
This sets the colors used for light color schemes, use :opt:`dark_pygments_style` to change the
colors for dark color schemes.
''')
opt('dark_pygments_style', 'github-dark', long_text='''
The pygments color scheme to use for syntax highlighting with dark colors. See :link:`pygments
builtin styles <https://pygments.org/styles/>` for a list of schemes.
This sets the colors used for dark color schemes, use :opt:`pygments_style` to change the
colors for light color schemes.''')
opt('syntax_aliases', 'pyj:py pyi:py recipe:py', ctype='strdict_ _:', option_type='syntax_aliases',
long_text='''
File extension aliases for syntax highlight. For example, to syntax highlight
:file:`file.xyz` as :file:`file.abc` use a setting of :code:`xyz:abc`.
Multiple aliases must be separated by spaces.
''')
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 or directory name for saving that does not yet exist.
When choosing existing directories, will accept the directory whoose
contents are being currently displayed as the choice.
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 .')
map('Change to parent directory', 'cd_parent shift+tab cd ..')
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+i toggle ignorefiles')
map('Toggle sorting by dates', 'toggle_sort_by_dates alt+d toggle sort_by_dates')
map('Toggle showing preview', 'toggle_preview alt+p toggle preview')
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
--display-title
type=bool-set
Show the window title at the top, useful when this kitten is used in an
OS window without a title bar.
--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,316 +0,0 @@
package choose_files
import (
"fmt"
"io/fs"
"maps"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"unicode/utf8"
"github.com/kovidgoyal/kitty/tools/highlight"
"github.com/kovidgoyal/kitty/tools/icons"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/humanize"
"github.com/kovidgoyal/kitty/tools/utils/style"
"github.com/kovidgoyal/kitty/tools/wcswidth"
)
var _ = fmt.Print
type Preview interface {
Render(h *Handler, x, y, width, height int)
IsValidForColorScheme(light bool) bool
}
type PreviewManager struct {
report_errors chan error
settings Settings
WakeupMainThread func() bool
cache map[string]Preview
lock sync.Mutex
highlighter highlight.Highlighter
}
func NewPreviewManager(err_chan chan error, settings Settings, WakeupMainThread func() bool) (ans *PreviewManager) {
defer func() { sanitize = ans.highlighter.Sanitize }()
return &PreviewManager{
report_errors: err_chan, settings: settings, WakeupMainThread: WakeupMainThread,
cache: make(map[string]Preview), highlighter: highlight.NewHighlighter(nil),
}
}
func (pm *PreviewManager) cached_preview(path string) Preview {
pm.lock.Lock()
defer pm.lock.Unlock()
return pm.cache[path]
}
func (pm *PreviewManager) set_cached_preview(path string, val Preview) {
pm.lock.Lock()
defer pm.lock.Unlock()
pm.cache[path] = val
}
func (h *Handler) render_wrapped_text_in_region(text string, x, y, width, height int, centered bool) int {
lines := style.WrapTextAsLines(text, width, style.WrapOptions{})
for i, line := range lines {
extra := 0
if centered {
extra = max(0, width-wcswidth.Stringwidth(line)) / 2
}
h.lp.MoveCursorTo(x+extra, y+i)
h.lp.QueueWriteString(line)
if i >= height {
break
}
}
return len(lines)
}
type MessagePreview struct {
title string
msg string
trailers []string
}
func (p MessagePreview) IsValidForColorScheme(bool) bool { return true }
func (p MessagePreview) Render(h *Handler, x, y, width, height int) {
offset := 0
if p.title != "" {
offset += h.render_wrapped_text_in_region(p.title, x, y, width, height, true)
}
offset += h.render_wrapped_text_in_region(p.msg, x, y+offset, width, height-offset, false)
limit := height - offset
if limit > 1 {
for i, line := range p.trailers {
text := wcswidth.TruncateToVisualLength(line, width-1)
if len(text) < len(line) {
text += "…"
}
h.lp.MoveCursorTo(x, y+offset+i-1)
if i >= limit {
h.lp.QueueWriteString("…")
break
}
h.lp.QueueWriteString(text)
}
}
}
func NewErrorPreview(err error) Preview {
sctx := style.Context{AllowEscapeCodes: true}
text := fmt.Sprintf("%s: %s", sctx.SprintFunc("fg=red")("Error"), err)
return &MessagePreview{msg: text}
}
var sanitize func(string) string
func write_file_metadata(abspath string, metadata fs.FileInfo, entries []fs.DirEntry) (header string, trailers []string) {
buf := strings.Builder{}
buf.Grow(4096)
add := func(key, val string) { fmt.Fprintf(&buf, "%s: %s\n", key, val) }
ftype := metadata.Mode().Type()
const file_icon = " "
switch ftype {
case 0:
add("Size", humanize.Bytes(uint64(metadata.Size())))
case fs.ModeSymlink:
if tgt, err := os.Readlink(abspath); err == nil {
add("Target", sanitize(tgt))
} else {
add("Target", err.Error())
}
case fs.ModeDir:
num_files, num_dirs := 0, 0
for _, e := range entries {
if e.IsDir() {
num_dirs++
} else {
num_files++
}
}
add("Children", fmt.Sprintf("%d %s %d %s", num_dirs, icons.IconForFileWithMode("dir", fs.ModeDir, false), num_files, file_icon))
}
add("Modified", humanize.Time(metadata.ModTime()))
add("Mode", metadata.Mode().String())
if len(entries) > 0 {
type entry struct {
lname string
ftype fs.FileMode
}
type_map := make(map[string]entry, len(entries))
for _, e := range entries {
type_map[e.Name()] = entry{strings.ToLower(e.Name()), e.Type()}
}
names := utils.Map(func(e fs.DirEntry) string { return e.Name() }, entries)
slices.SortFunc(names, func(a, b string) int { return strings.Compare(type_map[a].lname, type_map[b].lname) })
fmt.Fprintln(&buf, "Contents:")
for _, n := range names {
trailers = append(trailers, icons.IconForFileWithMode(n, type_map[n].ftype, false)+" "+sanitize(n))
}
}
return buf.String(), trailers
}
func NewDirectoryPreview(abspath string, metadata fs.FileInfo) Preview {
entries, err := os.ReadDir(abspath)
if err != nil {
return NewErrorPreview(fmt.Errorf("failed to read the directory %s with error: %w", abspath, err))
}
title := icons.IconForFileWithMode("dir", fs.ModeDir, false) + " Directory\n"
header, extra := write_file_metadata(abspath, metadata, entries)
return &MessagePreview{title: title, msg: header, trailers: extra}
}
func NewFileMetadataPreview(abspath string, metadata fs.FileInfo) Preview {
title := icons.IconForFileWithMode(filepath.Base(abspath), metadata.Mode().Type(), false) + " File"
h, t := write_file_metadata(abspath, metadata, nil)
return &MessagePreview{title: title, msg: h, trailers: t}
}
type highlighed_data struct {
text string
light bool
err error
}
type TextFilePreview struct {
plain_text, highlighted_text string
highlighted_chan chan highlighed_data
light bool
path string
}
func (p TextFilePreview) IsValidForColorScheme(light bool) bool { return p.light == light }
func (p *TextFilePreview) Render(h *Handler, x, y, width, height int) {
if p.highlighted_chan != nil {
select {
case hd := <-p.highlighted_chan:
p.highlighted_chan = nil
if hd.err == nil {
p.highlighted_text = hd.text
}
default:
}
}
text := p.highlighted_text
if text == "" {
text = p.plain_text
}
s := utils.NewLineScanner(text)
buf := strings.Builder{}
buf.Grow(1024 * height)
for num := 0; s.Scan() && num < height; num++ {
line := s.Text()
truncated := wcswidth.TruncateToVisualLength(line, width)
buf.WriteString(fmt.Sprintf(loop.MoveCursorToTemplate, y+num, x))
buf.WriteString(truncated)
if len(truncated) < len(line) {
wcswidth.KeepOnlyCSI(line[len(truncated):], &buf)
}
}
buf.WriteString("\x1b[m") // reset any highlight styles
h.lp.QueueWriteString(buf.String())
}
func NewTextFilePreview(abspath string, metadata fs.FileInfo, highlighted_chan chan highlighed_data, sanitize func(string) string) Preview {
data, err := os.ReadFile(abspath)
if err != nil {
return NewFileMetadataPreview(abspath, metadata)
}
text := utils.UnsafeBytesToString(data)
if !utf8.ValidString(text) {
text = "Error: not valid utf-8 text"
}
return &TextFilePreview{path: abspath, plain_text: sanitize(text), highlighted_chan: highlighted_chan, light: use_light_colors}
}
type style_resolver struct {
light bool
light_style, dark_style string
syntax_aliases map[string]string
}
func (s style_resolver) StyleName() string {
return utils.IfElse(s.light, s.light_style, s.dark_style)
}
func (s style_resolver) UseLightColors() bool { return s.light }
func (s style_resolver) SyntaxAliases() map[string]string { return s.syntax_aliases }
func (s style_resolver) TextForPath(path string) (string, error) {
ans, err := os.ReadFile(path)
if err == nil {
return utils.UnsafeBytesToString(ans), nil
}
return "", err
}
func (pm *PreviewManager) highlight_file_async(path string, output chan highlighed_data) {
s := style_resolver{light: use_light_colors, syntax_aliases: pm.settings.SyntaxAliases()}
s.light_style, s.dark_style = pm.settings.HighlightStyles()
go func() {
highlighted, err := pm.highlighter.HighlightFile(path, &s)
if err != nil {
debugprintln(fmt.Sprintf("Failed to highlight: %s with error: %s", path, err))
}
output <- highlighed_data{text: highlighted, err: err, light: s.light}
close(output)
pm.WakeupMainThread()
}()
}
func (pm *PreviewManager) invalidate_color_scheme_based_cached_items() {
pm.lock.Lock()
defer pm.lock.Unlock()
maps.DeleteFunc(pm.cache, func(key string, p Preview) bool { return !p.IsValidForColorScheme(use_light_colors) })
}
func (pm *PreviewManager) preview_for(abspath string, ftype fs.FileMode) (ans Preview) {
if ans = pm.cached_preview(abspath); ans != nil {
return ans
}
defer func() { pm.set_cached_preview(abspath, ans) }()
s, err := os.Lstat(abspath)
if err != nil {
return NewErrorPreview(err)
}
if s.IsDir() {
return NewDirectoryPreview(abspath, s)
}
if ftype&fs.ModeSymlink != 0 && ftype&SymlinkToDir != 0 {
s, err = os.Stat(abspath)
if err != nil {
return NewErrorPreview(err)
}
return NewDirectoryPreview(abspath, s)
}
mt := utils.GuessMimeType(filepath.Base(abspath))
const MAX_TEXT_FILE_SIZE = 16 * 1024 * 1024
if s.Size() <= MAX_TEXT_FILE_SIZE && (utils.KnownTextualMimes[mt] || strings.HasPrefix(mt, "text/")) {
ch := make(chan highlighed_data, 2)
pm.highlight_file_async(abspath, ch)
return NewTextFilePreview(abspath, s, ch, pm.highlighter.Sanitize)
}
return NewFileMetadataPreview(abspath, s)
}
func (h *Handler) draw_preview_content(x, y, width, height int) {
matches, _ := h.get_results()
r := matches.At(h.state.CurrentIndex())
if r == nil {
h.render_wrapped_text_in_region("No preview available", x, y, width, height, false)
return
}
abspath := filepath.Join(h.state.CurrentDir(), r.text)
if p := h.preview_manager.preview_for(abspath, r.ftype); p == nil {
h.render_wrapped_text_in_region("No preview available", x, y, width, height, false)
} else {
p.Render(h, x, y, width, height)
}
}

View File

@@ -1,369 +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):]
}
text = sanitize(text)
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 := sanitize(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, preview_width int) {
const BASE_COL_WIDTH = 40
available_width := h.screen_size.width - 2
show_preview := h.state.ShowPreview()
if show_preview && available_width < BASE_COL_WIDTH+30 {
show_preview = false
}
if show_preview {
switch {
case available_width < BASE_COL_WIDTH*2:
preview_width = max(30, available_width/2)
default:
preview_width = BASE_COL_WIDTH
}
available_width -= preview_width
}
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 = BASE_COL_WIDTH
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, preview_width
}
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_preview(y int) {
x := h.screen_size.width - h.state.last_render.preview_width
height := h.state.last_render.num_of_slots
buf := strings.Builder{}
buf.Grow(16 * height)
buf.WriteString(fmt.Sprintf(loop.MoveCursorToTemplate, y-1, x))
buf.WriteString("┬")
for i := range height {
buf.WriteString(fmt.Sprintf(loop.MoveCursorToTemplate, y+i, x))
buf.WriteString("│")
}
buf.WriteString(fmt.Sprintf(loop.MoveCursorToTemplate, y+height, x))
buf.WriteString("┴")
h.lp.QueueWriteString(buf.String())
h.draw_preview_content(x+1, y, h.state.last_render.preview_width-1, h.state.last_render.num_of_slots)
}
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
h.state.last_render.preview_width = 0
switch num {
case 0:
h.draw_no_matches_message(in_progress)
default:
num_cols, num_shown, h.state.last_render.preview_width = 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)
if h.state.last_render.preview_width > 0 {
h.draw_preview(y)
}
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,143 +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")
if h.state.DisplayTitle() {
h.lp.Println(h.state.WindowTitle())
}
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,751 +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
sort_order := 1
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]
sort_order = -1
} 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 sort_order * 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
HighlightStyles() (string, string)
SyntaxAliases() map[string]string
}
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.scorer.sort_by_last_modified = m.settings.SortByLastModified()
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,130 +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.ShowPreview(), "hide preview", "show preview"), func() {
h.state.show_preview = !h.state.show_preview
})
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,49 +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_size', '', long_text='''
The size in lines and columns of the file chooser popup window. By default it is full screen. For example:
:code:`file_chooser_size 25 80` will cause the popup to be of size 25 lines and 80 columns. Note that if you
use this option, depending on the compositor you are running, the popup window may not be properly modal.
''')
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,929 +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) {
// $XDG_DATA_DIRS defines the preference-ordered set of base directories
// to search for data files **in addition to the $XDG_DATA_HOME** base
// directory. The directories in $XDG_DATA_DIRS should be separated with
// a colon ':'.
// https://specifications.freedesktop.org/basedir-spec/0.8/#variables
data_dirs := os.Getenv("XDG_DATA_DIRS")
if data_dirs == "" {
data_dirs = "/usr/local/share:/usr/share"
}
data_home := os.Getenv("XDG_DATA_HOME")
if data_home == "" {
data_home = utils.Expanduser("~/.local/share")
}
return utils.Uniq(append([]string{data_home}, strings.Split(data_dirs, ":")...))
})
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
patched := 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")
}
patched = true
} else if in_preferred {
remove := false
for _, iface := range AllPortalInterfaces() {
if strings.HasPrefix(sl, iface) {
remove = true
break
}
}
if !remove {
lines = append(lines, line)
}
}
}
if !patched {
// the file was empty or did not contain a section
lines = append(lines, "[preferred]")
for _, iface := range AllPortalInterfaces() {
lines = append(lines, iface+"=kitty")
}
}
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() {
// Find-or-create the first available xdg-desktop-portals/portals directory
q := filepath.Join(x, "xdg-desktop-portal", "portals")
if (unix.Access(q, unix.W_OK) == nil && IsDir(q)) || (os.MkdirAll(q, 0o755) == 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
fmt.Printf("Patched %s to use the kitty portals\n", patched_file)
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("Created %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()
edge, lines, columns := `center`, ``, ``
if self.opts.File_chooser_size != "" {
l, c, _ := strings.Cut(strings.TrimSpace(self.opts.File_chooser_size), " ")
l, c = strings.TrimSpace(l), strings.TrimSpace(c)
if li, err := strconv.Atoi(l); err == nil {
if ci, err := strconv.Atoi(c); err == nil {
if li < 10 || ci < 40 {
log.Printf("file chooser size %s too small, ignoring", self.opts.File_chooser_size)
} else {
edge, lines, columns = `center-sized`, l, c
}
} else {
log.Printf("file chooser size %s invalid with error: %s\n", self.opts.File_chooser_size, err)
}
} else {
log.Printf("file chooser size %s invalid with error: %s\n", self.opts.File_chooser_size, err)
}
}
args := []string{
"+kitten", "panel", "--layer=overlay", "--edge=" + edge, "--focus-policy=exclusive",
"-o", "background_opacity=0.85", "--wait-for-single-instance-window-close",
"--grab-keyboard", "--single-instance", "--instance-group", "cfp-" + strconv.Itoa(os.Getpid()),
}
if edge == "center-sized" {
args = append(args, "--lines="+lines, "--columns="+columns)
}
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`, `--display-title`)
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
@@ -117,14 +117,26 @@ func hash_for_path(path string) (string, error) {
}
// Remove all control codes except newlines
func sanitize_control_codes(x string) string {
pat := utils.MustCompile("[\x00-\x09\x0b-\x1f\x7f\u0080-\u009f]")
return pat.ReplaceAllLiteralString(x, "░")
}
func sanitize_tabs_and_carriage_returns(x string) string {
return strings.NewReplacer("\t", conf.Replace_tab_by, "\r", "⏎").Replace(x)
}
func sanitize(x string) string {
return sanitize_control_codes(sanitize_tabs_and_carriage_returns(x))
}
func text_to_lines(text string) []string {
lines := make([]string, 0, 512)
splitlines_like_git(text, false, func(line string) { lines = append(lines, line) })
return lines
}
func sanitize(text string) string { return utils.ReplaceControlCodes(text, conf.Replace_tab_by, "\n") }
func lines_for_path(path string) ([]string, error) {
return lines_cache.GetOrCreate(path, func(path string) ([]string, error) {
ans, err := data_for_path(path)

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

@@ -3,39 +3,249 @@
package diff
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"github.com/kovidgoyal/kitty/tools/highlight"
"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"
"github.com/alecthomas/chroma/v2/styles"
)
var _ = fmt.Print
var _ = os.WriteFile
type prefer_light_colors bool
var ErrNoLexer = errors.New("No lexer available for this format")
var DefaultStyle = sync.OnceValue(func() *chroma.Style {
// Default style generated by python style.py default pygments.styles.default.DefaultStyle
// with https://raw.githubusercontent.com/alecthomas/chroma/master/_tools/style.py
return styles.Register(chroma.MustNewStyle("default", chroma.StyleEntries{
chroma.TextWhitespace: "#bbbbbb",
chroma.Comment: "italic #3D7B7B",
chroma.CommentPreproc: "noitalic #9C6500",
chroma.Keyword: "bold #008000",
chroma.KeywordPseudo: "nobold",
chroma.KeywordType: "nobold #B00040",
chroma.Operator: "#666666",
chroma.OperatorWord: "bold #AA22FF",
chroma.NameBuiltin: "#008000",
chroma.NameFunction: "#0000FF",
chroma.NameClass: "bold #0000FF",
chroma.NameNamespace: "bold #0000FF",
chroma.NameException: "bold #CB3F38",
chroma.NameVariable: "#19177C",
chroma.NameConstant: "#880000",
chroma.NameLabel: "#767600",
chroma.NameEntity: "bold #717171",
chroma.NameAttribute: "#687822",
chroma.NameTag: "bold #008000",
chroma.NameDecorator: "#AA22FF",
chroma.LiteralString: "#BA2121",
chroma.LiteralStringDoc: "italic",
chroma.LiteralStringInterpol: "bold #A45A77",
chroma.LiteralStringEscape: "bold #AA5D1F",
chroma.LiteralStringRegex: "#A45A77",
chroma.LiteralStringSymbol: "#19177C",
chroma.LiteralStringOther: "#008000",
chroma.LiteralNumber: "#666666",
chroma.GenericHeading: "bold #000080",
chroma.GenericSubheading: "bold #800080",
chroma.GenericDeleted: "#A00000",
chroma.GenericInserted: "#008400",
chroma.GenericError: "#E40000",
chroma.GenericEmph: "italic",
chroma.GenericStrong: "bold",
chroma.GenericPrompt: "bold #000080",
chroma.GenericOutput: "#717171",
chroma.GenericTraceback: "#04D",
chroma.Error: "border:#FF0000",
chroma.Background: " bg:#f8f8f8",
}))
})
func (s prefer_light_colors) StyleName() string {
return utils.IfElse(bool(s), conf.Pygments_style, conf.Dark_pygments_style)
// Clear the background colour.
func clear_background(style *chroma.Style) *chroma.Style {
builder := style.Builder()
bg := builder.Get(chroma.Background)
bg.Background = 0
bg.NoInherit = true
builder.AddEntry(chroma.Background, bg)
style, _ = builder.Build()
return style
}
func (s prefer_light_colors) UseLightColors() bool { return bool(s) }
func (s prefer_light_colors) SyntaxAliases() map[string]string { return conf.Syntax_aliases }
func (s prefer_light_colors) TextForPath(path string) (string, error) { return data_for_path(path) }
func ansi_formatter(w io.Writer, style *chroma.Style, it chroma.Iterator) (err error) {
const SGR_PREFIX = "\033["
const SGR_SUFFIX = "m"
style = clear_background(style)
before, after := make([]byte, 0, 64), make([]byte, 0, 64)
nl := []byte{'\n'}
write_sgr := func(which []byte) (err error) {
if len(which) > 1 {
if _, err = w.Write(utils.UnsafeStringToBytes(SGR_PREFIX)); err != nil {
return err
}
if _, err = w.Write(which[:len(which)-1]); err != nil {
return err
}
if _, err = w.Write(utils.UnsafeStringToBytes(SGR_SUFFIX)); err != nil {
return err
}
}
return
}
write := func(text string) (err error) {
if err = write_sgr(before); err != nil {
return err
}
if _, err = w.Write(utils.UnsafeStringToBytes(text)); err != nil {
return err
}
if err = write_sgr(after); err != nil {
return err
}
return
}
var highlighter = sync.OnceValue(func() highlight.Highlighter {
return highlight.NewHighlighter(sanitize)
})
for token := it(); token != chroma.EOF; token = it() {
entry := style.Get(token.Type)
before, after = before[:0], after[:0]
if !entry.IsZero() {
if entry.Bold == chroma.Yes {
before = append(before, '1', ';')
after = append(after, '2', '2', '1', ';')
}
if entry.Underline == chroma.Yes {
before = append(before, '4', ';')
after = append(after, '2', '4', ';')
}
if entry.Italic == chroma.Yes {
before = append(before, '3', ';')
after = append(after, '2', '3', ';')
}
if entry.Colour.IsSet() {
before = append(before, fmt.Sprintf("38:2:%d:%d:%d;", entry.Colour.Red(), entry.Colour.Green(), entry.Colour.Blue())...)
after = append(after, '3', '9', ';')
}
}
// independently format each line in a multiline token, needed for the diff kitten highlighting to work, also
// pagers like less reset SGR formatting at line boundaries
text := sanitize(token.Value)
for text != "" {
idx := strings.IndexByte(text, '\n')
if idx < 0 {
if err = write(text); err != nil {
return err
}
break
}
if err = write(text[:idx]); err != nil {
return err
}
if _, err = w.Write(nl); err != nil {
return err
}
text = text[idx+1:]
}
}
return nil
}
func resolved_chroma_style(use_light_colors bool) *chroma.Style {
name := utils.IfElse(use_light_colors, conf.Pygments_style, conf.Dark_pygments_style)
var style *chroma.Style
if name == "default" {
style = DefaultStyle()
} else {
style = styles.Get(name)
}
if style == nil {
if resolved_colors.Background.IsDark() && !resolved_colors.Foreground.IsDark() {
style = styles.Get("monokai")
if style == nil {
style = styles.Get("github-dark")
}
} else {
style = DefaultStyle()
}
if style == nil {
style = styles.Fallback
}
}
return style
}
var tokens_map map[string][]chroma.Token
var mu sync.Mutex
func highlight_file(path string, use_light_colors bool) (highlighted string, err error) {
defer func() {
if r := recover(); r != nil {
e, ok := r.(error)
if !ok {
e = fmt.Errorf("%v", r)
}
err = e
}
}()
filename_for_detection := filepath.Base(path)
ext := filepath.Ext(filename_for_detection)
if ext != "" {
ext = strings.ToLower(ext[1:])
r := conf.Syntax_aliases[ext]
if r != "" {
filename_for_detection = "file." + r
}
}
text, err := data_for_path(path)
if err != nil {
return "", err
}
mu.Lock()
if tokens_map == nil {
tokens_map = make(map[string][]chroma.Token)
}
tokens := tokens_map[path]
mu.Unlock()
if tokens == nil {
lexer := lexers.Match(filename_for_detection)
if lexer == nil {
lexer = lexers.Analyse(text)
}
if lexer == nil {
return "", fmt.Errorf("Cannot highlight %#v: %w", path, ErrNoLexer)
}
lexer = chroma.Coalesce(lexer)
iterator, err := lexer.Tokenise(nil, text)
if err != nil {
return "", err
}
tokens = iterator.Tokens()
mu.Lock()
tokens_map[path] = tokens
mu.Unlock()
}
formatter := chroma.FormatterFunc(ansi_formatter)
w := strings.Builder{}
w.Grow(len(text) * 2)
err = formatter.Format(&w, resolved_chroma_style(use_light_colors), chroma.Literator(tokens...))
// os.WriteFile(filepath.Base(path+".highlighted"), []byte(w.String()), 0o600)
return w.String(), err
}
func highlight_all(paths []string, light bool) {
ctx := images.Context{}
srd := prefer_light_colors(light)
ctx.Parallel(0, len(paths), func(nums <-chan int) {
for i := range nums {
path := paths[i]
raw, err := highlighter().HighlightFile(path, &srd)
raw, err := highlight_file(path, light)
if err != nil {
continue
}

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"

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