mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-14 04:28:00 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
715645e69c | ||
|
|
4e697abb34 | ||
|
|
96b0c463e8 |
@@ -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()
|
||||
|
||||
@@ -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}"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -106,69 +106,6 @@ consumption to do the same tasks.
|
||||
Detailed list of changes
|
||||
-------------------------------------
|
||||
|
||||
0.42.2 [2025-07-16]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- A new :ref:`protocol extension <mouse_leave_window>` to notify terminal programs that have turned on SGR Pixel mouse reporting when the mouse leaves the window (:disc:`8808`)
|
||||
|
||||
- clipboard kitten: Can now optionally take a password to avoid repeated
|
||||
permission prompts when accessing the clipboard. Based on a
|
||||
:ref:`protocol extension <clipboard_repeated_permission>`. (:iss:`8789`)
|
||||
|
||||
- A new :option:`launch --hold-after-ssh` to not close a launched window
|
||||
that connects directly to a remote host because of
|
||||
:option:`launch --cwd`:code:`=current` when the connection ends (:pull:`8807`)
|
||||
|
||||
- Fix :opt:`remember_window_position` not working because of a stupid typo (:iss:`8646`)
|
||||
|
||||
- A new :option:`kitty --grab-keyboard` that can be used to grab the keyboard so that global shortcuts are sent to kitty instead
|
||||
|
||||
- Remote control: Fix holding a remote control socket open causing the kitty I/O thread to go into a loop and not respond on other remote control sockets (:disc:`8670`)
|
||||
|
||||
- hints kitten: Preserve line breaks when the hint is over a line break (:iss:`8674`)
|
||||
|
||||
- Fix a segfault when using the :ac:`copy_ansi_to_clipboard` action (:iss:`8682`)
|
||||
|
||||
- Fix a crash when using linear easing curves for animations (:iss:`8692`)
|
||||
|
||||
- Graphics protocol: Add a note clarifying image update behavior on re-transmission (:iss:`8701`)
|
||||
|
||||
- Wayland GNOME: Fix incorrect OS Window tracking because GNOME has started
|
||||
activating windows on non-current workspaces (:iss:`8716`)
|
||||
|
||||
- Fix a regression in 0.40.0 that broke rendering of VS15 variation selectors in some circumstances (:iss:`8731`, :iss:`8794`)
|
||||
|
||||
- Fix a regression in 0.40.0 that broke serialization of tab characters as ANSI text (:iss:`8741`)
|
||||
|
||||
- Fix a regression in 0.40.0 that broke erasing of characters in a line in the presence of wide characters (:iss:`8758`)
|
||||
|
||||
- Fix a regression in 0.40.0 that broke hyperlinking of wide characters (:iss:`8796`)
|
||||
|
||||
- Fix a regression that broke using :kbd:`esc` to exit visual select window mode (:iss:`8767`)
|
||||
|
||||
- kitten run-shell: Fix SIGINT blocked when execing the shell (:iss:`8754`)
|
||||
|
||||
0.42.1 [2025-05-17]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Fix ambiguous width and private use characters not being rendered when used with variable width text-sizing protocol escape codes
|
||||
|
||||
- Quick access terminal: Restore focus to previously active window when hiding the quick access terminal window on macOS (:iss:`8627`)
|
||||
|
||||
- Wayland: Fix an abort if the terminal program sets a window title longer than 2KB that contains CSI escape sequences and multibyte UTF-8 (:iss:`8619`)
|
||||
|
||||
- Quick access terminal: Allow toggling the window to full screen using the standard kitty :sc:`toggle_fullscreen` shortcut (:iss:`8626`)
|
||||
|
||||
- Quick access terminal: Allow configuring the monitor to display the panel on in Wayland/X11 (:iss:`8630`)
|
||||
|
||||
- A new setting :opt:`remember_window_position` to optionally use the position of the last closed kitty OS Window as the position of the first kitty OS Window when running a new kitty instance (:pull:`8601`)
|
||||
|
||||
- Panel kitten: A new ``center-sized`` value for :option:`--edge <kitty +kitten panel --edge>` to allow easily creating sized and centered panels
|
||||
|
||||
- Wayland: The `kitty --name` flag now sets the XDG *window tag* on compositors
|
||||
that support the `xdg-toplevel-tag <https://wayland.app/protocols/xdg-toplevel-tag-v1>`__ protocol.
|
||||
|
||||
|
||||
0.42.0 [2025-05-11]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -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
|
||||
------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
41
docs/conf.py
41
docs/conf.py
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -139,10 +139,6 @@ images.
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
A video player that can play videos in the terminal.
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
mpv --profile=sw-fast --vo=kitty --vo-kitty-use-shm=yes --really-quiet video.mkv
|
||||
|
||||
.. _tool_timg:
|
||||
|
||||
`timg <https://github.com/hzeller/timg>`_
|
||||
@@ -336,14 +332,6 @@ A kitten-panel based desktop panel for your desktop
|
||||
Miscellaneous
|
||||
------------------
|
||||
|
||||
.. tool_doom:
|
||||
|
||||
DOOM
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Play the classic shooter DOOM in `kitty <https://github.com/cryptocode/terminal-doom>`__ or even inside `neovim inside kitty
|
||||
<https://github.com/seandewar/actually-doom.nvim>`__.
|
||||
|
||||
.. tool_gattino:
|
||||
|
||||
`gattino <https://github.com/salvozappa/gattino>`__
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
Selecting files, fast
|
||||
========================
|
||||
|
||||
.. only:: man
|
||||
|
||||
Overview
|
||||
--------------
|
||||
|
||||
.. versionadded:: 0.43.0
|
||||
|
||||
The choose-files kitten is designed to allow you to select files, very fast,
|
||||
with just a few key strokes. It operates like `fzf
|
||||
<https://github.com/junegunn/fzf/>`__ and similar fuzzy finders, except that
|
||||
it is specialised for finding files. As such it supports features such as
|
||||
filtering by file type, file type icons, content previews (coming soon) and
|
||||
so on, out of the box. It can be used as a drop in (but much more efficient and
|
||||
keyboard friendly) replacement for the :guilabel:`File open and save`
|
||||
dialog boxes common to GUI programs. On Linux, with the help of the
|
||||
:doc:`desktop-ui </kittens/desktop-ui>` kitten, you can even convince
|
||||
most GUI programs on your computer to use this kitten instead of regular file
|
||||
dialogs.
|
||||
|
||||
Simply run it as::
|
||||
|
||||
kitten choose-files
|
||||
|
||||
to select a single file from the tree rooted at the current working directory.
|
||||
|
||||
Configuration
|
||||
------------------------
|
||||
|
||||
You can configure various aspects of the kitten's operation by creating a
|
||||
:file:`choose-files.conf` in your :ref:`kitty config folder <confloc>`.
|
||||
See below for the supported configuration directives.
|
||||
|
||||
|
||||
.. include:: /generated/conf-kitten-choose_files.rst
|
||||
|
||||
|
||||
.. include:: /generated/cli-kitten-choose_files.rst
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
Using terminal programs to provide Linux desktop components
|
||||
===============================================================
|
||||
|
||||
.. only:: man
|
||||
|
||||
Overview
|
||||
--------------
|
||||
|
||||
.. versionadded:: 0.43.0
|
||||
|
||||
Power users of terminals on Linux also often like to use bare bones window
|
||||
managers instead of full fledged desktop environments. This kitten helps
|
||||
provide parts of the desktop environment that are missing from such setups,
|
||||
and does so using keyboard friendly, terminal first UI components. Some of its
|
||||
features are:
|
||||
|
||||
* Replace the typical File Open/Save dialogs used in GUI programs with the
|
||||
fast and keyboard centric :doc:`choose-files </kittens/choose-files>` kitten
|
||||
running in a semi-transparent kitty overlay.
|
||||
|
||||
* Allow simple command line based management of the desktop light/dark modes.
|
||||
@@ -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.
|
||||
|
||||
@@ -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 ...
|
||||
|
||||
@@ -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
|
||||
---------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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::
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
11101
gen/nerd-fonts-glyphs.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,18 @@ from functools import lru_cache, partial
|
||||
from html.entities import html5
|
||||
from io import StringIO
|
||||
from math import ceil, log
|
||||
from typing import Callable, DefaultDict, Iterator, Literal, NamedTuple, Optional, Protocol, Sequence, TypedDict, TypeVar, Union
|
||||
from typing import (
|
||||
Callable,
|
||||
DefaultDict,
|
||||
Iterator,
|
||||
Literal,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Protocol,
|
||||
Sequence,
|
||||
TypedDict,
|
||||
Union,
|
||||
)
|
||||
from urllib.request import urlopen
|
||||
|
||||
if __name__ == '__main__' and not __package__:
|
||||
@@ -32,7 +43,8 @@ if len(non_characters) != 66:
|
||||
emoji_skin_tone_modifiers = frozenset(range(0x1f3fb, 0x1F3FF + 1))
|
||||
|
||||
|
||||
def fetch_url(url: str) -> str:
|
||||
def get_data(fname: str, folder: str = 'UCD') -> Iterable[str]:
|
||||
url = f'https://www.unicode.org/Public/{folder}/latest/{fname}'
|
||||
bn = os.path.basename(url)
|
||||
local = os.path.join('/tmp', bn)
|
||||
if os.path.exists(local):
|
||||
@@ -42,12 +54,7 @@ def fetch_url(url: str) -> str:
|
||||
data = urlopen(url).read()
|
||||
with open(local, 'wb') as f:
|
||||
f.write(data)
|
||||
return data.decode()
|
||||
|
||||
|
||||
def get_data(fname: str, folder: str = 'UCD') -> Iterable[str]:
|
||||
url = f'https://www.unicode.org/Public/{folder}/latest/{fname}'
|
||||
for line in fetch_url(url).splitlines():
|
||||
for line in data.decode('utf-8').splitlines():
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#'):
|
||||
yield line
|
||||
@@ -144,14 +151,13 @@ def parse_ucd() -> None:
|
||||
# the future.
|
||||
marks.add(codepoint)
|
||||
|
||||
gndata = fetch_url('https://raw.githubusercontent.com/ryanoasis/nerd-fonts/refs/heads/master/glyphnames.json')
|
||||
for name, val in json.loads(gndata).items():
|
||||
if name != 'METADATA':
|
||||
codepoint = int(val['code'], 16)
|
||||
category, sep, name = name.rpartition('-')
|
||||
name = name or category
|
||||
name = name.replace('_', ' ')
|
||||
print(11111111, name)
|
||||
with open('gen/nerd-fonts-glyphs.txt') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
code, category, name = line.split(' ', 2)
|
||||
codepoint = int(code, 16)
|
||||
if name and codepoint not in name_map:
|
||||
name_map[codepoint] = name.upper()
|
||||
for word in name.lower().split():
|
||||
@@ -456,9 +462,7 @@ def mask_for(bits: int) -> int:
|
||||
return ~((~0) << bits)
|
||||
|
||||
|
||||
HashableType = TypeVar('HashableType', bound=Hashable)
|
||||
|
||||
def splitbins(t: tuple[HashableType, ...], property_size: int, use_fixed_shift: int = 0) -> tuple[list[int], list[int], list[HashableType], int]:
|
||||
def splitbins[T: Hashable](t: tuple[T, ...], property_size: int, use_fixed_shift: int = 0) -> tuple[list[int], list[int], list[T], int]:
|
||||
if use_fixed_shift:
|
||||
candidates = range(use_fixed_shift, use_fixed_shift + 1)
|
||||
else:
|
||||
@@ -469,8 +473,8 @@ def splitbins(t: tuple[HashableType, ...], property_size: int, use_fixed_shift:
|
||||
n >>= 1
|
||||
maxshift += 1
|
||||
candidates = range(maxshift + 1)
|
||||
t3: list[HashableType] = []
|
||||
tmap: dict[HashableType, int] = {}
|
||||
t3: list[T] = []
|
||||
tmap: dict[T, int] = {}
|
||||
seen = set()
|
||||
for x in t:
|
||||
if x not in seen:
|
||||
|
||||
@@ -828,7 +828,7 @@ int _glfwPlatformInit(bool *supports_window_occlusion)
|
||||
{
|
||||
debug_key("---------------- key down -------------------\n");
|
||||
debug_key("%s\n", [[event description] UTF8String]);
|
||||
if (!_glfw.ignoreOSKeyboardProcessing && !_glfw.keyboard_grabbed) {
|
||||
if (!_glfw.ignoreOSKeyboardProcessing) {
|
||||
// first check if there is a global menu bar shortcut
|
||||
if ([[NSApp mainMenu] performKeyEquivalent:event]) {
|
||||
debug_key("keyDown triggered global menu bar action ignoring\n");
|
||||
@@ -1144,4 +1144,3 @@ void _glfwPlatformUpdateTimer(unsigned long long timer_id, monotonic_t interval,
|
||||
}
|
||||
|
||||
void _glfwPlatformInputColorScheme(GLFWColorScheme appearance UNUSED) { }
|
||||
bool _glfwPlatformGrabKeyboard(bool grab UNUSED) { return true; /* directly uses _glfw.keyboard_grabbed */ }
|
||||
|
||||
1
glfw/cocoa_platform.h
vendored
1
glfw/cocoa_platform.h
vendored
@@ -125,7 +125,6 @@ typedef struct _GLFWwindowNS
|
||||
id delegate;
|
||||
id view;
|
||||
id layer;
|
||||
pid_t previous_front_most_application;
|
||||
|
||||
bool maximized;
|
||||
bool retina;
|
||||
|
||||
@@ -1940,22 +1940,6 @@ screen_for_window_center(_GLFWwindow *window) {
|
||||
return NSScreen.mainScreen;
|
||||
}
|
||||
|
||||
const GLFWLayerShellConfig*
|
||||
_glfwPlatformGetLayerShellConfig(_GLFWwindow *window) {
|
||||
return &window->ns.layer_shell.config;
|
||||
}
|
||||
|
||||
static NSScreen*
|
||||
screen_for_name(const char *name) {
|
||||
int count = 0;
|
||||
GLFWmonitor **monitors = glfwGetMonitors(&count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
const char *q = glfwGetMonitorName(monitors[i]);
|
||||
if (q && strcmp(q, name) == 0) return ((_GLFWmonitor*)monitors[i])->ns.screen;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool
|
||||
_glfwPlatformSetLayerShellConfig(_GLFWwindow* window, const GLFWLayerShellConfig *value) {
|
||||
#define config window->ns.layer_shell.config
|
||||
@@ -1986,10 +1970,6 @@ _glfwPlatformSetLayerShellConfig(_GLFWwindow* window, const GLFWLayerShellConfig
|
||||
// HACK: Changing the style mask can cause the first responder to be cleared
|
||||
[nswindow makeFirstResponder:window->ns.view];
|
||||
NSScreen *screen = screen_for_window_center(window);
|
||||
if (config.output_name[0]) {
|
||||
NSScreen *q = screen_for_name(config.output_name);
|
||||
if (q) screen = q;
|
||||
}
|
||||
unsigned cell_width, cell_height; double left_edge_spacing, top_edge_spacing, right_edge_spacing, bottom_edge_spacing;
|
||||
float xscale = (float)config.expected.xscale, yscale = (float)config.expected.yscale;
|
||||
_glfwPlatformGetWindowContentScale(window, &xscale, &yscale);
|
||||
@@ -2031,11 +2011,6 @@ _glfwPlatformSetLayerShellConfig(_GLFWwindow* window, const GLFWLayerShellConfig
|
||||
x += width - panel_width + 1.;
|
||||
width = panel_width;
|
||||
break;
|
||||
case GLFW_EDGE_CENTER_SIZED:
|
||||
x += (width - panel_width) / 2;
|
||||
y += (height - panel_height) / 2;
|
||||
width = panel_width; height = panel_height;
|
||||
break;
|
||||
default: // top left
|
||||
y += height - panel_height + 1.;
|
||||
height = panel_height; width = panel_width;
|
||||
@@ -2045,10 +2020,8 @@ _glfwPlatformSetLayerShellConfig(_GLFWwindow* window, const GLFWLayerShellConfig
|
||||
if (height < 1.) height = NSWidth(screen.visibleFrame);
|
||||
}
|
||||
|
||||
if (config.edge != GLFW_EDGE_CENTER_SIZED) {
|
||||
x += config.requested_left_margin; width -= config.requested_left_margin + config.requested_right_margin;
|
||||
y += config.requested_bottom_margin; height -= config.requested_top_margin + config.requested_bottom_margin;
|
||||
}
|
||||
x += config.requested_left_margin; width -= config.requested_left_margin + config.requested_right_margin;
|
||||
y += config.requested_bottom_margin; height -= config.requested_top_margin + config.requested_bottom_margin;
|
||||
|
||||
[nswindow setAnimationBehavior:animation_behavior];
|
||||
[nswindow setLevel:level];
|
||||
@@ -2228,39 +2201,14 @@ void _glfwPlatformMaximizeWindow(_GLFWwindow* window)
|
||||
|
||||
void _glfwPlatformShowWindow(_GLFWwindow* window)
|
||||
{
|
||||
NSRunningApplication *app = [[NSWorkspace sharedWorkspace] frontmostApplication];
|
||||
window->ns.previous_front_most_application = 0;
|
||||
if (app && app.processIdentifier != getpid()) window->ns.previous_front_most_application = app.processIdentifier;
|
||||
if (window->ns.layer_shell.is_active && window->ns.layer_shell.config.type == GLFW_LAYER_SHELL_BACKGROUND) {
|
||||
[window->ns.object orderBack:nil];
|
||||
} else [window->ns.object orderFront:nil];
|
||||
debug("Previously active application pid: %d bundle identifier: %s\n",
|
||||
window->ns.previous_front_most_application, app ? app.bundleIdentifier.UTF8String : "");
|
||||
}
|
||||
|
||||
void _glfwPlatformHideWindow(_GLFWwindow* window)
|
||||
{
|
||||
[window->ns.object orderOut:nil];
|
||||
pid_t prev_app_pid = window->ns.previous_front_most_application; window->ns.previous_front_most_application = 0;
|
||||
NSRunningApplication *app;
|
||||
if (window->ns.layer_shell.is_active && prev_app_pid > 0 && (app = [NSRunningApplication runningApplicationWithProcessIdentifier:prev_app_pid])) {
|
||||
unsigned num_visible = 0;
|
||||
for (_GLFWwindow *w = _glfw.windowListHead; w; w = w->next) {
|
||||
if (_glfwPlatformWindowVisible(w)) num_visible++;
|
||||
}
|
||||
if (!num_visible) {
|
||||
// yieldActivationToApplication was introduced in macOS 14 (Sonoma)
|
||||
SEL selector = NSSelectorFromString(@"yieldActivationToApplication:");
|
||||
if ([NSApp respondsToSelector:selector]) {
|
||||
[NSApp performSelector:selector withObject:app];
|
||||
[app activateWithOptions:0];
|
||||
} else {
|
||||
#define NSApplicationActivateIgnoringOtherApps 2
|
||||
[app activateWithOptions:NSApplicationActivateIgnoringOtherApps];
|
||||
#undef NSApplicationActivateIgnoringOtherApps
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _glfwPlatformRequestWindowAttention(_GLFWwindow* window UNUSED)
|
||||
|
||||
@@ -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
13
glfw/glfw3.h
vendored
@@ -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
7
glfw/input.c
vendored
@@ -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
8
glfw/internal.h
vendored
@@ -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
15
glfw/monitor.c
vendored
@@ -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;
|
||||
|
||||
@@ -84,10 +84,8 @@
|
||||
"staging/fractional-scale/fractional-scale-v1.xml",
|
||||
"staging/single-pixel-buffer/single-pixel-buffer-v1.xml",
|
||||
"unstable/idle-inhibit/idle-inhibit-unstable-v1.xml",
|
||||
"unstable/keyboard-shortcuts-inhibit/keyboard-shortcuts-inhibit-unstable-v1.xml",
|
||||
"staging/xdg-toplevel-icon/xdg-toplevel-icon-v1.xml",
|
||||
"staging/xdg-system-bell/xdg-system-bell-v1.xml",
|
||||
"staging/xdg-toplevel-tag/xdg-toplevel-tag-v1.xml",
|
||||
|
||||
"kwin-blur-v1.xml",
|
||||
"wlr-layer-shell-unstable-v1.xml"
|
||||
|
||||
13
glfw/window.c
vendored
13
glfw/window.c
vendored
@@ -495,10 +495,6 @@ GLFWAPI void glfwWindowHintString(int hint, const char* value)
|
||||
strncpy(_glfw.hints.window.wl.appId, value,
|
||||
sizeof(_glfw.hints.window.wl.appId) - 1);
|
||||
return;
|
||||
case GLFW_WAYLAND_WINDOW_TAG:
|
||||
strncpy(_glfw.hints.window.wl.windowTag, value,
|
||||
sizeof(_glfw.hints.window.wl.windowTag) - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
_glfwInputError(GLFW_INVALID_ENUM, "Invalid window hint string 0x%08X", hint);
|
||||
@@ -555,15 +551,6 @@ GLFWAPI void glfwSetWindowShouldClose(GLFWwindow* handle, int value)
|
||||
window->shouldClose = value;
|
||||
}
|
||||
|
||||
GLFWAPI const GLFWLayerShellConfig* glfwGetLayerShellConfig(GLFWwindow* handle) {
|
||||
_GLFWwindow* window = (_GLFWwindow*) handle;
|
||||
assert(window != NULL);
|
||||
|
||||
_GLFW_REQUIRE_INIT_OR_RETURN(false);
|
||||
return _glfwPlatformGetLayerShellConfig(window);
|
||||
}
|
||||
|
||||
|
||||
GLFWAPI bool glfwSetLayerShellConfig(GLFWwindow* handle, const GLFWLayerShellConfig *value) {
|
||||
_GLFWwindow* window = (_GLFWwindow*) handle;
|
||||
assert(window != NULL);
|
||||
|
||||
10
glfw/wl_init.c
vendored
10
glfw/wl_init.c
vendored
@@ -600,16 +600,11 @@ static void registryHandleGlobal(void* data UNUSED,
|
||||
else if (is(zwp_idle_inhibit_manager_v1)) {
|
||||
_glfw.wl.idle_inhibit_manager = wl_registry_bind(registry, name, &zwp_idle_inhibit_manager_v1_interface, 1);
|
||||
}
|
||||
else if (is(zwp_keyboard_shortcuts_inhibit_manager_v1)) {
|
||||
_glfw.wl.keyboard_shortcuts_inhibit_manager = wl_registry_bind(registry, name, &zwp_keyboard_shortcuts_inhibit_manager_v1_interface, 1);
|
||||
}
|
||||
else if (is(xdg_toplevel_icon_manager_v1)) {
|
||||
_glfw.wl.xdg_toplevel_icon_manager_v1 = wl_registry_bind(registry, name, &xdg_toplevel_icon_manager_v1_interface, 1);
|
||||
}
|
||||
else if (is(xdg_system_bell_v1)) {
|
||||
_glfw.wl.xdg_system_bell_v1 = wl_registry_bind(registry, name, &xdg_system_bell_v1_interface, 1);
|
||||
} else if (is(xdg_toplevel_tag_manager_v1)) {
|
||||
_glfw.wl.xdg_toplevel_tag_manager_v1 = wl_registry_bind(registry, name, &xdg_toplevel_tag_manager_v1_interface, 1);
|
||||
}
|
||||
#undef is
|
||||
}
|
||||
@@ -721,7 +716,6 @@ get_compositor_missing_capabilities(void) {
|
||||
C(cursor_shape, wp_cursor_shape_manager_v1); C(layer_shell, zwlr_layer_shell_v1);
|
||||
C(single_pixel_buffer, wp_single_pixel_buffer_manager_v1); C(preferred_scale, has_preferred_buffer_scale);
|
||||
C(idle_inhibit, idle_inhibit_manager); C(icon, xdg_toplevel_icon_manager_v1); C(bell, xdg_system_bell_v1);
|
||||
C(window-tag, xdg_toplevel_tag_manager_v1); C(keyboard_shortcuts_inhibit, keyboard_shortcuts_inhibit_manager);
|
||||
if (_glfw.wl.xdg_wm_base_version < 6) p += snprintf(p, sizeof(buf) - (p - buf), "%s ", "window-state-suspended");
|
||||
if (_glfw.wl.xdg_wm_base_version < 5) p += snprintf(p, sizeof(buf) - (p - buf), "%s ", "window-capabilities");
|
||||
#undef C
|
||||
@@ -900,8 +894,6 @@ void _glfwPlatformTerminate(void)
|
||||
xdg_toplevel_icon_manager_v1_destroy(_glfw.wl.xdg_toplevel_icon_manager_v1);
|
||||
if (_glfw.wl.xdg_system_bell_v1)
|
||||
xdg_system_bell_v1_destroy(_glfw.wl.xdg_system_bell_v1);
|
||||
if (_glfw.wl.xdg_toplevel_tag_manager_v1)
|
||||
xdg_toplevel_tag_manager_v1_destroy(_glfw.wl.xdg_toplevel_tag_manager_v1);
|
||||
if (_glfw.wl.wp_single_pixel_buffer_manager_v1)
|
||||
wp_single_pixel_buffer_manager_v1_destroy(_glfw.wl.wp_single_pixel_buffer_manager_v1);
|
||||
if (_glfw.wl.wp_cursor_shape_manager_v1)
|
||||
@@ -916,8 +908,6 @@ void _glfwPlatformTerminate(void)
|
||||
zwlr_layer_shell_v1_destroy(_glfw.wl.zwlr_layer_shell_v1);
|
||||
if (_glfw.wl.idle_inhibit_manager)
|
||||
zwp_idle_inhibit_manager_v1_destroy(_glfw.wl.idle_inhibit_manager);
|
||||
if (_glfw.wl.keyboard_shortcuts_inhibit_manager)
|
||||
zwp_keyboard_shortcuts_inhibit_manager_v1_destroy(_glfw.wl.keyboard_shortcuts_inhibit_manager);
|
||||
|
||||
if (_glfw.wl.registry)
|
||||
wl_registry_destroy(_glfw.wl.registry);
|
||||
|
||||
24
glfw/wl_monitor.c
vendored
24
glfw/wl_monitor.c
vendored
@@ -41,15 +41,20 @@ static void outputHandleGeometry(void* data,
|
||||
int32_t physicalWidth,
|
||||
int32_t physicalHeight,
|
||||
int32_t subpixel UNUSED,
|
||||
const char* make UNUSED,
|
||||
const char* model UNUSED,
|
||||
const char* make,
|
||||
const char* model,
|
||||
int32_t transform UNUSED)
|
||||
{
|
||||
struct _GLFWmonitor *monitor = data;
|
||||
char name[1024];
|
||||
|
||||
monitor->wl.x = x;
|
||||
monitor->wl.y = y;
|
||||
monitor->widthMM = physicalWidth;
|
||||
monitor->heightMM = physicalHeight;
|
||||
|
||||
snprintf(name, sizeof(name), "%s %s", make, model);
|
||||
monitor->name = _glfw_strdup(name);
|
||||
}
|
||||
|
||||
static void outputHandleMode(void* data,
|
||||
@@ -100,20 +105,15 @@ static void outputHandleName(void* data,
|
||||
struct wl_output* output UNUSED,
|
||||
const char* name) {
|
||||
struct _GLFWmonitor *monitor = data;
|
||||
if (name) {
|
||||
if (monitor->name) free((void*)monitor->name);
|
||||
monitor->name = _glfw_strdup(name);
|
||||
}
|
||||
if (name) strncpy(monitor->wl.friendly_name, name, sizeof(monitor->wl.friendly_name)-1);
|
||||
}
|
||||
|
||||
static void outputHandleDescription(void* data,
|
||||
struct wl_output* output UNUSED,
|
||||
const char* description) {
|
||||
struct _GLFWmonitor *monitor = data;
|
||||
if (description) {
|
||||
if (monitor->description) free((void*)monitor->description);
|
||||
monitor->description = _glfw_strdup(description);
|
||||
}
|
||||
if (description) strncpy(monitor->wl.description, description, sizeof(monitor->wl.description)-1);
|
||||
|
||||
}
|
||||
|
||||
static const struct wl_output_listener outputListener = {
|
||||
@@ -142,8 +142,8 @@ void _glfwAddOutputWayland(uint32_t name, uint32_t version)
|
||||
return;
|
||||
}
|
||||
|
||||
// The actual name of this output will be set in the handlers.
|
||||
monitor = _glfwAllocMonitor("unnamed", 0, 0);
|
||||
// The actual name of this output will be set in the geometry handler.
|
||||
monitor = _glfwAllocMonitor(NULL, 0, 0);
|
||||
|
||||
output = wl_registry_bind(_glfw.wl.registry,
|
||||
name,
|
||||
|
||||
8
glfw/wl_platform.h
vendored
8
glfw/wl_platform.h
vendored
@@ -66,10 +66,8 @@ typedef VkBool32 (APIENTRY *PFN_vkGetPhysicalDeviceWaylandPresentationSupportKHR
|
||||
#include "wayland-wlr-layer-shell-unstable-v1-client-protocol.h"
|
||||
#include "wayland-single-pixel-buffer-v1-client-protocol.h"
|
||||
#include "wayland-idle-inhibit-unstable-v1-client-protocol.h"
|
||||
#include "wayland-keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h"
|
||||
#include "wayland-xdg-toplevel-icon-v1-client-protocol.h"
|
||||
#include "wayland-xdg-system-bell-v1-client-protocol.h"
|
||||
#include "wayland-xdg-toplevel-tag-v1-client-protocol.h"
|
||||
|
||||
#define _glfw_dlopen(name) dlopen(name, RTLD_LAZY | RTLD_LOCAL)
|
||||
#define _glfw_dlclose(handle) dlclose(handle)
|
||||
@@ -212,7 +210,7 @@ typedef struct _GLFWwindowWayland
|
||||
double cursorPosX, cursorPosY, allCursorPosX, allCursorPosY;
|
||||
|
||||
char* title;
|
||||
char appId[256], windowTag[256];
|
||||
char appId[256];
|
||||
|
||||
// We need to track the monitors the window spans on to calculate the
|
||||
// optimal scaling factor.
|
||||
@@ -291,7 +289,6 @@ typedef struct _GLFWwindowWayland
|
||||
WaylandWindowState toplevel_states;
|
||||
uint32_t decoration_mode;
|
||||
} current, pending;
|
||||
struct zwp_keyboard_shortcuts_inhibitor_v1 *keyboard_shortcuts_inhibitor;
|
||||
} _GLFWwindowWayland;
|
||||
|
||||
typedef enum _GLFWWaylandOfferType
|
||||
@@ -343,7 +340,6 @@ typedef struct _GLFWlibraryWayland
|
||||
struct xdg_activation_v1* xdg_activation_v1;
|
||||
struct xdg_toplevel_icon_manager_v1* xdg_toplevel_icon_manager_v1;
|
||||
struct xdg_system_bell_v1* xdg_system_bell_v1;
|
||||
struct xdg_toplevel_tag_manager_v1* xdg_toplevel_tag_manager_v1;
|
||||
struct wp_cursor_shape_manager_v1* wp_cursor_shape_manager_v1;
|
||||
struct wp_cursor_shape_device_v1* wp_cursor_shape_device_v1;
|
||||
struct wp_fractional_scale_manager_v1 *wp_fractional_scale_manager_v1;
|
||||
@@ -352,7 +348,6 @@ typedef struct _GLFWlibraryWayland
|
||||
struct zwlr_layer_shell_v1* zwlr_layer_shell_v1; uint32_t zwlr_layer_shell_v1_version;
|
||||
struct wp_single_pixel_buffer_manager_v1 *wp_single_pixel_buffer_manager_v1;
|
||||
struct zwp_idle_inhibit_manager_v1* idle_inhibit_manager;
|
||||
struct zwp_keyboard_shortcuts_inhibit_manager_v1 *keyboard_shortcuts_inhibit_manager;
|
||||
|
||||
int compositorVersion;
|
||||
int seatVersion;
|
||||
@@ -411,6 +406,7 @@ typedef struct _GLFWmonitorWayland
|
||||
{
|
||||
struct wl_output* output;
|
||||
uint32_t name;
|
||||
char friendly_name[64], description[64];
|
||||
int currentMode;
|
||||
|
||||
int x;
|
||||
|
||||
46
glfw/wl_window.c
vendored
46
glfw/wl_window.c
vendored
@@ -46,18 +46,6 @@
|
||||
static bool
|
||||
is_layer_shell(_GLFWwindow *window) { return window->wl.layer_shell.config.type != GLFW_LAYER_SHELL_NONE; }
|
||||
|
||||
static void
|
||||
inhibit_shortcuts_for(_GLFWwindow *window, bool inhibit) {
|
||||
if (inhibit) {
|
||||
if (window->wl.keyboard_shortcuts_inhibitor) return;
|
||||
window->wl.keyboard_shortcuts_inhibitor = zwp_keyboard_shortcuts_inhibit_manager_v1_inhibit_shortcuts(_glfw.wl.keyboard_shortcuts_inhibit_manager, window->wl.surface, _glfw.wl.seat);
|
||||
} else {
|
||||
if (!window->wl.keyboard_shortcuts_inhibitor) return;
|
||||
zwp_keyboard_shortcuts_inhibitor_v1_destroy(window->wl.keyboard_shortcuts_inhibitor);
|
||||
window->wl.keyboard_shortcuts_inhibitor = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
activation_token_done(void *data, struct xdg_activation_token_v1 *xdg_token, const char *token) {
|
||||
for (size_t i = 0; i < _glfw.wl.activation_requests.sz; i++) {
|
||||
@@ -628,7 +616,6 @@ static bool createSurface(_GLFWwindow* window,
|
||||
update_regions(window);
|
||||
|
||||
wl_surface_set_buffer_scale(window->wl.surface, scale);
|
||||
if (_glfw.keyboard_grabbed) inhibit_shortcuts_for(window, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -809,6 +796,7 @@ apply_xdg_configure_changes(_GLFWwindow *window) {
|
||||
window->wl.current.toplevel_states = new_states;
|
||||
window->wl.current.width = width;
|
||||
window->wl.current.height = height;
|
||||
_glfwInputWindowFocus(window, window->wl.current.toplevel_states & TOPLEVEL_STATE_ACTIVATED);
|
||||
if (live_resize_done) report_live_resize(window, false);
|
||||
}
|
||||
}
|
||||
@@ -983,7 +971,7 @@ find_output_by_name(const char* name) {
|
||||
if (!name || !name[0]) return NULL;
|
||||
for (int i = 0; i < _glfw.monitorCount; i++) {
|
||||
_GLFWmonitor *m = _glfw.monitors[i];
|
||||
if (strcmp(m->name, name) == 0) return m->wl.output;
|
||||
if (strcmp(m->wl.friendly_name, name) == 0) return m->wl.output;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
@@ -1040,13 +1028,12 @@ layer_set_properties(const _GLFWwindow *window, bool during_creation, uint32_t w
|
||||
if (!config.override_exclusive_zone) exclusive_zone = width;
|
||||
break;
|
||||
case GLFW_EDGE_CENTER:
|
||||
break;
|
||||
case GLFW_EDGE_CENTER_SIZED:
|
||||
panel_width = width; panel_height = height;
|
||||
which_anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT | ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM;
|
||||
break;
|
||||
case GLFW_EDGE_NONE:
|
||||
which_anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP;
|
||||
panel_width = width; panel_height = height;
|
||||
panel_width = width;
|
||||
panel_height = height;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1156,7 +1143,7 @@ create_layer_shell_surface(_GLFWwindow *window) {
|
||||
struct wl_output *wl_output = find_output_by_name(window->wl.layer_shell.config.output_name);
|
||||
#define ls window->wl.layer_shell.zwlr_layer_surface_v1
|
||||
ls = zwlr_layer_shell_v1_get_layer_surface(
|
||||
_glfw.wl.zwlr_layer_shell_v1, window->wl.surface, wl_output, get_layer_shell_layer(window), window->wl.appId[0] ? window->wl.appId : "kitty");
|
||||
_glfw.wl.zwlr_layer_shell_v1, window->wl.surface, wl_output, get_layer_shell_layer(window), "kitty");
|
||||
if (!ls) {
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: layer-surface creation failed");
|
||||
return false;
|
||||
@@ -1210,10 +1197,8 @@ create_window_desktop_surface(_GLFWwindow* window)
|
||||
zxdg_toplevel_decoration_v1_add_listener(window->wl.xdg.decoration, &xdgDecorationListener, window);
|
||||
}
|
||||
|
||||
if (window->wl.appId[0])
|
||||
if (strlen(window->wl.appId))
|
||||
xdg_toplevel_set_app_id(window->wl.xdg.toplevel, window->wl.appId);
|
||||
if (window->wl.windowTag[0] && _glfw.wl.xdg_toplevel_tag_manager_v1)
|
||||
xdg_toplevel_tag_manager_v1_set_toplevel_tag(_glfw.wl.xdg_toplevel_tag_manager_v1, window->wl.xdg.toplevel, window->wl.windowTag);
|
||||
|
||||
if (window->wl.title)
|
||||
xdg_toplevel_set_title(window->wl.xdg.toplevel, window->wl.title);
|
||||
@@ -1481,8 +1466,6 @@ void _glfwPlatformDestroyWindow(_GLFWwindow* window)
|
||||
if (window->id == _glfw.wl.keyRepeatInfo.keyboardFocusId) {
|
||||
_glfw.wl.keyRepeatInfo.keyboardFocusId = 0;
|
||||
}
|
||||
if (window->wl.keyboard_shortcuts_inhibitor)
|
||||
zwp_keyboard_shortcuts_inhibitor_v1_destroy(window->wl.keyboard_shortcuts_inhibitor);
|
||||
|
||||
if (window->wl.temp_buffer_used_during_window_creation)
|
||||
wl_buffer_destroy(window->wl.temp_buffer_used_during_window_creation);
|
||||
@@ -2834,16 +2817,6 @@ _glfwPlatformSetWindowBlur(_GLFWwindow *window, int blur_radius) {
|
||||
return has_blur ? 1 : 0;
|
||||
}
|
||||
|
||||
bool
|
||||
_glfwPlatformGrabKeyboard(bool grab) {
|
||||
if (!_glfw.wl.keyboard_shortcuts_inhibit_manager) {
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR, "The Wayland compositor does not implement inhibit-keyboard-shortcuts, cannot grab keyboard");
|
||||
return false;
|
||||
}
|
||||
for (_GLFWwindow* window = _glfw.windowListHead; window; window = window->next) inhibit_shortcuts_for(window, grab);
|
||||
return true;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
////// GLFW native API //////
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
@@ -2919,9 +2892,8 @@ GLFWAPI void glfwWaylandRedrawCSDWindowTitle(GLFWwindow *handle) {
|
||||
if (csd_change_title(window)) commit_window_surface_if_safe(window);
|
||||
}
|
||||
|
||||
const GLFWLayerShellConfig*
|
||||
_glfwPlatformGetLayerShellConfig(_GLFWwindow *window) {
|
||||
return &window->wl.layer_shell.config;
|
||||
GLFWAPI GLFWLayerShellConfig* glfwWaylandLayerShellConfig(GLFWwindow *handle) {
|
||||
return &((_GLFWwindow*)handle)->wl.layer_shell.config;
|
||||
}
|
||||
|
||||
GLFWAPI bool glfwIsLayerShellSupported(void) { return _glfw.wl.zwlr_layer_shell_v1 != NULL; }
|
||||
|
||||
24
glfw/x11_window.c
vendored
24
glfw/x11_window.c
vendored
@@ -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
15
go.mod
@@ -1,4 +1,4 @@
|
||||
module github.com/kovidgoyal/kitty
|
||||
module kitty
|
||||
|
||||
go 1.23.0
|
||||
|
||||
@@ -6,27 +6,22 @@ toolchain go1.24.1
|
||||
|
||||
require (
|
||||
github.com/ALTree/bigfloat v0.2.0
|
||||
github.com/alecthomas/chroma/v2 v2.19.0
|
||||
github.com/bmatcuk/doublestar/v4 v4.9.0
|
||||
github.com/alecthomas/chroma/v2 v2.17.2
|
||||
github.com/bmatcuk/doublestar/v4 v4.8.1
|
||||
github.com/dlclark/regexp2 v1.11.5
|
||||
github.com/edwvee/exiffix v0.0.0-20240229113213-0dbb146775be
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1
|
||||
github.com/kovidgoyal/imaging v1.6.4
|
||||
github.com/seancfoley/ipaddress-go v1.7.1
|
||||
github.com/shirou/gopsutil/v3 v3.24.5
|
||||
github.com/zeebo/xxh3 v1.0.2
|
||||
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b
|
||||
golang.org/x/image v0.29.0
|
||||
golang.org/x/sys v0.34.0
|
||||
golang.org/x/text v0.27.0
|
||||
golang.org/x/image v0.26.0
|
||||
golang.org/x/sys v0.32.0
|
||||
howett.net/plist v1.0.1
|
||||
)
|
||||
|
||||
// Uncomment the following to use a local checkout of dbus
|
||||
// replace github.com/kovidgoyal/dbus => ../dbus
|
||||
|
||||
require (
|
||||
github.com/disintegration/imaging v1.6.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
|
||||
20
go.sum
20
go.sum
@@ -2,12 +2,12 @@ github.com/ALTree/bigfloat v0.2.0 h1:AwNzawrpFuw55/YDVlcPw0F0cmmXrmngBHhVrvdXPvM
|
||||
github.com/ALTree/bigfloat v0.2.0/go.mod h1:+NaH2gLeY6RPBPPQf4aRotPPStg+eXc8f9ZaE4vRfD4=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.19.0 h1:Im+SLRgT8maArxv81mULDWN8oKxkzboH07CHesxElq4=
|
||||
github.com/alecthomas/chroma/v2 v2.19.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
|
||||
github.com/alecthomas/chroma/v2 v2.17.2 h1:Rm81SCZ2mPoH+Q8ZCc/9YvzPUN/E7HgPiPJD8SLV6GI=
|
||||
github.com/alecthomas/chroma/v2 v2.17.2/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/bmatcuk/doublestar/v4 v4.9.0 h1:DBvuZxjdKkRP/dr4GVV4w2fnmrk5Hxc90T51LZjv0JA=
|
||||
github.com/bmatcuk/doublestar/v4 v4.9.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
|
||||
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
@@ -28,8 +28,6 @@ github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSo
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1 h1:rMY/hWfcVzBm6BLX6YLA+gLJEpuXBed/VP6YEkXt8R4=
|
||||
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1/go.mod h1:RbNG3Q1g6GUy1/WzWVx+S24m7VKyvl57vV+cr2hpt50=
|
||||
github.com/kovidgoyal/imaging v1.6.4 h1:K0idhRPXnRrJBKnBYcTfI1HTWSNDeAn7hYDvf9I0dCk=
|
||||
github.com/kovidgoyal/imaging v1.6.4/go.mod h1:bEIgsaZmXlvFfkv/CUxr9rJook6AQkJnpB5EPosRfRY=
|
||||
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik=
|
||||
@@ -65,18 +63,16 @@ github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaD
|
||||
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b h1:r+vk0EmXNmekl0S0BascoeeoHk/L7wmaW2QF90K+kYI=
|
||||
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas=
|
||||
golang.org/x/image v0.29.0/go.mod h1:RVJROnf3SLK8d26OW91j4FrIHGbsJ8QnbEocVTOWQDA=
|
||||
golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
|
||||
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,814 +0,0 @@
|
||||
package choose_files
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/cli"
|
||||
"github.com/kovidgoyal/kitty/tools/config"
|
||||
"github.com/kovidgoyal/kitty/tools/ignorefiles"
|
||||
"github.com/kovidgoyal/kitty/tools/tty"
|
||||
"github.com/kovidgoyal/kitty/tools/tui"
|
||||
"github.com/kovidgoyal/kitty/tools/tui/loop"
|
||||
"github.com/kovidgoyal/kitty/tools/tui/readline"
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
"golang.org/x/text/message"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
var debugprintln = tty.DebugPrintln
|
||||
|
||||
type Screen int
|
||||
|
||||
const (
|
||||
NORMAL Screen = iota
|
||||
SAVE_FILE
|
||||
)
|
||||
|
||||
type Mode int
|
||||
|
||||
const (
|
||||
SELECT_SINGLE_FILE Mode = iota
|
||||
SELECT_MULTIPLE_FILES
|
||||
SELECT_SAVE_FILE
|
||||
SELECT_SAVE_FILES
|
||||
SELECT_SAVE_DIR
|
||||
SELECT_SINGLE_DIR
|
||||
SELECT_MULTIPLE_DIRS
|
||||
)
|
||||
|
||||
func (m Mode) CanSelectNonExistent() bool {
|
||||
switch m {
|
||||
case SELECT_SAVE_FILE, SELECT_SAVE_DIR, SELECT_SAVE_FILES:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m Mode) AllowsMultipleSelection() bool {
|
||||
switch m {
|
||||
case SELECT_MULTIPLE_FILES, SELECT_MULTIPLE_DIRS, SELECT_SAVE_FILES:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m Mode) OnlyDirs() bool {
|
||||
switch m {
|
||||
case SELECT_SINGLE_DIR, SELECT_MULTIPLE_DIRS, SELECT_SAVE_DIR:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m Mode) SelectFiles() bool {
|
||||
switch m {
|
||||
case SELECT_SINGLE_FILE, SELECT_MULTIPLE_FILES, SELECT_SAVE_FILE, SELECT_SAVE_FILES:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m Mode) WindowTitle() string {
|
||||
switch m {
|
||||
case SELECT_SINGLE_FILE:
|
||||
return "Choose an existing file"
|
||||
case SELECT_MULTIPLE_FILES:
|
||||
return "Choose one or more existing files"
|
||||
case SELECT_SAVE_FILE:
|
||||
return "Choose a file to save"
|
||||
case SELECT_SAVE_DIR:
|
||||
return "Choose a directory to save"
|
||||
case SELECT_SINGLE_DIR:
|
||||
return "Choose an existing directory"
|
||||
case SELECT_MULTIPLE_DIRS:
|
||||
return "Choose one or more directories"
|
||||
case SELECT_SAVE_FILES:
|
||||
return "Choose files to save"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type render_state struct {
|
||||
num_matches, num_of_slots, num_before, num_per_column, num_columns, num_shown int
|
||||
first_idx CollectionIndex
|
||||
}
|
||||
|
||||
type State struct {
|
||||
base_dir string
|
||||
current_dir string
|
||||
select_dirs bool
|
||||
multiselect bool
|
||||
search_text string
|
||||
mode Mode
|
||||
suggested_save_file_name string
|
||||
suggested_save_file_path string
|
||||
window_title string
|
||||
screen Screen
|
||||
current_filter string
|
||||
filter_map map[string]Filter
|
||||
filter_names []string
|
||||
show_hidden bool
|
||||
respect_ignores bool
|
||||
sort_by_last_modified bool
|
||||
global_ignores ignorefiles.IgnoreFile
|
||||
keyboard_shortcuts []*config.KeyAction
|
||||
|
||||
selections []string
|
||||
current_idx CollectionIndex
|
||||
last_render render_state
|
||||
mouse_state tui.MouseState
|
||||
redraw_needed bool
|
||||
}
|
||||
|
||||
func (s State) ShowHidden() bool { return s.show_hidden }
|
||||
func (s State) RespectIgnores() bool { return s.respect_ignores }
|
||||
func (s State) SortByLastModified() bool { return s.sort_by_last_modified }
|
||||
func (s State) GlobalIgnores() ignorefiles.IgnoreFile { return s.global_ignores }
|
||||
func (s State) BaseDir() string { return utils.IfElse(s.base_dir == "", default_cwd, s.base_dir) }
|
||||
func (s State) Filter() Filter { return s.filter_map[s.current_filter] }
|
||||
func (s State) SelectDirs() bool { return s.select_dirs }
|
||||
func (s State) Multiselect() bool { return s.multiselect }
|
||||
func (s State) String() string { return utils.Repr(s) }
|
||||
func (s State) SearchText() string { return s.search_text }
|
||||
func (s State) OnlyDirs() bool { return s.mode.OnlyDirs() }
|
||||
func (s *State) SetSearchText(val string) {
|
||||
if s.search_text != val {
|
||||
s.search_text = val
|
||||
s.current_idx = CollectionIndex{}
|
||||
}
|
||||
}
|
||||
func (s *State) SetCurrentDir(val string) {
|
||||
if q, err := filepath.Abs(val); err == nil {
|
||||
val = q
|
||||
}
|
||||
if s.CurrentDir() != val {
|
||||
s.search_text = ""
|
||||
s.current_idx = CollectionIndex{}
|
||||
s.current_dir = val
|
||||
}
|
||||
}
|
||||
func (s State) CurrentIndex() CollectionIndex { return s.current_idx }
|
||||
func (s *State) SetCurrentIndex(val CollectionIndex) { s.current_idx = val }
|
||||
func (s State) CurrentDir() string {
|
||||
return utils.IfElse(s.current_dir == "", s.BaseDir(), s.current_dir)
|
||||
}
|
||||
func (s State) WindowTitle() string {
|
||||
if s.window_title == "" {
|
||||
return s.mode.WindowTitle()
|
||||
}
|
||||
return s.window_title
|
||||
}
|
||||
|
||||
func (s *State) AddSelection(abspath string) bool {
|
||||
if !slices.Contains(s.selections, abspath) {
|
||||
s.selections = append(s.selections, abspath)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *State) ToggleSelection(abspath string) (added bool) {
|
||||
before := len(s.selections)
|
||||
s.selections = slices.DeleteFunc(s.selections, func(x string) bool { return x == abspath })
|
||||
if len(s.selections) == before {
|
||||
s.selections = append(s.selections, abspath)
|
||||
added = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *State) IsSelected(x *ResultItem) bool {
|
||||
if len(s.selections) == 0 {
|
||||
return false
|
||||
}
|
||||
q := filepath.Join(s.CurrentDir(), x.text)
|
||||
return slices.Contains(s.selections, q)
|
||||
}
|
||||
|
||||
type ScreenSize struct {
|
||||
width, height, cell_width, cell_height, width_px, height_px int
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
state State
|
||||
screen_size ScreenSize
|
||||
result_manager *ResultManager
|
||||
lp *loop.Loop
|
||||
rl *readline.Readline
|
||||
err_chan chan error
|
||||
shortcut_tracker config.ShortcutTracker
|
||||
msg_printer *message.Printer
|
||||
spinner *tui.Spinner
|
||||
}
|
||||
|
||||
func (h *Handler) draw_screen() (err error) {
|
||||
h.state.redraw_needed = false
|
||||
h.lp.StartAtomicUpdate()
|
||||
defer func() {
|
||||
h.state.mouse_state.UpdateHoveredIds()
|
||||
h.state.mouse_state.ApplyHoverStyles(h.lp)
|
||||
h.lp.EndAtomicUpdate()
|
||||
}()
|
||||
h.lp.ClearScreen()
|
||||
h.state.mouse_state.ClearCellRegions()
|
||||
switch h.state.screen {
|
||||
case NORMAL:
|
||||
matches, is_complete := h.get_results()
|
||||
h.lp.SetWindowTitle(h.state.WindowTitle())
|
||||
defer func() { // so that the cursor ends up in the right place
|
||||
h.lp.MoveCursorTo(1, 1)
|
||||
h.draw_search_bar(0)
|
||||
}()
|
||||
y := SEARCH_BAR_HEIGHT
|
||||
footer_height, err := h.draw_footer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
y += h.draw_results(y, footer_height, matches, !is_complete)
|
||||
case SAVE_FILE:
|
||||
err = h.draw_save_file_name_screen()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func load_config(opts *Options) (ans *Config, err error) {
|
||||
ans = NewConfig()
|
||||
p := config.ConfigParser{LineHandler: ans.Parse}
|
||||
err = p.LoadConfig("choose-files.conf", opts.Config, opts.Override)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ans.KeyboardShortcuts = config.ResolveShortcuts(ans.KeyboardShortcuts)
|
||||
return ans, nil
|
||||
}
|
||||
|
||||
func (h *Handler) init_sizes(new_size loop.ScreenSize) {
|
||||
h.screen_size.width = int(new_size.WidthCells)
|
||||
h.screen_size.height = int(new_size.HeightCells)
|
||||
h.screen_size.cell_width = int(new_size.CellWidth)
|
||||
h.screen_size.cell_height = int(new_size.CellHeight)
|
||||
h.screen_size.width_px = int(new_size.WidthPx)
|
||||
h.screen_size.height_px = int(new_size.HeightPx)
|
||||
h.rl.ClearCachedScreenSize()
|
||||
}
|
||||
|
||||
func (h *Handler) OnInitialize() (ans string, err error) {
|
||||
if sz, err := h.lp.ScreenSize(); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
h.init_sizes(sz)
|
||||
}
|
||||
h.lp.AllowLineWrapping(false)
|
||||
h.lp.SetCursorShape(loop.BAR_CURSOR, true)
|
||||
h.lp.StartBracketedPaste()
|
||||
if h.state.suggested_save_file_path != "" {
|
||||
switch h.state.mode {
|
||||
case SELECT_SAVE_FILE, SELECT_SAVE_DIR:
|
||||
if s, err := os.Stat(h.state.suggested_save_file_path); err == nil {
|
||||
if (s.IsDir() && h.state.mode != SELECT_SAVE_FILE) || (!s.IsDir() && h.state.mode == SELECT_SAVE_FILE) {
|
||||
h.state.SetCurrentDir(filepath.Dir(h.state.suggested_save_file_path))
|
||||
h.state.SetSearchText(filepath.Base(h.state.suggested_save_file_name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
h.result_manager.set_root_dir()
|
||||
h.draw_screen()
|
||||
return
|
||||
}
|
||||
|
||||
func (h *Handler) current_abspath() string {
|
||||
matches, _ := h.get_results()
|
||||
if r := matches.At(h.state.CurrentIndex()); r != nil {
|
||||
return filepath.Join(h.state.CurrentDir(), r.text)
|
||||
}
|
||||
return ""
|
||||
|
||||
}
|
||||
|
||||
func (s *State) CanSelect(r *ResultItem) bool {
|
||||
return utils.IfElse(s.OnlyDirs(), r.ftype.IsDir(), !r.ftype.IsDir())
|
||||
}
|
||||
|
||||
func (h *Handler) toggle_selection_at(idx CollectionIndex) bool {
|
||||
matches, _ := h.get_results()
|
||||
if r := matches.At(idx); r != nil && h.state.CanSelect(r) {
|
||||
m := filepath.Join(h.state.CurrentDir(), r.text)
|
||||
if added := h.state.ToggleSelection(m); added {
|
||||
h.result_manager.last_click_anchor = &idx
|
||||
} else {
|
||||
h.result_manager.last_click_anchor = nil
|
||||
if len(h.state.selections) > 0 {
|
||||
x := utils.NewSetWithItems(h.state.selections...)
|
||||
cdir := h.state.CurrentDir()
|
||||
h.result_manager.last_click_anchor = matches.Closest(idx, func(q *ResultItem) bool { return x.Has(filepath.Join(cdir, q.text)) })
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *Handler) toggle_selection() bool {
|
||||
return h.toggle_selection_at(h.state.CurrentIndex())
|
||||
}
|
||||
|
||||
func (h *Handler) change_current_dir(dir string) {
|
||||
if dir != h.state.CurrentDir() {
|
||||
h.state.SetCurrentDir(dir)
|
||||
h.result_manager.set_root_dir()
|
||||
h.state.last_render = render_state{}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) set_query(q string) {
|
||||
if q != h.state.SearchText() {
|
||||
h.state.SetSearchText(q)
|
||||
h.result_manager.set_query()
|
||||
h.state.last_render = render_state{}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) set_filter(filter_name string) {
|
||||
if filter_name != h.state.current_filter {
|
||||
h.state.current_filter = filter_name
|
||||
h.result_manager.set_filter()
|
||||
h.state.last_render = render_state{}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) change_to_current_dir_if_possible() error {
|
||||
if m := h.current_abspath(); m != "" {
|
||||
if st, err := os.Stat(m); err == nil {
|
||||
if !st.IsDir() {
|
||||
m = filepath.Dir(m)
|
||||
}
|
||||
h.change_current_dir(m)
|
||||
return h.draw_screen()
|
||||
}
|
||||
}
|
||||
h.lp.Beep()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) finish_selection() error {
|
||||
if h.state.mode.CanSelectNonExistent() && len(h.state.selections) == 0 {
|
||||
return h.switch_to_save_file_name_mode()
|
||||
}
|
||||
h.lp.Quit(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) change_filter(delta int) bool {
|
||||
if len(h.state.filter_names) < 2 {
|
||||
return false
|
||||
}
|
||||
idx := slices.Index(h.state.filter_names, h.state.current_filter)
|
||||
idx += delta + len(h.state.filter_names)
|
||||
idx %= len(h.state.filter_names)
|
||||
h.set_filter(h.state.filter_names[idx])
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *Handler) switch_to_save_file_name_mode() error {
|
||||
name := h.state.suggested_save_file_name
|
||||
if h.state.SearchText() != "" {
|
||||
name = h.state.SearchText()
|
||||
}
|
||||
h.initialize_save_file_name(name)
|
||||
return h.draw_screen()
|
||||
}
|
||||
|
||||
func (h *Handler) accept_idx(idx CollectionIndex) (accepted bool, err error) {
|
||||
matches, _ := h.get_results()
|
||||
if r := matches.At(idx); r != nil {
|
||||
m := filepath.Join(h.state.CurrentDir(), r.text)
|
||||
|
||||
if h.state.mode.SelectFiles() {
|
||||
var s os.FileInfo
|
||||
if s, err = os.Stat(m); err != nil {
|
||||
return false, nil
|
||||
}
|
||||
if s.IsDir() {
|
||||
if h.state.mode.CanSelectNonExistent() {
|
||||
return true, h.switch_to_save_file_name_mode()
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
h.state.AddSelection(m)
|
||||
h.result_manager.last_click_anchor = &idx
|
||||
if len(h.state.selections) > 0 {
|
||||
return true, h.finish_selection()
|
||||
}
|
||||
return true, h.draw_screen()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (h *Handler) dispatch_action(name, args string) (err error) {
|
||||
switch name {
|
||||
case "quit":
|
||||
h.lp.Quit(1)
|
||||
case "next":
|
||||
if n, nerr := strconv.Atoi(args); nerr == nil {
|
||||
h.next_result(n)
|
||||
} else {
|
||||
switch args {
|
||||
case "":
|
||||
h.next_result(1)
|
||||
case "left":
|
||||
h.move_sideways(true)
|
||||
case "right":
|
||||
h.move_sideways(false)
|
||||
case "first":
|
||||
h.state.SetCurrentIndex(CollectionIndex{})
|
||||
h.state.last_render.num_before = 0
|
||||
case "last":
|
||||
matches, _ := h.get_results()
|
||||
h.state.SetCurrentIndex(matches.IncrementIndexWithWrapAround(CollectionIndex{}, -1))
|
||||
h.state.last_render.num_before = 0
|
||||
case "first_on_screen":
|
||||
h.state.SetCurrentIndex(h.state.last_render.first_idx)
|
||||
h.state.last_render.num_before = 0
|
||||
case "last_on_screen":
|
||||
matches, _ := h.get_results()
|
||||
h.state.SetCurrentIndex(matches.IncrementIndexWithWrapAround(h.state.last_render.first_idx, h.state.last_render.num_shown-1))
|
||||
h.state.last_render.num_before = h.state.last_render.num_shown - 1
|
||||
}
|
||||
}
|
||||
return h.draw_screen()
|
||||
case "next_filter":
|
||||
if n, nerr := strconv.Atoi(args); nerr == nil {
|
||||
h.change_filter(n)
|
||||
return h.draw_screen()
|
||||
}
|
||||
h.lp.Beep()
|
||||
case "select":
|
||||
if !h.toggle_selection() {
|
||||
h.lp.Beep()
|
||||
} else {
|
||||
return h.draw_screen()
|
||||
}
|
||||
case "accept":
|
||||
accepted, aerr := h.accept_idx(h.state.CurrentIndex())
|
||||
if aerr != nil {
|
||||
return aerr
|
||||
}
|
||||
if !accepted {
|
||||
h.lp.Beep()
|
||||
}
|
||||
case "typename":
|
||||
if !h.state.mode.CanSelectNonExistent() {
|
||||
h.lp.Beep()
|
||||
} else {
|
||||
return h.switch_to_save_file_name_mode()
|
||||
}
|
||||
case "toggle":
|
||||
switch args {
|
||||
case "dotfiles":
|
||||
h.state.show_hidden = !h.state.show_hidden
|
||||
h.result_manager.set_show_hidden()
|
||||
return h.draw_screen()
|
||||
case "ignorefiles":
|
||||
h.state.respect_ignores = !h.state.respect_ignores
|
||||
h.result_manager.set_respect_ignores()
|
||||
return h.draw_screen()
|
||||
case "sort_by_dates":
|
||||
h.state.sort_by_last_modified = !h.state.sort_by_last_modified
|
||||
h.result_manager.set_sort_by_last_modified()
|
||||
return h.draw_screen()
|
||||
default:
|
||||
h.lp.Beep()
|
||||
}
|
||||
case "cd":
|
||||
switch args {
|
||||
case "current":
|
||||
return h.change_to_current_dir_if_possible()
|
||||
case "up":
|
||||
curr := h.state.CurrentDir()
|
||||
switch curr {
|
||||
case "/":
|
||||
case ".":
|
||||
if curr, err = os.Getwd(); err == nil && curr != "/" {
|
||||
h.change_current_dir(filepath.Dir(curr))
|
||||
return h.draw_screen()
|
||||
}
|
||||
default:
|
||||
h.change_current_dir(filepath.Dir(curr))
|
||||
return h.draw_screen()
|
||||
}
|
||||
h.lp.Beep()
|
||||
default:
|
||||
args = utils.Expanduser(args)
|
||||
if st, serr := os.Stat(args); serr != nil || !st.IsDir() {
|
||||
h.lp.Beep()
|
||||
return
|
||||
}
|
||||
if absp, err := filepath.Abs(args); err == nil {
|
||||
h.change_current_dir(absp)
|
||||
return h.draw_screen()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (h *Handler) OnKeyEvent(ev *loop.KeyEvent) (err error) {
|
||||
switch h.state.screen {
|
||||
case NORMAL:
|
||||
if h.handle_edit_keys(ev) {
|
||||
ev.Handled = true
|
||||
h.draw_screen()
|
||||
}
|
||||
ac := h.shortcut_tracker.Match(ev, h.state.keyboard_shortcuts)
|
||||
if ac != nil {
|
||||
ev.Handled = true
|
||||
return h.dispatch_action(ac.Name, ac.Args)
|
||||
}
|
||||
case SAVE_FILE:
|
||||
err = h.save_file_name_handle_key(ev)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (h *Handler) OnMouseEvent(event *loop.MouseEvent) (err error) {
|
||||
h.state.redraw_needed = h.state.mouse_state.UpdateState(event)
|
||||
if err = h.state.mouse_state.DispatchEventToHoveredRegions(event); err != nil {
|
||||
return
|
||||
}
|
||||
if h.state.redraw_needed {
|
||||
err = h.draw_screen()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (h *Handler) OnText(text string, from_key_event, in_bracketed_paste bool) (err error) {
|
||||
switch h.state.screen {
|
||||
case NORMAL:
|
||||
h.set_query(h.state.SearchText() + text)
|
||||
return h.draw_screen()
|
||||
case SAVE_FILE:
|
||||
if err = h.rl.OnText(text, from_key_event, in_bracketed_paste); err == nil {
|
||||
err = h.draw_screen()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type CachedValues struct {
|
||||
Show_hidden bool `json:"show_hidden"`
|
||||
Respect_ignores bool `json:"respect_ignores"`
|
||||
Sort_by_last_modified bool `json:"sort_by_last_modified"`
|
||||
}
|
||||
|
||||
const cache_filename = "choose-files.json"
|
||||
|
||||
var cached_values = sync.OnceValue(func() *CachedValues {
|
||||
ans := CachedValues{Respect_ignores: true}
|
||||
fname := filepath.Join(utils.CacheDir(), cache_filename)
|
||||
if data, err := os.ReadFile(fname); err == nil {
|
||||
_ = json.Unmarshal(data, &ans)
|
||||
}
|
||||
return &ans
|
||||
})
|
||||
|
||||
func (s State) save_cached_values() {
|
||||
c := CachedValues{Show_hidden: s.show_hidden, Respect_ignores: s.respect_ignores, Sort_by_last_modified: s.sort_by_last_modified}
|
||||
fname := filepath.Join(utils.CacheDir(), cache_filename)
|
||||
if data, err := json.Marshal(c); err == nil {
|
||||
_ = os.WriteFile(fname, data, 0600)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) set_state_from_config(conf *Config, opts *Options) (err error) {
|
||||
h.state = State{}
|
||||
switch opts.Mode {
|
||||
case "file":
|
||||
h.state.mode = SELECT_SINGLE_FILE
|
||||
case "files":
|
||||
h.state.mode = SELECT_MULTIPLE_FILES
|
||||
case "save-file":
|
||||
h.state.mode = SELECT_SAVE_FILE
|
||||
case "dir":
|
||||
h.state.mode = SELECT_SINGLE_DIR
|
||||
case "dirs":
|
||||
h.state.mode = SELECT_MULTIPLE_DIRS
|
||||
case "save-dir":
|
||||
h.state.mode = SELECT_SAVE_DIR
|
||||
case "save-files":
|
||||
h.state.mode = SELECT_SAVE_FILES
|
||||
default:
|
||||
h.state.mode = SELECT_SINGLE_FILE
|
||||
}
|
||||
h.state.suggested_save_file_name = opts.SuggestedSaveFileName
|
||||
h.state.suggested_save_file_path = opts.SuggestedSaveFilePath
|
||||
h.state.filter_map = nil
|
||||
h.state.current_filter = ""
|
||||
if len(opts.FileFilter) > 0 {
|
||||
opts.FileFilter = utils.Uniq(opts.FileFilter)
|
||||
has_all_files := false
|
||||
fmap := make(map[string][]Filter)
|
||||
seen := utils.NewSet[string](len(opts.FileFilter))
|
||||
for _, x := range opts.FileFilter {
|
||||
f, ferr := NewFilter(x)
|
||||
if ferr != nil {
|
||||
return ferr
|
||||
}
|
||||
if f.Match == nil {
|
||||
has_all_files = true
|
||||
}
|
||||
if h.state.current_filter == "" {
|
||||
h.state.current_filter = f.Name
|
||||
}
|
||||
fmap[f.Name] = append(fmap[f.Name], *f)
|
||||
if !seen.Has(f.Name) {
|
||||
seen.Add(f.Name)
|
||||
h.state.filter_names = append(h.state.filter_names, f.Name)
|
||||
}
|
||||
}
|
||||
if !has_all_files {
|
||||
af, _ := NewFilter("glob:*:All files")
|
||||
fmap[af.Name] = append(fmap[af.Name], *af)
|
||||
if !seen.Has(af.Name) {
|
||||
h.state.filter_names = append(h.state.filter_names, af.Name)
|
||||
}
|
||||
}
|
||||
h.state.filter_map = make(map[string]Filter)
|
||||
for name, filters := range fmap {
|
||||
h.state.filter_map[name] = CombinedFilter(filters...)
|
||||
}
|
||||
}
|
||||
h.state.sort_by_last_modified = false
|
||||
h.state.respect_ignores = true
|
||||
h.state.show_hidden = false
|
||||
switch conf.Show_hidden {
|
||||
case Show_hidden_true, Show_hidden_y, Show_hidden_yes:
|
||||
h.state.show_hidden = true
|
||||
case Show_hidden_false, Show_hidden_n, Show_hidden_no:
|
||||
h.state.show_hidden = false
|
||||
case Show_hidden_last:
|
||||
h.state.show_hidden = cached_values().Show_hidden
|
||||
}
|
||||
switch conf.Respect_ignores {
|
||||
case Respect_ignores_true, Respect_ignores_y, Respect_ignores_yes:
|
||||
h.state.respect_ignores = true
|
||||
case Respect_ignores_false, Respect_ignores_n, Respect_ignores_no:
|
||||
h.state.respect_ignores = false
|
||||
case Respect_ignores_last:
|
||||
h.state.respect_ignores = cached_values().Respect_ignores
|
||||
}
|
||||
switch conf.Sort_by_last_modified {
|
||||
case Sort_by_last_modified_true, Sort_by_last_modified_y, Sort_by_last_modified_yes:
|
||||
h.state.sort_by_last_modified = true
|
||||
case Sort_by_last_modified_false, Sort_by_last_modified_n, Sort_by_last_modified_no:
|
||||
h.state.sort_by_last_modified = false
|
||||
case Sort_by_last_modified_last:
|
||||
h.state.sort_by_last_modified = cached_values().Sort_by_last_modified
|
||||
}
|
||||
h.state.global_ignores = ignorefiles.NewGitignore()
|
||||
if err = h.state.global_ignores.LoadLines(conf.Ignore...); err != nil {
|
||||
return err
|
||||
}
|
||||
h.state.keyboard_shortcuts = conf.KeyboardShortcuts
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var default_cwd string
|
||||
|
||||
func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) {
|
||||
write_output := func(selections []string, interrupted bool, current_filter string) {
|
||||
payload := make(map[string]any)
|
||||
if err != nil {
|
||||
if opts.WriteOutputTo != "" {
|
||||
m := fmt.Sprint(err)
|
||||
if opts.OutputFormat == "json" {
|
||||
payload["error"] = m
|
||||
b, _ := json.MarshalIndent(payload, "", " ")
|
||||
m = string(b)
|
||||
}
|
||||
os.WriteFile(opts.WriteOutputTo, []byte(m), 0600)
|
||||
}
|
||||
return
|
||||
}
|
||||
if interrupted {
|
||||
if opts.WriteOutputTo != "" {
|
||||
if opts.OutputFormat == "json" {
|
||||
payload["interrupted"] = true
|
||||
b, _ := json.MarshalIndent(payload, "", " ")
|
||||
os.WriteFile(opts.WriteOutputTo, b, 0600)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
m := strings.Join(selections, "\n")
|
||||
fmt.Print(m)
|
||||
if opts.WriteOutputTo != "" {
|
||||
if opts.OutputFormat == "json" {
|
||||
payload["paths"] = selections
|
||||
if current_filter != "" {
|
||||
payload["current_filter"] = current_filter
|
||||
}
|
||||
b, _ := json.MarshalIndent(payload, "", " ")
|
||||
m = string(b)
|
||||
}
|
||||
os.WriteFile(opts.WriteOutputTo, []byte(m), 0600)
|
||||
}
|
||||
}
|
||||
|
||||
conf, err := load_config(opts)
|
||||
if err != nil {
|
||||
return 1, err
|
||||
}
|
||||
lp, err := loop.New()
|
||||
if err != nil {
|
||||
return 1, err
|
||||
}
|
||||
lp.MouseTrackingMode(loop.FULL_MOUSE_TRACKING)
|
||||
handler := Handler{lp: lp, err_chan: make(chan error, 8), msg_printer: message.NewPrinter(utils.LanguageTag()), spinner: tui.NewSpinner("dots")}
|
||||
handler.rl = readline.New(lp, readline.RlInit{
|
||||
Prompt: "> ", ContinuationPrompt: ". ", Completer: handler.complete_save_prompt,
|
||||
})
|
||||
if err = handler.set_state_from_config(conf, opts); err != nil {
|
||||
return 1, err
|
||||
}
|
||||
handler.result_manager = NewResultManager(handler.err_chan, &handler.state, lp.WakeupMainThread)
|
||||
switch len(args) {
|
||||
case 0:
|
||||
if default_cwd, err = os.Getwd(); err != nil {
|
||||
return
|
||||
}
|
||||
case 1:
|
||||
default_cwd = args[0]
|
||||
default:
|
||||
return 1, fmt.Errorf("Can only specify one directory to search in")
|
||||
}
|
||||
default_cwd = utils.Expanduser(default_cwd)
|
||||
if default_cwd, err = filepath.Abs(default_cwd); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
lp.OnInitialize = func() (string, error) {
|
||||
if opts.WritePidTo != "" {
|
||||
if err := utils.AtomicWriteFile(opts.WritePidTo, bytes.NewReader([]byte(strconv.Itoa(os.Getpid()))), 0600); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
if opts.Title != "" {
|
||||
lp.SetWindowTitle(opts.Title)
|
||||
}
|
||||
return handler.OnInitialize()
|
||||
}
|
||||
lp.OnResize = func(old, new_size loop.ScreenSize) (err error) {
|
||||
handler.init_sizes(new_size)
|
||||
return handler.draw_screen()
|
||||
}
|
||||
lp.OnKeyEvent = handler.OnKeyEvent
|
||||
lp.OnText = handler.OnText
|
||||
lp.OnMouseEvent = handler.OnMouseEvent
|
||||
lp.OnWakeup = func() (err error) {
|
||||
select {
|
||||
case err = <-handler.err_chan:
|
||||
default:
|
||||
err = handler.draw_screen()
|
||||
}
|
||||
return
|
||||
}
|
||||
err = lp.Run()
|
||||
handler.state.save_cached_values()
|
||||
if err != nil {
|
||||
write_output(nil, false, "")
|
||||
return 1, err
|
||||
}
|
||||
ds := lp.DeathSignalName()
|
||||
if ds != "" {
|
||||
fmt.Println("Killed by signal: ", ds)
|
||||
lp.KillIfSignalled()
|
||||
write_output(nil, true, "")
|
||||
return 1, nil
|
||||
}
|
||||
rc = lp.ExitCode()
|
||||
switch rc {
|
||||
case 0:
|
||||
write_output(handler.state.selections, false, handler.state.current_filter)
|
||||
default:
|
||||
write_output(nil, true, "")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func EntryPoint(parent *cli.Command) {
|
||||
create_cmd(parent, main)
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2025, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import sys
|
||||
|
||||
from kitty.conf.types import Definition
|
||||
from kitty.constants import appname
|
||||
from kitty.simple_cli_definitions import CONFIG_HELP, CompletionSpec
|
||||
|
||||
definition = Definition(
|
||||
'!kittens.choose_files',
|
||||
)
|
||||
|
||||
agr = definition.add_group
|
||||
egr = definition.end_group
|
||||
opt = definition.add_option
|
||||
map = definition.add_map
|
||||
mma = definition.add_mouse_map
|
||||
|
||||
agr('scanning', 'Filesystem scanning') # {{{
|
||||
|
||||
opt('show_hidden', 'last', choices=('last', 'yes', 'y', 'true', 'no', 'n', 'false'), long_text='''
|
||||
Whether to show hidden files. The default value of :code:`last` means remember the last
|
||||
used value. This setting can be toggled withing the program.''')
|
||||
|
||||
opt('sort_by_last_modified', 'last', choices=('last', 'yes', 'y', 'true', 'no', 'n', 'false'), long_text='''
|
||||
Whether to sort the list of entries by last modified, instead of name. Note that sorting only applies
|
||||
before any query is entered. Once a query is entered entries are sorted by their matching score.
|
||||
The default value of :code:`last` means remember the last
|
||||
used value. This setting can be toggled withing the program.''')
|
||||
|
||||
opt('respect_ignores', 'last', choices=('last', 'yes', 'y', 'true', 'no', 'n', 'false'), long_text='''
|
||||
Whether to respect .gitignore and .ignore files and the :opt:`ignore` setting.
|
||||
The default value of :code:`last` means remember the last used value.
|
||||
This setting can be toggled withing the program.''')
|
||||
|
||||
opt('+ignore', '', add_to_default=False, long_text='''
|
||||
An ignore pattern to ignore matched files. Uses the same sytax as :code:`.gitignore` files (see :code:`man gitignore`).
|
||||
Anchored patterns match with respect to whatever directory is currently being displayed.
|
||||
Can be specified multiple times to use multiple patterns. Note that every pattern
|
||||
has to be checked against every file, so use sparingly.
|
||||
''')
|
||||
egr() # }}}
|
||||
|
||||
agr('shortcuts', 'Keyboard shortcuts') # {{{
|
||||
map('Quit', 'quit esc quit')
|
||||
map('Quit', 'quit ctrl+c quit')
|
||||
|
||||
map('Accept current result', 'accept enter accept')
|
||||
map('Select current result', 'select shift+enter select', long_text='''
|
||||
When selecting multiple files, this will add the current file to the list of selected files.
|
||||
You can also toggle the selected status of a file by holding down the :kbd:`Ctrl` key and clicking on
|
||||
it. Similarly, the :kbd:`Alt` key can be held to click and extend the range of selected files.
|
||||
''')
|
||||
map('Type file name', 'typename ctrl+enter typename', long_text='''
|
||||
Type a file name/path rather than filtering the list of existing files.
|
||||
Useful when specifying a file name for saving that does not yet exist.
|
||||
Does not work when selecting files to open rather than to save.
|
||||
''')
|
||||
|
||||
map('Next result', 'next_result down next 1')
|
||||
map('Previous result', 'prev_result up next -1')
|
||||
map('Left result', 'left_result left next left')
|
||||
map('Right result', 'right_result right next right')
|
||||
map('First result on screen', 'first_result_on_screen home next first_on_screen')
|
||||
map('Last result on screen', 'last_result_on_screen end next last_on_screen')
|
||||
map('First result', 'first_result_on_screen ctrl+home next first')
|
||||
map('Last result', 'last_result_on_screen ctrl+end next last')
|
||||
|
||||
map('Change to currently selected dir', 'cd_current tab cd current')
|
||||
map('Change to parent directory', 'cd_parent shift+tab cd up')
|
||||
map('Change to root directory', 'cd_root ctrl+/ cd /')
|
||||
map('Change to home directory', 'cd_home ctrl+~ cd ~')
|
||||
map('Change to home directory', 'cd_home ctrl+` cd ~')
|
||||
map('Change to home directory', 'cd_home ctrl+shift+` cd ~')
|
||||
map('Change to temp directory', 'cd_tmp ctrl+t cd /tmp')
|
||||
|
||||
map('Next filter', 'next_filter ctrl+f 1')
|
||||
map('Previous filter', 'prev_filter alt+f -1')
|
||||
|
||||
map('Toggle showing dotfiles', 'toggle_dotfiles alt+h toggle dotfiles')
|
||||
map('Toggle showing ignored files', 'toggle_ignorefiles alt+d toggle ignorefiles')
|
||||
map('Toggle sorting by dates', 'toggle_sort_by_dates alt+d toggle sort_by_dates')
|
||||
|
||||
egr() # }}}
|
||||
|
||||
def main(args: list[str]) -> None:
|
||||
raise SystemExit('This must be run as kitten choose-files')
|
||||
|
||||
|
||||
usage = '[directory to start choosing files in]'
|
||||
|
||||
|
||||
OPTIONS = '''
|
||||
--mode
|
||||
type=choices
|
||||
choices=file,files,save-file,dir,save-dir,dirs,save-files
|
||||
default=file
|
||||
The type of object(s) to select
|
||||
|
||||
|
||||
--file-filter
|
||||
type=list
|
||||
A list of filters to restrict the displayed files. Can be either mimetypes, or glob style patterns. Can be specified multiple times.
|
||||
The syntax is :code:`type:expression:Descriptive Name`.
|
||||
For example: :code:`mime:image/png:Images` and :code:`mime:image/gif:Images` and :code:`glob:*.[tT][xX][Tt]:Text files`.
|
||||
Note that glob patterns are case-sensitive. The mimetype specification is treated as a glob expressions as well, so you can,
|
||||
for example, use :code:`mime:text/*` to match all text files. The first filter in the list will be applied by default. Use a filter
|
||||
such as :code:`glob:*:All` to match all files. Note that filtering only appies to files, not directories.
|
||||
|
||||
|
||||
--suggested-save-file-name
|
||||
A suggested name when picking a save file.
|
||||
|
||||
|
||||
--suggested-save-file-path
|
||||
Path to an existing file to use as the save file.
|
||||
|
||||
|
||||
--title
|
||||
Window title to use for this chooser
|
||||
|
||||
|
||||
--override -o
|
||||
type=list
|
||||
Override individual configuration options, can be specified multiple times.
|
||||
Syntax: :italic:`name=value`.
|
||||
|
||||
|
||||
--config
|
||||
type=list
|
||||
completion=type:file ext:conf group:"Config files" kwds:none,NONE
|
||||
{config_help}
|
||||
|
||||
|
||||
--write-output-to
|
||||
Path to a file to which the output is written in addition to STDOUT.
|
||||
|
||||
|
||||
--output-format
|
||||
choices=text,json
|
||||
default=text
|
||||
The format in which to write the output.
|
||||
|
||||
|
||||
--write-pid-to
|
||||
Path to a file to which to write the process ID (PID) of this process to.
|
||||
'''.format(config_help=CONFIG_HELP.format(conf_name='choose-files', appname=appname)).format
|
||||
|
||||
|
||||
help_text = '''\
|
||||
Select one or more files, quickly, using fuzzy finding, by typing just a few characters from
|
||||
the file name. Browse matching files, using the arrow keys to navigate matches and press :kbd:`Enter`
|
||||
to select. The :kbd:`Tab` key can be used to change to a sub-folder. See the :doc:`online docs </kittens/choose-files>`
|
||||
for full details.
|
||||
'''
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
elif __name__ == '__doc__':
|
||||
cd = sys.cli_docs # type: ignore
|
||||
cd['usage'] = usage
|
||||
cd['options'] = OPTIONS
|
||||
cd['help_text'] = help_text
|
||||
cd['short_desc'] = 'Choose files, fast'
|
||||
cd['args_completion'] = CompletionSpec.from_string('type:directory')
|
||||
elif __name__ == '__conf__':
|
||||
sys.options_definition = definition # type: ignore
|
||||
@@ -1,333 +0,0 @@
|
||||
package choose_files
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/icons"
|
||||
"github.com/kovidgoyal/kitty/tools/tui"
|
||||
"github.com/kovidgoyal/kitty/tools/tui/loop"
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
"github.com/kovidgoyal/kitty/tools/utils/style"
|
||||
"github.com/kovidgoyal/kitty/tools/wcswidth"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
func (h *Handler) draw_results_title() {
|
||||
text := filepath.Clean(h.state.CurrentDir())
|
||||
home := filepath.Clean(utils.Expanduser("~"))
|
||||
if strings.HasPrefix(text, home) {
|
||||
text = "~" + text[len(home):]
|
||||
}
|
||||
available_width := h.screen_size.width - 9
|
||||
if available_width < 2 {
|
||||
return
|
||||
}
|
||||
tt := wcswidth.TruncateToVisualLength(text, available_width)
|
||||
if len(tt) < len(text) {
|
||||
text = wcswidth.TruncateToVisualLength(text, available_width-1)
|
||||
}
|
||||
text = fmt.Sprintf(" %s %s ", h.lp.SprintStyled("fg=blue", icons.IconForFileWithMode(text, fs.ModeDir, false)+" "), h.lp.SprintStyled("fg=intense-white bold", text))
|
||||
extra := available_width - wcswidth.Stringwidth(text)
|
||||
x := 3
|
||||
if extra > 1 {
|
||||
x += extra / 2
|
||||
}
|
||||
h.lp.MoveCursorHorizontally(x)
|
||||
h.lp.QueueWriteString(text)
|
||||
}
|
||||
|
||||
func (h *Handler) draw_no_matches_message(in_progress bool) {
|
||||
text := "Scanning filesystem, please wait…"
|
||||
if !in_progress {
|
||||
text = utils.IfElse(h.state.SearchText() == "", "No files present in this folder", "No matches found")
|
||||
}
|
||||
for _, line := range style.WrapTextAsLines(text, h.screen_size.width-2, style.WrapOptions{}) {
|
||||
h.lp.QueueWriteString("\r")
|
||||
h.lp.MoveCursorHorizontally(1)
|
||||
h.lp.QueueWriteString(line)
|
||||
h.lp.MoveCursorVertically(1)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const matching_position_style = "fg=green"
|
||||
const selected_style = "fg=magenta"
|
||||
const current_style = "fg=intense-white bold"
|
||||
|
||||
func (h *Handler) render_match_with_positions(text string, add_ellipsis bool, positions []int, is_current bool) {
|
||||
prefix, suffix, _ := strings.Cut(h.lp.SprintStyled(matching_position_style, " "), " ")
|
||||
if is_current {
|
||||
p, s, _ := strings.Cut(h.lp.SprintStyled(current_style, " "), " ")
|
||||
h.lp.QueueWriteString(p)
|
||||
defer h.lp.QueueWriteString(s)
|
||||
suffix += p
|
||||
}
|
||||
write_chunk := func(text string, emphasize bool) {
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
if emphasize {
|
||||
h.lp.QueueWriteString(prefix)
|
||||
defer func() {
|
||||
h.lp.QueueWriteString(suffix)
|
||||
}()
|
||||
}
|
||||
h.lp.QueueWriteString(text)
|
||||
}
|
||||
at := 0
|
||||
limit := len(text)
|
||||
for _, p := range positions {
|
||||
if p > limit || at > limit {
|
||||
break
|
||||
}
|
||||
write_chunk(text[at:p], false)
|
||||
at = p
|
||||
if r, sz := utf8.DecodeRuneInString(text[p:]); r != utf8.RuneError {
|
||||
write_chunk(string(r), true)
|
||||
at += sz
|
||||
}
|
||||
}
|
||||
if at < len(text) {
|
||||
write_chunk(text[at:], false)
|
||||
}
|
||||
if add_ellipsis {
|
||||
write_chunk("…", false)
|
||||
}
|
||||
}
|
||||
|
||||
var icon_cache map[string]string
|
||||
|
||||
func icon_for(path string, x os.FileMode) string {
|
||||
if icon_cache == nil {
|
||||
icon_cache = make(map[string]string, 512)
|
||||
}
|
||||
if ans := icon_cache[path]; ans != "" {
|
||||
return ans
|
||||
}
|
||||
var ans string
|
||||
if x&fs.ModeSymlink != 0 && x&SymlinkToDir != 0 {
|
||||
ans = string(icons.SYMLINK_TO_DIR)
|
||||
} else {
|
||||
ans = icons.IconForFileWithMode(path, x, true)
|
||||
}
|
||||
if wcswidth.Stringwidth(ans) == 1 {
|
||||
ans += " "
|
||||
}
|
||||
icon_cache[path] = ans
|
||||
return ans
|
||||
}
|
||||
|
||||
func (h *Handler) draw_column_of_matches(matches ResultsType, current_idx int, x, y, available_width, colnum int) {
|
||||
root_dir := h.state.CurrentDir()
|
||||
for i, m := range matches {
|
||||
h.lp.QueueWriteString("\r")
|
||||
h.lp.MoveCursorHorizontally(x)
|
||||
is_selected := h.state.IsSelected(m)
|
||||
var icon string
|
||||
if is_selected {
|
||||
icon = " "
|
||||
} else {
|
||||
icon = icon_for(filepath.Join(root_dir, m.text), m.ftype)
|
||||
}
|
||||
text := m.text
|
||||
add_ellipsis := false
|
||||
width := wcswidth.Stringwidth(text)
|
||||
if width > available_width-3 {
|
||||
text = wcswidth.TruncateToVisualLength(text, available_width-4)
|
||||
add_ellipsis = true
|
||||
width = available_width - 3
|
||||
}
|
||||
is_current := i == current_idx
|
||||
if is_current {
|
||||
h.lp.QueueWriteString(h.lp.SprintStyled(matching_position_style, icon+" "))
|
||||
} else {
|
||||
if is_selected {
|
||||
h.lp.QueueWriteString(h.lp.SprintStyled(selected_style, icon+" "))
|
||||
} else {
|
||||
h.lp.QueueWriteString(icon + " ")
|
||||
}
|
||||
}
|
||||
h.render_match_with_positions(text, add_ellipsis, m.sorted_positions(), is_current)
|
||||
h.lp.MoveCursorVertically(1)
|
||||
cr := h.state.mouse_state.AddCellRegion(fmt.Sprintf("result-%d-%d", colnum, i), x, y-1+i, x+width+2, y-1+i)
|
||||
cr.HoverStyle = HOVER_STYLE
|
||||
var data struct {
|
||||
colnum, i int
|
||||
}
|
||||
data.colnum, data.i = colnum, i
|
||||
cr.OnClickEvent = func(id string, ev *loop.MouseEvent, cell_offset tui.Point) error {
|
||||
if ev.Buttons&loop.LEFT_MOUSE_BUTTON == 0 {
|
||||
return nil
|
||||
}
|
||||
ctrl_mod := utils.IfElse(runtime.GOOS == "darwin", loop.SUPER, loop.CTRL)
|
||||
mods := ev.Mods & (ctrl_mod | loop.ALT) // shift alone and ctrl+shift are used for kitty bindings
|
||||
matches, _ := h.get_results()
|
||||
num_before := h.state.last_render.num_of_slots*data.colnum + data.i
|
||||
idx, did_wrap := matches.IncrementIndexWithWrapAroundAndCheck(h.state.last_render.first_idx, num_before)
|
||||
if did_wrap {
|
||||
h.lp.Beep()
|
||||
return nil
|
||||
}
|
||||
d := matches.SignedDistance(idx, h.state.current_idx)
|
||||
h.state.SetCurrentIndex(idx)
|
||||
h.state.last_render.num_before = max(0, h.state.last_render.num_before+d)
|
||||
switch mods {
|
||||
case 0:
|
||||
h.dispatch_action("accept", "")
|
||||
case ctrl_mod, ctrl_mod | loop.ALT:
|
||||
h.dispatch_action("select", "")
|
||||
case loop.ALT:
|
||||
r := matches.At(idx)
|
||||
if (r != nil && h.state.IsSelected(r)) || h.result_manager.last_click_anchor == nil {
|
||||
h.dispatch_action("select", "")
|
||||
return nil
|
||||
}
|
||||
already_selected := utils.NewSetWithItems(h.state.selections...)
|
||||
cdir := h.state.CurrentDir()
|
||||
matches.Apply(idx, *h.result_manager.last_click_anchor, func(r *ResultItem) bool {
|
||||
m := filepath.Join(cdir, r.text)
|
||||
if !already_selected.Has(m) && h.state.CanSelect(r) {
|
||||
already_selected.Add(m)
|
||||
h.state.selections = append(h.state.selections, m)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return h.draw_screen()
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) draw_list_of_results(matches *SortedResults, y, height int) (num_cols, num_shown int) {
|
||||
available_width := h.screen_size.width - 2
|
||||
col_width := available_width
|
||||
num_cols = 1
|
||||
calc_num_cols := func(num_matches int) int {
|
||||
if num_matches == 0 || height < 2 {
|
||||
return 0
|
||||
}
|
||||
if num_matches > height {
|
||||
col_width = 40
|
||||
num_cols = available_width / col_width
|
||||
for num_cols > 0 && height*(num_cols-1) >= num_matches {
|
||||
num_cols--
|
||||
}
|
||||
col_width = available_width / num_cols
|
||||
}
|
||||
return num_cols
|
||||
}
|
||||
columns, num_before, first_idx := matches.SplitIntoColumns(calc_num_cols, height, h.state.last_render.num_before, h.state.CurrentIndex())
|
||||
h.state.last_render.num_before = num_before
|
||||
h.state.last_render.num_per_column = height
|
||||
h.state.last_render.num_columns = num_cols
|
||||
h.state.last_render.first_idx = first_idx
|
||||
x := 1
|
||||
for i, col := range columns {
|
||||
h.lp.MoveCursorTo(x, y)
|
||||
h.draw_column_of_matches(col, num_before, x, y, col_width-1, i)
|
||||
num_before -= height
|
||||
num_shown += len(col)
|
||||
x += col_width
|
||||
}
|
||||
return len(columns), num_shown
|
||||
}
|
||||
|
||||
func (h *Handler) draw_num_of_matches(num_shown, y int, in_progress bool) {
|
||||
m := ""
|
||||
switch h.state.last_render.num_matches {
|
||||
case 0:
|
||||
m = " no matches "
|
||||
default:
|
||||
m = fmt.Sprintf(" %d of %s matches ", min(num_shown, h.state.last_render.num_matches), h.msg_printer.Sprint(h.state.last_render.num_matches))
|
||||
}
|
||||
w := int(math.Ceil(float64(wcswidth.Stringwidth(m)) / 2.0))
|
||||
spinner := ""
|
||||
spinner_width := 0
|
||||
if in_progress {
|
||||
spinner = h.spinner.Tick()
|
||||
spinner_width = 1 + wcswidth.Stringwidth(spinner)
|
||||
}
|
||||
h.lp.MoveCursorTo(h.screen_size.width-w-spinner_width-2, y)
|
||||
st := loop.SizedText{Subscale_denominator: 2, Subscale_numerator: 1, Vertical_alignment: 2, Width: 1}
|
||||
graphemes := wcswidth.SplitIntoGraphemes(m)
|
||||
for len(graphemes) > 0 {
|
||||
s := ""
|
||||
for w := 0; w < 2 && len(graphemes) > 0; {
|
||||
w += wcswidth.Stringwidth(graphemes[0])
|
||||
s += graphemes[0]
|
||||
graphemes = graphemes[1:]
|
||||
}
|
||||
h.lp.DrawSizedText(s, st)
|
||||
}
|
||||
if spinner != "" {
|
||||
h.lp.QueueWriteString(spinner)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) draw_results(y, bottom_margin int, matches *SortedResults, in_progress bool) (height int) {
|
||||
height = h.screen_size.height - y - bottom_margin
|
||||
h.lp.MoveCursorTo(1, 1+y)
|
||||
h.draw_frame(h.screen_size.width, height, in_progress)
|
||||
h.lp.MoveCursorTo(1, 1+y)
|
||||
h.draw_results_title()
|
||||
y += 2
|
||||
h.lp.MoveCursorTo(1, y)
|
||||
h.state.last_render.num_of_slots = height - 2
|
||||
num_cols := 0
|
||||
num := matches.Len()
|
||||
num_shown := 0
|
||||
switch num {
|
||||
case 0:
|
||||
h.draw_no_matches_message(in_progress)
|
||||
default:
|
||||
num_cols, num_shown = h.draw_list_of_results(matches, y, h.state.last_render.num_of_slots)
|
||||
}
|
||||
h.state.last_render.num_matches = num
|
||||
h.state.last_render.num_shown = num_shown
|
||||
h.draw_num_of_matches(h.state.last_render.num_of_slots*num_cols, y+height-2, in_progress)
|
||||
return
|
||||
}
|
||||
|
||||
func (h *Handler) next_result(amt int) {
|
||||
if h.state.last_render.num_matches > 0 {
|
||||
idx := h.state.CurrentIndex()
|
||||
idx = h.result_manager.scorer.sorted_results.IncrementIndexWithWrapAround(idx, amt)
|
||||
h.state.SetCurrentIndex(idx)
|
||||
h.state.last_render.num_before = max(0, h.state.last_render.num_before+amt)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) move_sideways(leftwards bool) {
|
||||
r := h.state.last_render
|
||||
if r.num_matches > 0 && r.num_per_column > 0 {
|
||||
cidx := h.state.CurrentIndex()
|
||||
slots := r.num_of_slots
|
||||
if leftwards {
|
||||
idx := h.result_manager.scorer.sorted_results.IncrementIndexWithWrapAround(cidx, -slots)
|
||||
if idx.Less(cidx) {
|
||||
h.state.SetCurrentIndex(idx)
|
||||
if r.num_columns > 1 && r.num_before >= r.num_per_column {
|
||||
h.state.last_render.num_before = max(0, h.state.last_render.num_before-slots)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
idx := h.result_manager.scorer.sorted_results.IncrementIndexWithWrapAround(cidx, slots)
|
||||
if cidx.Less(idx) {
|
||||
h.state.SetCurrentIndex(idx)
|
||||
if r.num_columns > 1 && r.num_before < (r.num_columns-1)*r.num_per_column {
|
||||
h.state.last_render.num_before = max(0, h.state.last_render.num_before+slots)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
package choose_files
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/cli"
|
||||
"github.com/kovidgoyal/kitty/tools/tui/loop"
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
func (h *Handler) complete_save_prompt(before_cursor, after_cursor string) *cli.Completions {
|
||||
path := before_cursor
|
||||
prefix := ""
|
||||
if idx := strings.Index(path, string(os.PathSeparator)); idx > -1 {
|
||||
prefix = filepath.Dir(path) + string(os.PathSeparator)
|
||||
}
|
||||
dir := ""
|
||||
if path == "" {
|
||||
path = h.state.CurrentDir()
|
||||
dir = path
|
||||
} else {
|
||||
if !filepath.IsAbs(path) {
|
||||
path = filepath.Join(h.state.CurrentDir(), path)
|
||||
}
|
||||
dir = filepath.Dir(path)
|
||||
if strings.HasSuffix(before_cursor, string(os.PathSeparator)) {
|
||||
dir = path
|
||||
}
|
||||
}
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
ans := cli.NewCompletions()
|
||||
dirs := ans.AddMatchGroup("Directories")
|
||||
dirs.IsFiles = true
|
||||
dirs.NoTrailingSpace = true
|
||||
files := ans.AddMatchGroup("Files")
|
||||
files.IsFiles = true
|
||||
files.NoTrailingSpace = true
|
||||
leading, _ := filepath.Rel(dir, path)
|
||||
if leading == "." {
|
||||
leading = ""
|
||||
}
|
||||
for _, e := range entries {
|
||||
word := e.Name()
|
||||
if leading == "" || strings.HasPrefix(word, leading) {
|
||||
collection := utils.IfElse(e.Type().IsDir(), dirs, files)
|
||||
if prefix != "" {
|
||||
word = prefix + word
|
||||
}
|
||||
collection.Matches = append(collection.Matches, &cli.Match{Word: word})
|
||||
}
|
||||
}
|
||||
return ans
|
||||
}
|
||||
|
||||
func (h *Handler) current_save_file_path() string {
|
||||
ans := h.rl.AllText()
|
||||
if ans != "" {
|
||||
ans = utils.Expanduser(ans)
|
||||
if !filepath.IsAbs(ans) {
|
||||
ans = filepath.Join(h.state.CurrentDir(), ans)
|
||||
}
|
||||
}
|
||||
return ans
|
||||
}
|
||||
|
||||
func (h *Handler) save_file_name_handle_key(ev *loop.KeyEvent) (err error) {
|
||||
ac := h.shortcut_tracker.Match(ev, h.state.keyboard_shortcuts)
|
||||
if ac != nil {
|
||||
switch ac.Name {
|
||||
case "accept":
|
||||
if p := h.current_save_file_path(); p != "" {
|
||||
h.state.selections = append(h.state.selections, p)
|
||||
h.lp.Quit(0)
|
||||
} else {
|
||||
h.lp.Beep()
|
||||
}
|
||||
ev.Handled = true
|
||||
return nil
|
||||
case "select":
|
||||
if p := h.current_save_file_path(); p != "" {
|
||||
h.state.selections = append(h.state.selections, p)
|
||||
}
|
||||
ev.Handled = true
|
||||
h.rl.ResetText()
|
||||
return h.draw_screen()
|
||||
case "quit":
|
||||
h.state.screen = NORMAL
|
||||
ev.Handled = true
|
||||
return h.draw_screen()
|
||||
}
|
||||
}
|
||||
if err = h.rl.OnKeyEvent(ev); err == nil {
|
||||
err = h.draw_screen()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (h *Handler) initialize_save_file_name(fname string) {
|
||||
h.state.screen = SAVE_FILE
|
||||
h.rl.ResetText()
|
||||
if len(h.state.selections) > 0 && fname == "" {
|
||||
if q, err := filepath.Abs(h.state.selections[0]); err == nil {
|
||||
if s, err := os.Stat(q); err == nil {
|
||||
if s.IsDir() == h.state.mode.OnlyDirs() {
|
||||
if fname, err = filepath.Rel(h.state.CurrentDir(), q); err != nil {
|
||||
fname = q
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
h.rl.SetText(fname)
|
||||
}
|
||||
|
||||
func (h *Handler) draw_save_file_name_screen() (err error) {
|
||||
h.lp.AllowLineWrapping(true)
|
||||
desc := utils.IfElse(h.state.mode == SELECT_SAVE_FILE, "file", "directory")
|
||||
h.lp.Println("Enter the name of the", desc, "below, relative to:")
|
||||
h.lp.Println(h.lp.SprintStyled("fg=green", h.state.CurrentDir()))
|
||||
if h.state.mode.AllowsMultipleSelection() {
|
||||
h.lp.Println("Use shift+enter (or whatever you mapped the select action to) to enter multiple filenames")
|
||||
}
|
||||
h.lp.Println()
|
||||
h.rl.RedrawNonAtomic()
|
||||
h.lp.AllowLineWrapping(false)
|
||||
if len(h.state.selections) > 0 {
|
||||
h.lp.SaveCursorPosition()
|
||||
h.draw_footer()
|
||||
h.lp.RestoreCursorPosition()
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,746 +0,0 @@
|
||||
package choose_files
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/fzf"
|
||||
"github.com/kovidgoyal/kitty/tools/ignorefiles"
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
func (c CombinedScore) String() string {
|
||||
return fmt.Sprintf("{score: %d length: %d index: %d}", c.Score(), c.Length(), c.Index())
|
||||
}
|
||||
|
||||
type ignore_file_with_prefix struct {
|
||||
impl ignorefiles.IgnoreFile
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (i *ignore_file_with_prefix) is_ignored(name string, ftype fs.FileMode) (ans bool, was_match bool) {
|
||||
ans, linenum, _ := i.impl.IsIgnored(i.prefix+name, ftype)
|
||||
was_match = linenum > -1
|
||||
return
|
||||
}
|
||||
|
||||
type ResultItem struct {
|
||||
text string
|
||||
ftype fs.FileMode
|
||||
positions []int // may be nil
|
||||
score CombinedScore
|
||||
ignore_files []ignore_file_with_prefix
|
||||
}
|
||||
type ResultsType []*ResultItem
|
||||
|
||||
func (r *ResultItem) SetScoreResult(x fzf.Result) {
|
||||
r.positions = x.Positions
|
||||
r.score.Set_score(uint16(math.MaxUint16 - uint16(x.Score)))
|
||||
}
|
||||
|
||||
func (r ResultItem) IsMatching() bool {
|
||||
return r.score.Score() < uint16(math.MaxUint16)
|
||||
}
|
||||
|
||||
func (r ResultItem) String() string {
|
||||
return fmt.Sprintf("{text: %#v, %s, positions: %#v}", r.text, r.score, r.positions)
|
||||
}
|
||||
|
||||
func (r *ResultItem) sorted_positions() []int {
|
||||
if len(r.positions) > 1 {
|
||||
sort.Ints(r.positions)
|
||||
}
|
||||
return r.positions
|
||||
}
|
||||
|
||||
type FileSystemScanner struct {
|
||||
listeners []chan bool
|
||||
in_progress, keep_going atomic.Bool
|
||||
root_dir string
|
||||
mutex sync.Mutex
|
||||
collection *ResultCollection
|
||||
dir_reader func(path string) ([]fs.DirEntry, error)
|
||||
file_reader func(path string) ([]byte, error)
|
||||
filter_func func(filename string) bool
|
||||
global_gitignore ignorefiles.IgnoreFile
|
||||
global_ignore ignorefiles.IgnoreFile
|
||||
respect_ignores, show_hidden bool
|
||||
sort_by_last_modified bool
|
||||
|
||||
err error
|
||||
}
|
||||
|
||||
func new_filesystem_scanner(root_dir string, notify chan bool, filter_func func(string) bool) (fss *FileSystemScanner) {
|
||||
ans := &FileSystemScanner{root_dir: root_dir, listeners: []chan bool{notify}, collection: NewResultCollection(4096)}
|
||||
ans.in_progress.Store(true)
|
||||
ans.keep_going.Store(true)
|
||||
ans.dir_reader = os.ReadDir
|
||||
ans.file_reader = os.ReadFile
|
||||
ans.filter_func = utils.IfElse(filter_func == nil, accept_all, filter_func)
|
||||
ans.global_gitignore = ignorefiles.NewGitignore()
|
||||
ans.global_ignore = ignorefiles.NewGitignore()
|
||||
ans.respect_ignores = true
|
||||
ans.show_hidden = false
|
||||
return ans
|
||||
}
|
||||
|
||||
type Scanner interface {
|
||||
Start()
|
||||
Cancel()
|
||||
AddListener(chan bool)
|
||||
Len() int
|
||||
Batch(offset *CollectionIndex) []ResultItem
|
||||
Finished() bool
|
||||
Error() error
|
||||
}
|
||||
|
||||
func (fss *FileSystemScanner) lock() { fss.mutex.Lock() }
|
||||
func (fss *FileSystemScanner) unlock() { fss.mutex.Unlock() }
|
||||
|
||||
func (fss *FileSystemScanner) Error() error {
|
||||
fss.lock()
|
||||
defer fss.unlock()
|
||||
return fss.err
|
||||
}
|
||||
|
||||
func (fss *FileSystemScanner) Start() {
|
||||
go fss.worker()
|
||||
}
|
||||
|
||||
func (fss *FileSystemScanner) Cancel() {
|
||||
fss.keep_going.Store(false)
|
||||
}
|
||||
|
||||
func (fss *FileSystemScanner) AddListener(x chan bool) {
|
||||
fss.lock()
|
||||
defer fss.unlock()
|
||||
if !fss.in_progress.Load() {
|
||||
close(x)
|
||||
} else {
|
||||
fss.listeners = append(fss.listeners, x)
|
||||
}
|
||||
}
|
||||
|
||||
func (fss *FileSystemScanner) Len() int {
|
||||
fss.lock()
|
||||
defer fss.unlock()
|
||||
return fss.collection.Len()
|
||||
}
|
||||
|
||||
func (fss *FileSystemScanner) Batch(offset *CollectionIndex) []ResultItem {
|
||||
fss.lock()
|
||||
defer fss.unlock()
|
||||
return fss.collection.Batch(offset)
|
||||
}
|
||||
|
||||
func (fss *FileSystemScanner) Finished() bool {
|
||||
return !fss.in_progress.Load()
|
||||
}
|
||||
|
||||
type sortable_dir_entry struct {
|
||||
name string
|
||||
ftype fs.FileMode
|
||||
sort_key []byte
|
||||
buf [unix.NAME_MAX + 1]byte
|
||||
}
|
||||
|
||||
const SymlinkToDir = 1
|
||||
|
||||
// lowercase a string into a pre-existing byte buffer with speedups for ASCII
|
||||
func as_lower(s string, output []byte) int {
|
||||
limit := min(len(s), len(output))
|
||||
found_non_ascii := false
|
||||
pos := 0
|
||||
for i := range limit {
|
||||
c := s[i]
|
||||
if 'A' <= c && c <= 'Z' {
|
||||
c += 'a' - 'A'
|
||||
if pos < i {
|
||||
copy(output[pos:i], s[pos:i])
|
||||
}
|
||||
output[i] = c
|
||||
pos = i + 1
|
||||
} else if c >= utf8.RuneSelf {
|
||||
if pos < i {
|
||||
copy(output[pos:i], s[pos:i])
|
||||
}
|
||||
found_non_ascii = true
|
||||
pos = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found_non_ascii {
|
||||
if pos < limit {
|
||||
copy(output[pos:limit], s[pos:limit])
|
||||
}
|
||||
return limit
|
||||
}
|
||||
buf := [4]byte{}
|
||||
var n int
|
||||
for _, r := range s[pos:] {
|
||||
o := output[pos:]
|
||||
r = unicode.ToLower(r)
|
||||
if len(o) > 3 {
|
||||
n = utf8.EncodeRune(o, r)
|
||||
} else {
|
||||
n = utf8.EncodeRune(buf[:], r)
|
||||
n = copy(o, buf[:n])
|
||||
}
|
||||
pos += n
|
||||
if pos >= len(output) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return pos
|
||||
}
|
||||
|
||||
func accept_all(filename string) bool { return true }
|
||||
|
||||
func (fss *FileSystemScanner) worker() {
|
||||
defer func() {
|
||||
fss.lock()
|
||||
defer fss.unlock()
|
||||
fss.in_progress.Store(false)
|
||||
if r := recover(); r != nil {
|
||||
st, qerr := utils.Format_stacktrace_on_panic(r)
|
||||
fss.err = fmt.Errorf("%w\n%s", qerr, st)
|
||||
}
|
||||
for _, l := range fss.listeners {
|
||||
close(l)
|
||||
}
|
||||
}()
|
||||
root_dir, _ := filepath.Abs(fss.root_dir)
|
||||
if !strings.HasSuffix(root_dir, string(os.PathSeparator)) {
|
||||
root_dir += string(os.PathSeparator)
|
||||
}
|
||||
dir := root_dir
|
||||
var ignore_files []ignore_file_with_prefix
|
||||
base := ""
|
||||
pos := &CollectionIndex{}
|
||||
var arena []sortable_dir_entry
|
||||
var sortable []*sortable_dir_entry
|
||||
var ignoreable []*sortable_dir_entry
|
||||
var idx uint32
|
||||
dot_git := os.Getenv("GIT_DIR")
|
||||
if dot_git == "" {
|
||||
dot_git = ".git"
|
||||
}
|
||||
// do a breadth first traversal of the filesystem
|
||||
is_root := true
|
||||
for dir != "" {
|
||||
if !fss.keep_going.Load() {
|
||||
break
|
||||
}
|
||||
entries, err := fss.dir_reader(dir)
|
||||
if err != nil {
|
||||
if is_root {
|
||||
fss.keep_going.Store(false)
|
||||
fss.lock()
|
||||
fss.err = err
|
||||
fss.unlock()
|
||||
}
|
||||
entries = nil
|
||||
}
|
||||
if cap(arena) < len(entries) {
|
||||
arena = make([]sortable_dir_entry, 0, max(1024, len(entries), 2*cap(arena)))
|
||||
sortable = make([]*sortable_dir_entry, 0, cap(arena))
|
||||
ignoreable = make([]*sortable_dir_entry, 0, cap(arena))
|
||||
}
|
||||
arena = arena[:len(entries)]
|
||||
sortable = sortable[:0]
|
||||
ignoreable = ignoreable[:0]
|
||||
ignore_files_copied := false
|
||||
add_ignore_file_from_impl := func(impl ignorefiles.IgnoreFile) {
|
||||
// we want ignore_files to be a copy as we dont want to
|
||||
// change the underlying array of ignore_files as it is
|
||||
// referenced by multiple ResultItems
|
||||
if !ignore_files_copied {
|
||||
ignore_files_copied = true
|
||||
n := make([]ignore_file_with_prefix, len(ignore_files), len(ignore_files)+4)
|
||||
copy(n, ignore_files)
|
||||
ignore_files = n
|
||||
}
|
||||
ignore_files = append(ignore_files, ignore_file_with_prefix{impl: impl})
|
||||
}
|
||||
add_ignore_file := func(name string) {
|
||||
if data, rerr := fss.file_reader(dir + name); rerr == nil {
|
||||
impl := ignorefiles.NewGitignore()
|
||||
if rerr = impl.LoadString(utils.UnsafeBytesToString(data)); rerr == nil && impl.Len() > 0 {
|
||||
add_ignore_file_from_impl(impl)
|
||||
}
|
||||
}
|
||||
}
|
||||
entry_is_ignored := func(name string, ftype fs.FileMode) (is_ignored bool) {
|
||||
for _, ignore_file := range ignore_files {
|
||||
if iig, was_match := ignore_file.is_ignored(name, ftype); was_match {
|
||||
is_ignored = iig
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
has_git_ignore, has_dot_git, has_dot_ignore := false, false, false
|
||||
for i, e := range entries {
|
||||
name := e.Name()
|
||||
ftype := e.Type()
|
||||
is_dir := ftype&fs.ModeDir != 0
|
||||
if !is_dir {
|
||||
switch name {
|
||||
case ".ignore":
|
||||
has_dot_ignore = true
|
||||
case ".gitignore":
|
||||
has_git_ignore = true
|
||||
}
|
||||
if !fss.filter_func(name) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if name == dot_git {
|
||||
has_dot_git = true
|
||||
}
|
||||
}
|
||||
if !fss.show_hidden && name[0] == '.' {
|
||||
continue
|
||||
}
|
||||
arena[i].name = name
|
||||
if ftype&fs.ModeSymlink != 0 {
|
||||
if st, serr := os.Stat(dir + arena[i].name); serr == nil && st.IsDir() {
|
||||
ftype |= SymlinkToDir
|
||||
}
|
||||
}
|
||||
arena[i].ftype = ftype
|
||||
if is_dir {
|
||||
arena[i].buf[0] = '0'
|
||||
} else {
|
||||
arena[i].buf[0] = '1'
|
||||
}
|
||||
if fss.sort_by_last_modified {
|
||||
var ts time.Time
|
||||
if info, err := e.Info(); err == nil {
|
||||
ts = info.ModTime()
|
||||
}
|
||||
binary.BigEndian.PutUint64(arena[i].buf[1:], uint64(ts.UnixNano()))
|
||||
arena[i].sort_key = arena[i].buf[:1+8]
|
||||
} else {
|
||||
n := as_lower(arena[i].name, arena[i].buf[1:])
|
||||
arena[i].sort_key = arena[i].buf[:1+n]
|
||||
}
|
||||
sortable = append(sortable, &arena[i])
|
||||
}
|
||||
if fss.respect_ignores {
|
||||
if is_root && fss.global_ignore.Len() > 0 {
|
||||
add_ignore_file_from_impl(fss.global_ignore)
|
||||
}
|
||||
if has_dot_git {
|
||||
if fss.global_gitignore.Len() > 0 {
|
||||
add_ignore_file_from_impl(fss.global_gitignore)
|
||||
}
|
||||
add_ignore_file(filepath.Join(dot_git, "info", "exclude"))
|
||||
if has_git_ignore {
|
||||
add_ignore_file(".gitignore")
|
||||
}
|
||||
}
|
||||
if has_dot_ignore {
|
||||
add_ignore_file(".ignore")
|
||||
}
|
||||
}
|
||||
final_entries := sortable
|
||||
if len(ignore_files) > 0 {
|
||||
for _, e := range sortable {
|
||||
if !entry_is_ignored(e.name, e.ftype) {
|
||||
ignoreable = append(ignoreable, e)
|
||||
}
|
||||
|
||||
}
|
||||
final_entries = ignoreable
|
||||
}
|
||||
slices.SortFunc(final_entries, func(a, b *sortable_dir_entry) int { return bytes.Compare(a.sort_key, b.sort_key) })
|
||||
fss.lock()
|
||||
for _, e := range final_entries {
|
||||
i := fss.collection.NextAppendPointer()
|
||||
i.ftype = e.ftype
|
||||
i.text = base + e.name
|
||||
i.score.Set_index(idx)
|
||||
i.ignore_files = ignore_files
|
||||
idx++
|
||||
}
|
||||
listeners := fss.listeners
|
||||
fss.unlock()
|
||||
for _, l := range listeners {
|
||||
select {
|
||||
case l <- true:
|
||||
default:
|
||||
}
|
||||
}
|
||||
ignore_files = nil
|
||||
if relpath, ignf := fss.collection.NextDir(pos); relpath != "" {
|
||||
base = relpath + string(os.PathSeparator)
|
||||
dir = root_dir + base
|
||||
if len(ignf) != 0 {
|
||||
name := filepath.Base(relpath) + string(os.PathSeparator)
|
||||
ignore_files = utils.Map(func(ignore_file ignore_file_with_prefix) ignore_file_with_prefix {
|
||||
return ignore_file_with_prefix{impl: ignore_file.impl, prefix: ignore_file.prefix + name}
|
||||
}, ignf)
|
||||
}
|
||||
} else {
|
||||
dir = ""
|
||||
}
|
||||
is_root = false
|
||||
}
|
||||
}
|
||||
|
||||
type FileSystemScorer struct {
|
||||
scanner Scanner
|
||||
keep_going, is_complete atomic.Bool
|
||||
root_dir, query string
|
||||
filter Filter
|
||||
only_dirs bool
|
||||
mutex sync.Mutex
|
||||
sorted_results *SortedResults
|
||||
on_results func(error, bool)
|
||||
current_worker_wait *sync.WaitGroup
|
||||
scorer *fzf.FuzzyMatcher
|
||||
dir_reader func(path string) ([]fs.DirEntry, error)
|
||||
file_reader func(path string) ([]byte, error)
|
||||
global_gitignore, global_ignore ignorefiles.IgnoreFile
|
||||
respect_ignores, show_hidden bool
|
||||
sort_by_last_modified bool
|
||||
}
|
||||
|
||||
func NewFileSystemScorer(root_dir, query string, filter Filter, only_dirs bool, on_results func(error, bool)) (ans *FileSystemScorer) {
|
||||
return &FileSystemScorer{
|
||||
query: query, root_dir: root_dir, only_dirs: only_dirs, filter: filter, on_results: on_results,
|
||||
scorer: fzf.NewFuzzyMatcher(fzf.PATH_SCHEME), sorted_results: NewSortedResults(), respect_ignores: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (fss *FileSystemScorer) lock() { fss.mutex.Lock() }
|
||||
func (fss *FileSystemScorer) unlock() { fss.mutex.Unlock() }
|
||||
|
||||
func (fss *FileSystemScorer) Start() {
|
||||
on_results := make(chan bool)
|
||||
fss.is_complete.Store(false)
|
||||
fss.keep_going.Store(true)
|
||||
if fss.scanner == nil {
|
||||
sc := new_filesystem_scanner(fss.root_dir, on_results, fss.filter.Match)
|
||||
if fss.dir_reader != nil {
|
||||
sc.dir_reader = fss.dir_reader
|
||||
}
|
||||
if fss.file_reader != nil {
|
||||
sc.file_reader = fss.file_reader
|
||||
}
|
||||
if fss.global_gitignore != nil {
|
||||
sc.global_gitignore = fss.global_gitignore
|
||||
} else if ignore := ignorefiles.GlobalGitignore(); ignore != nil {
|
||||
sc.global_gitignore = ignore
|
||||
}
|
||||
if fss.global_ignore != nil {
|
||||
sc.global_ignore = fss.global_ignore
|
||||
}
|
||||
sc.show_hidden, sc.respect_ignores = fss.show_hidden, fss.respect_ignores
|
||||
sc.sort_by_last_modified = fss.sort_by_last_modified
|
||||
fss.scanner = sc
|
||||
fss.scanner.Start()
|
||||
} else {
|
||||
fss.scanner.AddListener(on_results)
|
||||
}
|
||||
fss.current_worker_wait = &sync.WaitGroup{}
|
||||
fss.current_worker_wait.Add(1)
|
||||
go fss.worker(on_results, fss.current_worker_wait)
|
||||
}
|
||||
|
||||
func (fss *FileSystemScorer) Change_query(query string) {
|
||||
if fss.query == query {
|
||||
return
|
||||
}
|
||||
fss.keep_going.Store(false)
|
||||
if fss.current_worker_wait != nil {
|
||||
if fss.scanner != nil {
|
||||
fss.scanner.Cancel()
|
||||
}
|
||||
fss.current_worker_wait.Wait()
|
||||
}
|
||||
fss.lock()
|
||||
fss.query = query
|
||||
fss.sorted_results.Clear()
|
||||
fss.unlock()
|
||||
fss.Start()
|
||||
}
|
||||
|
||||
func (fss *FileSystemScorer) change_scanner_setting(callback func()) {
|
||||
fss.keep_going.Store(false)
|
||||
if fss.current_worker_wait != nil {
|
||||
if fss.scanner != nil {
|
||||
fss.scanner.Cancel()
|
||||
}
|
||||
fss.current_worker_wait.Wait()
|
||||
}
|
||||
fss.lock()
|
||||
callback()
|
||||
fss.sorted_results.Clear()
|
||||
fss.scanner = nil
|
||||
fss.unlock()
|
||||
fss.Start()
|
||||
|
||||
}
|
||||
|
||||
func (fss *FileSystemScorer) Change_filter(filter Filter) {
|
||||
if !fss.filter.Equal(filter) {
|
||||
fss.change_scanner_setting(func() { fss.filter = filter })
|
||||
}
|
||||
}
|
||||
|
||||
func (fss *FileSystemScorer) Change_show_hidden(val bool) {
|
||||
if fss.show_hidden != val {
|
||||
fss.change_scanner_setting(func() { fss.show_hidden = val })
|
||||
}
|
||||
}
|
||||
|
||||
func (fss *FileSystemScorer) Change_respect_ignores(val bool) {
|
||||
if fss.respect_ignores != val {
|
||||
fss.change_scanner_setting(func() { fss.respect_ignores = val })
|
||||
}
|
||||
}
|
||||
|
||||
func (fss *FileSystemScorer) Change_sort_by_last_modified(val bool) {
|
||||
if fss.sort_by_last_modified != val {
|
||||
fss.change_scanner_setting(func() { fss.sort_by_last_modified = val })
|
||||
}
|
||||
}
|
||||
|
||||
func (fss *FileSystemScorer) worker(on_results chan bool, worker_wait *sync.WaitGroup) {
|
||||
defer func() {
|
||||
fss.is_complete.Store(true)
|
||||
defer worker_wait.Done()
|
||||
if r := recover(); r != nil {
|
||||
if fss.keep_going.Load() {
|
||||
st, qerr := utils.Format_stacktrace_on_panic(r)
|
||||
fss.on_results(fmt.Errorf("%w\n%s", qerr, st), true)
|
||||
}
|
||||
} else {
|
||||
if fss.keep_going.Load() {
|
||||
fss.on_results(fss.scanner.Error(), true)
|
||||
}
|
||||
}
|
||||
}()
|
||||
handle_batch := func(results []ResultItem) (err error) {
|
||||
if err = fss.scanner.Error(); err != nil {
|
||||
return
|
||||
}
|
||||
var rp []*ResultItem
|
||||
if fss.only_dirs {
|
||||
rp = make([]*ResultItem, 0, len(results))
|
||||
for i, r := range results {
|
||||
if r.ftype.IsDir() {
|
||||
rp = append(rp, &results[i])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if fss.filter.Match == nil {
|
||||
rp = make([]*ResultItem, len(results))
|
||||
for i := range len(rp) {
|
||||
rp[i] = &results[i]
|
||||
}
|
||||
} else {
|
||||
rp = make([]*ResultItem, 0, len(results))
|
||||
for i, r := range results {
|
||||
if r.ftype.IsDir() || fss.filter.Match(filepath.Base(r.text)) {
|
||||
rp = append(rp, &results[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(rp) > 0 {
|
||||
if fss.query != "" {
|
||||
scores, err := fss.scorer.Score(utils.Map(func(r *ResultItem) string { return r.text }, rp), fss.query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, r := range rp {
|
||||
r.SetScoreResult(scores[i])
|
||||
r.score.Set_length(uint16(len(r.text)))
|
||||
}
|
||||
rp = utils.Filter(rp, func(r *ResultItem) bool { return r.IsMatching() })
|
||||
} else {
|
||||
for _, r := range rp {
|
||||
r.score &= 0b11111111111111111111111111111111 // only preserve index
|
||||
r.positions = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(rp) > 0 {
|
||||
slices.SortFunc(rp, func(a, b *ResultItem) int { return cmp.Compare(a.score, b.score) })
|
||||
}
|
||||
fss.sorted_results.AddSortedSlice(rp)
|
||||
return
|
||||
}
|
||||
|
||||
offset := &CollectionIndex{}
|
||||
for range on_results {
|
||||
if !fss.keep_going.Load() {
|
||||
break
|
||||
}
|
||||
results := fss.scanner.Batch(offset)
|
||||
if len(results) > 0 || fss.scanner.Error() != nil {
|
||||
fss.on_results(handle_batch(results), false)
|
||||
}
|
||||
}
|
||||
for fss.keep_going.Load() {
|
||||
b := fss.scanner.Batch(offset)
|
||||
if len(b) == 0 {
|
||||
break
|
||||
}
|
||||
fss.on_results(handle_batch(b), false)
|
||||
}
|
||||
}
|
||||
|
||||
func (fss *FileSystemScorer) Results() (ans *SortedResults, is_finished bool) {
|
||||
fss.lock()
|
||||
defer fss.unlock()
|
||||
return fss.sorted_results, fss.is_complete.Load()
|
||||
}
|
||||
|
||||
func (fss *FileSystemScorer) Cancel() {
|
||||
fss.keep_going.Store(false)
|
||||
fss.scanner.Cancel()
|
||||
}
|
||||
|
||||
type Settings interface {
|
||||
OnlyDirs() bool
|
||||
CurrentDir() string
|
||||
SearchText() string
|
||||
ShowHidden() bool
|
||||
RespectIgnores() bool
|
||||
SortByLastModified() bool
|
||||
Filter() Filter
|
||||
GlobalIgnores() ignorefiles.IgnoreFile
|
||||
}
|
||||
|
||||
type ResultManager struct {
|
||||
report_errors chan error
|
||||
WakeupMainThread func() bool
|
||||
settings Settings
|
||||
|
||||
scorer *FileSystemScorer
|
||||
mutex sync.Mutex
|
||||
last_wakeup_at time.Time
|
||||
|
||||
last_click_anchor *CollectionIndex
|
||||
}
|
||||
|
||||
func NewResultManager(err_chan chan error, settings Settings, WakeupMainThread func() bool) *ResultManager {
|
||||
ans := &ResultManager{
|
||||
report_errors: err_chan,
|
||||
settings: settings,
|
||||
WakeupMainThread: WakeupMainThread,
|
||||
}
|
||||
return ans
|
||||
}
|
||||
|
||||
func (m *ResultManager) new_scorer() {
|
||||
root_dir := m.current_root_dir()
|
||||
query := m.settings.SearchText()
|
||||
m.scorer = NewFileSystemScorer(root_dir, query, m.settings.Filter(), m.settings.OnlyDirs(), m.on_results)
|
||||
m.scorer.respect_ignores = m.settings.RespectIgnores()
|
||||
m.scorer.show_hidden = m.settings.ShowHidden()
|
||||
m.scorer.global_ignore = m.settings.GlobalIgnores()
|
||||
m.last_click_anchor = nil
|
||||
}
|
||||
|
||||
func (m *ResultManager) on_results(err error, is_finished bool) {
|
||||
if err != nil {
|
||||
m.report_errors <- err
|
||||
m.WakeupMainThread()
|
||||
return
|
||||
}
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
if is_finished || time.Since(m.last_wakeup_at) > time.Millisecond*50 {
|
||||
m.WakeupMainThread()
|
||||
m.last_wakeup_at = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ResultManager) current_root_dir() string {
|
||||
var err error
|
||||
root_dir := m.settings.CurrentDir()
|
||||
if root_dir == "" || root_dir == "." {
|
||||
if root_dir, err = os.Getwd(); err != nil {
|
||||
return "/"
|
||||
}
|
||||
}
|
||||
root_dir = utils.Expanduser(root_dir)
|
||||
if root_dir, err = filepath.Abs(root_dir); err != nil {
|
||||
return "/"
|
||||
}
|
||||
return root_dir
|
||||
}
|
||||
|
||||
func (m *ResultManager) set_root_dir() {
|
||||
if m.scorer != nil {
|
||||
m.scorer.Cancel()
|
||||
}
|
||||
_ = os.Chdir(m.current_root_dir()) // this is so the terminal emulator can read the wd for launch --directory=current
|
||||
m.new_scorer()
|
||||
m.mutex.Lock()
|
||||
m.last_wakeup_at = time.Time{}
|
||||
m.mutex.Unlock()
|
||||
m.scorer.Start()
|
||||
}
|
||||
|
||||
func (m *ResultManager) set_something(callback func()) {
|
||||
m.mutex.Lock()
|
||||
m.last_wakeup_at = time.Time{}
|
||||
m.mutex.Unlock()
|
||||
if m.scorer == nil {
|
||||
m.new_scorer()
|
||||
m.scorer.Start()
|
||||
} else {
|
||||
m.last_click_anchor = nil
|
||||
callback()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *ResultManager) set_query() {
|
||||
m.set_something(func() { m.scorer.Change_query(m.settings.SearchText()) })
|
||||
}
|
||||
|
||||
func (m *ResultManager) set_filter() {
|
||||
m.set_something(func() { m.scorer.Change_filter(m.settings.Filter()) })
|
||||
}
|
||||
|
||||
func (m *ResultManager) set_show_hidden() {
|
||||
m.set_something(func() { m.scorer.Change_show_hidden(m.settings.ShowHidden()) })
|
||||
}
|
||||
|
||||
func (m *ResultManager) set_respect_ignores() {
|
||||
m.set_something(func() { m.scorer.Change_respect_ignores(m.settings.RespectIgnores()) })
|
||||
}
|
||||
|
||||
func (m *ResultManager) set_sort_by_last_modified() {
|
||||
m.set_something(func() { m.scorer.Change_sort_by_last_modified(m.settings.SortByLastModified()) })
|
||||
}
|
||||
|
||||
func (h *Handler) get_results() (ans *SortedResults, is_complete bool) {
|
||||
if h.result_manager.scorer == nil {
|
||||
return
|
||||
}
|
||||
return h.result_manager.scorer.Results()
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package choose_files
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/tui/loop"
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
"github.com/kovidgoyal/kitty/tools/wcswidth"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
func (h *Handler) draw_frame(width, height int, in_progress bool) {
|
||||
lp := h.lp
|
||||
prefix, suffix := "", ""
|
||||
if in_progress {
|
||||
x := h.lp.SprintStyled("fg=cyan", " ")
|
||||
prefix, suffix, _ = strings.Cut(x, " ")
|
||||
lp.QueueWriteString(prefix)
|
||||
}
|
||||
for i := range height {
|
||||
lp.SaveCursorPosition()
|
||||
switch i {
|
||||
case 0:
|
||||
lp.QueueWriteString("╭")
|
||||
lp.QueueWriteString(strings.Repeat("─", width-2))
|
||||
lp.QueueWriteString("╮")
|
||||
case height - 1:
|
||||
lp.QueueWriteString("╰")
|
||||
lp.QueueWriteString(strings.Repeat("─", width-2))
|
||||
lp.QueueWriteString("╯")
|
||||
default:
|
||||
lp.QueueWriteString("│")
|
||||
lp.MoveCursorHorizontally(width - 2)
|
||||
lp.QueueWriteString("│")
|
||||
}
|
||||
lp.RestoreCursorPosition()
|
||||
lp.MoveCursorVertically(1)
|
||||
}
|
||||
if suffix != "" {
|
||||
lp.QueueWriteString(suffix)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) draw_search_text(available_width int) {
|
||||
text := h.state.SearchText()
|
||||
available_width /= 2
|
||||
if wcswidth.Stringwidth(text) > available_width {
|
||||
g := wcswidth.SplitIntoGraphemes(text)
|
||||
available_width -= 2
|
||||
g = g[len(g)-available_width:]
|
||||
text = "…" + strings.Join(g, "")
|
||||
}
|
||||
h.lp.DrawSizedText(text+" ", loop.SizedText{Scale: 2})
|
||||
h.lp.MoveCursorHorizontally(-2)
|
||||
}
|
||||
|
||||
const SEARCH_BAR_HEIGHT = 4
|
||||
|
||||
func (h *Handler) draw_controls(y int) (max_width int) {
|
||||
type entry struct {
|
||||
text string
|
||||
callback func()
|
||||
width int
|
||||
}
|
||||
lines := make([]entry, 0, SEARCH_BAR_HEIGHT)
|
||||
add_control := func(icon, text string, callback func()) {
|
||||
line := icon + " " + text
|
||||
width := wcswidth.Stringwidth(line)
|
||||
max_width = max(max_width, width)
|
||||
lines = append(lines, entry{line, callback, width})
|
||||
}
|
||||
add_control(utils.IfElse(h.state.ShowHidden(), " ", " "), utils.IfElse(h.state.ShowHidden(), "hide dotfiles", "show dotfiles"), func() {
|
||||
h.state.show_hidden = !h.state.show_hidden
|
||||
h.result_manager.set_show_hidden()
|
||||
})
|
||||
add_control(" ", utils.IfElse(h.state.RespectIgnores(), "show ignored", "hide ignored"), func() {
|
||||
h.state.respect_ignores = !h.state.respect_ignores
|
||||
h.result_manager.set_respect_ignores()
|
||||
})
|
||||
add_control(utils.IfElse(h.state.SortByLastModified(), " ", " "), utils.IfElse(h.state.SortByLastModified(), "sort names", "sort dates"), func() {
|
||||
h.state.sort_by_last_modified = !h.state.sort_by_last_modified
|
||||
h.result_manager.set_sort_by_last_modified()
|
||||
})
|
||||
x := h.screen_size.width - max_width
|
||||
for i, e := range lines {
|
||||
h.lp.MoveCursorTo(x+1, y+i+1)
|
||||
h.lp.QueueWriteString(e.text)
|
||||
cb := e.callback
|
||||
h.state.mouse_state.AddCellRegion("rcontrol-"+strconv.Itoa(i), x, y+i, x+e.width, y+i, func(_ string) error {
|
||||
cb()
|
||||
h.state.redraw_needed = true
|
||||
return nil
|
||||
}).HoverStyle = HOVER_STYLE
|
||||
}
|
||||
return max_width + 1
|
||||
}
|
||||
|
||||
func (h *Handler) draw_search_bar(y int) {
|
||||
left_margin, right_margin := 0, h.draw_controls(y)
|
||||
h.lp.MoveCursorTo(1+left_margin, 1+y)
|
||||
available_width := h.screen_size.width - left_margin - right_margin
|
||||
h.draw_frame(available_width, SEARCH_BAR_HEIGHT, false)
|
||||
for y1 := y; y1 < y+4; y1++ {
|
||||
cr := h.state.mouse_state.AddCellRegion("search-bar", left_margin, y1, left_margin+available_width, y1)
|
||||
cr.PointerShape = loop.TEXT_POINTER
|
||||
cr.HoverStyle = "none"
|
||||
}
|
||||
h.lp.MoveCursorTo(1+left_margin+1, 2+y)
|
||||
h.draw_search_text(available_width - 2)
|
||||
}
|
||||
|
||||
func (h *Handler) handle_edit_keys(ev *loop.KeyEvent) bool {
|
||||
switch {
|
||||
case ev.MatchesPressOrRepeat("backspace"):
|
||||
if h.state.SearchText() == "" {
|
||||
h.lp.Beep()
|
||||
} else {
|
||||
g := wcswidth.SplitIntoGraphemes(h.state.search_text)
|
||||
h.set_query(strings.Join(g[:len(g)-1], ""))
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
"kitty/tools/utils"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,7 @@ package choose_fonts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
"kitty/tools/utils"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2025, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
from kitty.conf.types import Definition
|
||||
|
||||
definition = Definition(
|
||||
'!kittens.choose_files',
|
||||
)
|
||||
|
||||
agr = definition.add_group
|
||||
egr = definition.end_group
|
||||
opt = definition.add_option
|
||||
map = definition.add_map
|
||||
mma = definition.add_mouse_map
|
||||
|
||||
agr('Appearance')
|
||||
opt('color_scheme', 'no-preference', choices=('no-preference', 'dark', 'light'), long_text='''\
|
||||
The color scheme for your system. This sets the initial value of the color scheme. It can be changed subsequently
|
||||
by using :code:`kitten desktop-ui color-scheme`.
|
||||
''')
|
||||
opt('accent_color', 'cyan', long_text='The RGB accent color for your system, can be specified as a color name or in hex a decimal format.')
|
||||
opt('contrast', 'normal', choices=('normal', 'high'), long_text='The preferred contrast level.')
|
||||
opt('+file_chooser_kitty_conf', '',
|
||||
long_text='Path to config file to use for kitty when drawing the file chooser window. Can be specified multiple times. By default, the'
|
||||
' normal kitty.conf is used. Relative paths are resolved with respect to the kitty config directory.'
|
||||
)
|
||||
opt('+file_chooser_kitty_override', '', long_text='Override individual kitty configuration options, for the file chooser window.'
|
||||
' Can be specified multiple times. Syntax: :italic:`name=value`. For example: :code:`font_size=20`.'
|
||||
)
|
||||
|
||||
egr()
|
||||
|
||||
|
||||
def main(args: list[str]) -> None:
|
||||
raise SystemExit('This must be run as kitten desktop-ui')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
elif __name__ == '__conf__':
|
||||
sys.options_definition = definition # type: ignore
|
||||
@@ -1,903 +0,0 @@
|
||||
package desktop_ui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"maps"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/kovidgoyal/dbus"
|
||||
"github.com/kovidgoyal/dbus/introspect"
|
||||
"github.com/kovidgoyal/dbus/prop"
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
"github.com/kovidgoyal/kitty/tools/utils/style"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
const PORTAL_APPEARANCE_NAMESPACE = "org.freedesktop.appearance"
|
||||
const PORTAL_COLOR_SCHEME_KEY = "color-scheme"
|
||||
const PORTAL_ACCENT_COLOR_KEY = "accent-color"
|
||||
const PORTAL_CONTRAST_KEY = "contrast"
|
||||
const PORTAL_BUS_NAME = "org.freedesktop.impl.portal.desktop.kitty"
|
||||
const DESKTOP_OBJECT_PATH = "/org/freedesktop/portal/desktop"
|
||||
const SETTINGS_INTERFACE = "org.freedesktop.impl.portal.Settings"
|
||||
const FILE_CHOOSER_INTERFACE = "org.freedesktop.impl.portal.FileChooser"
|
||||
const KITTY_OBJECT_PATH = "/net/kovidgoyal/kitty/portal"
|
||||
const CHANGE_SETTINGS_INTERFACE = "net.kovidgoyal.kitty.settings"
|
||||
const DESKTOP_PORTAL_NAME = "org.freedesktop.portal.Desktop"
|
||||
const REQUEST_INTERFACE = "org.freedesktop.impl.portal.Request"
|
||||
|
||||
// Special portal setting used to check if we are being called by xdg-desktop-portal
|
||||
const SETTINGS_CANARY_NAMESPACE = "net.kovidgoyal.kitty"
|
||||
const SETTINGS_CANARY_KEY = "status"
|
||||
|
||||
type ColorScheme uint32
|
||||
|
||||
const (
|
||||
NO_PREFERENCE ColorScheme = iota
|
||||
DARK
|
||||
LIGHT
|
||||
)
|
||||
const (
|
||||
RESPONSE_SUCCESS uint32 = iota
|
||||
RESPONSE_CANCELED
|
||||
RESPONSE_ENDED
|
||||
)
|
||||
|
||||
type SettingsMap map[string]map[string]dbus.Variant
|
||||
|
||||
type Portal struct {
|
||||
bus *dbus.Conn
|
||||
settings SettingsMap
|
||||
lock sync.Mutex
|
||||
opts *Config
|
||||
file_chooser_first_instance *exec.Cmd
|
||||
}
|
||||
|
||||
func to_color(spec string) (v dbus.Variant, err error) {
|
||||
if col, err := style.ParseColor(spec); err == nil {
|
||||
return dbus.MakeVariant([]float64{float64(col.Red) / 255., float64(col.Green) / 255., float64(col.Blue) / 255.}), nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func NewPortal(opts *Config) (p *Portal, err error) {
|
||||
ans := Portal{opts: opts}
|
||||
ans.settings = SettingsMap{
|
||||
SETTINGS_CANARY_NAMESPACE: map[string]dbus.Variant{
|
||||
SETTINGS_CANARY_KEY: dbus.MakeVariant("running"),
|
||||
},
|
||||
}
|
||||
ans.settings[PORTAL_APPEARANCE_NAMESPACE] = map[string]dbus.Variant{}
|
||||
switch opts.Color_scheme {
|
||||
case Color_scheme_dark:
|
||||
ans.settings[PORTAL_APPEARANCE_NAMESPACE][PORTAL_COLOR_SCHEME_KEY] = dbus.MakeVariant(uint32(DARK))
|
||||
case Color_scheme_light:
|
||||
ans.settings[PORTAL_APPEARANCE_NAMESPACE][PORTAL_COLOR_SCHEME_KEY] = dbus.MakeVariant(uint32(LIGHT))
|
||||
default:
|
||||
ans.settings[PORTAL_APPEARANCE_NAMESPACE][PORTAL_COLOR_SCHEME_KEY] = dbus.MakeVariant(uint32(NO_PREFERENCE))
|
||||
}
|
||||
ans.settings[PORTAL_APPEARANCE_NAMESPACE][PORTAL_ACCENT_COLOR_KEY], err = to_color(opts.Accent_color)
|
||||
var contrast uint32
|
||||
if opts.Contrast == Contrast_high {
|
||||
contrast = 1
|
||||
}
|
||||
ans.settings[PORTAL_APPEARANCE_NAMESPACE][PORTAL_CONTRAST_KEY] = dbus.MakeVariant(contrast)
|
||||
return &ans, nil
|
||||
}
|
||||
|
||||
type PropSpec map[string]*prop.Prop
|
||||
type SignalSpec map[string][]struct {
|
||||
Name, Type string
|
||||
}
|
||||
type MethodSpec map[string][]struct {
|
||||
Name, Type string
|
||||
Out bool
|
||||
}
|
||||
|
||||
func ExportInterface(conn *dbus.Conn, object any, interface_name, object_path string, method_spec MethodSpec, prop_spec PropSpec, signal_spec SignalSpec) (err error) {
|
||||
op := dbus.ObjectPath(object_path)
|
||||
method_map := make(map[string]string, len(method_spec))
|
||||
methods := []introspect.Method{}
|
||||
if len(method_spec) > 0 {
|
||||
for method_name, args := range method_spec {
|
||||
method_map[method_name] = method_name
|
||||
meth_args := make([]introspect.Arg, len(args))
|
||||
for i, a := range args {
|
||||
meth_args[i] = introspect.Arg{
|
||||
Name: a.Name,
|
||||
Type: a.Type,
|
||||
Direction: utils.IfElse(a.Out, "out", "in"),
|
||||
}
|
||||
}
|
||||
methods = append(methods, introspect.Method{
|
||||
Name: method_name,
|
||||
Args: meth_args,
|
||||
})
|
||||
}
|
||||
}
|
||||
if err = conn.ExportWithMap(object, method_map, op, interface_name); err != nil {
|
||||
return fmt.Errorf("failed to export interface: %s at object path: %s with error: %w", interface_name, object_path, err)
|
||||
}
|
||||
var properties []introspect.Property
|
||||
p := prop.Map{interface_name: prop_spec}
|
||||
if len(prop_spec) > 0 {
|
||||
if props, err := prop.Export(conn, op, p); err != nil {
|
||||
return fmt.Errorf("failed to export properties with error: %w", err)
|
||||
} else {
|
||||
properties = props.Introspection(interface_name)
|
||||
}
|
||||
}
|
||||
var signals []introspect.Signal
|
||||
if len(signal_spec) > 0 {
|
||||
for signal_name, args := range signal_spec {
|
||||
sig_args := make([]introspect.Arg, len(args))
|
||||
for i, a := range args {
|
||||
sig_args[i] = introspect.Arg{
|
||||
Name: a.Name,
|
||||
Type: a.Type,
|
||||
Direction: "out",
|
||||
}
|
||||
}
|
||||
signals = append(signals, introspect.Signal{
|
||||
Name: signal_name,
|
||||
Args: sig_args,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
interface_data := introspect.Interface{
|
||||
Name: interface_name,
|
||||
Methods: methods,
|
||||
Properties: properties,
|
||||
Signals: signals,
|
||||
}
|
||||
interfaces := []introspect.Interface{
|
||||
introspect.IntrospectData, interface_data,
|
||||
}
|
||||
if len(properties) > 0 {
|
||||
interfaces = append(interfaces, prop.IntrospectData)
|
||||
}
|
||||
n := &introspect.Node{Name: object_path, Interfaces: interfaces}
|
||||
if err = conn.Export(introspect.NewIntrospectable(n), op, introspect.IntrospectData.Name); err != nil {
|
||||
return fmt.Errorf("failed to export introspected methods with error: %w", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (self *Portal) Start() (err error) {
|
||||
if self.bus, err = dbus.SessionBus(); err != nil {
|
||||
return fmt.Errorf("could not connect to session D-Bus: %s", err)
|
||||
}
|
||||
reply, err := self.bus.RequestName(PORTAL_BUS_NAME, dbus.NameFlagDoNotQueue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register dbus name: %v", err)
|
||||
}
|
||||
if reply != dbus.RequestNameReplyPrimaryOwner {
|
||||
return fmt.Errorf("can't register D-Bus name: name already taken")
|
||||
}
|
||||
props := PropSpec{
|
||||
"version": {Value: uint32(1), Writable: false, Emit: prop.EmitFalse},
|
||||
}
|
||||
signals := SignalSpec{
|
||||
"SettingChanged": {{"namespace", "s"}, {"key", "s"}, {"value", "v"}},
|
||||
}
|
||||
methods := MethodSpec{
|
||||
"Read": {{"namespace", "s", false}, {"key", "s", false}, {"value", "v", true}},
|
||||
"ReadAll": {{"namespaces", "as", false}, {"value", "a{sa{sv}}", true}},
|
||||
}
|
||||
if err = ExportInterface(self.bus, self, SETTINGS_INTERFACE, DESKTOP_OBJECT_PATH, methods, props, signals); err != nil {
|
||||
return
|
||||
}
|
||||
methods = MethodSpec{
|
||||
"OpenFile": {{"handle", "o", false}, {"app_id", "s", false}, {"parent_window", "s", false}, {"title", "s", false}, {"options", "a{sv}", false},
|
||||
{"response", "u", true}, {"results", "a{sv}", false},
|
||||
},
|
||||
"SaveFile": {{"handle", "o", false}, {"app_id", "s", false}, {"parent_window", "s", false}, {"title", "s", false}, {"options", "a{sv}", false},
|
||||
{"response", "u", true}, {"results", "a{sv}", false},
|
||||
},
|
||||
}
|
||||
if err = ExportInterface(self.bus, self, FILE_CHOOSER_INTERFACE, DESKTOP_OBJECT_PATH, methods, nil, nil); err != nil {
|
||||
return
|
||||
}
|
||||
methods = MethodSpec{
|
||||
"ChangeSetting": {{"namespace", "s", false}, {"key", "s", false}, {"value", "v", false}},
|
||||
"RemoveSetting": {{"namespace", "s", false}, {"key", "s", false}},
|
||||
}
|
||||
props["version"].Value = uint32(1)
|
||||
if err = ExportInterface(self.bus, self, CHANGE_SETTINGS_INTERFACE, KITTY_OBJECT_PATH, methods, props, nil); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ParseValueWithSignature(value, value_type_signature string) (v dbus.Variant, err error) {
|
||||
var s dbus.Signature
|
||||
if value_type_signature != "" {
|
||||
if value_type_signature[0] == '@' {
|
||||
value_type_signature = value_type_signature[1:]
|
||||
}
|
||||
s, err = dbus.ParseSignature(value_type_signature)
|
||||
if err != nil {
|
||||
return dbus.Variant{}, fmt.Errorf("%s is not a valid type signature: %w", value_type_signature, err)
|
||||
}
|
||||
}
|
||||
v, err = dbus.ParseVariant(value, s)
|
||||
if err != nil {
|
||||
if value_type_signature == "" {
|
||||
return dbus.Variant{}, fmt.Errorf("could not guess the data type of: %s with error: %w", value, err)
|
||||
}
|
||||
return dbus.Variant{}, fmt.Errorf("%s is not a valid value for signature: %#v with error: %w", value, value_type_signature, err)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func ParseValue(value string) (dbus.Variant, error) {
|
||||
return ParseValueWithSignature(value, "")
|
||||
}
|
||||
|
||||
type ShowSettingsOptions struct {
|
||||
AsJson bool
|
||||
AllowOtherBackends bool
|
||||
InNamespace []string
|
||||
}
|
||||
|
||||
func fetch_settings(conn *dbus.Conn, namespaces ...string) (ans ReadAllType, err error) {
|
||||
path := "/" + strings.ToLower(strings.ReplaceAll(DESKTOP_PORTAL_NAME, ".", "/"))
|
||||
obj := conn.Object(DESKTOP_PORTAL_NAME, dbus.ObjectPath(path))
|
||||
interface_name := strings.ReplaceAll(DESKTOP_PORTAL_NAME, "Desktop", "Settings")
|
||||
if len(namespaces) == 0 {
|
||||
namespaces = append(namespaces, "")
|
||||
}
|
||||
call := obj.Call(interface_name+".ReadAll", dbus.FlagNoAutoStart, namespaces)
|
||||
if err = call.Store(&ans); err != nil {
|
||||
return nil, fmt.Errorf("Failed to read response from ReadAll with error: %w", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func show_settings(opts *ShowSettingsOptions) (err error) {
|
||||
conn, err := dbus.SessionBus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to system bus with error: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
var response ReadAllType
|
||||
response, err = fetch_settings(conn, opts.InNamespace...)
|
||||
if opts.AsJson {
|
||||
unwrapped := make(map[string]map[string]any, len(response))
|
||||
for ns, m := range response {
|
||||
w := make(map[string]any, len(m))
|
||||
for k, a := range m {
|
||||
w[k] = a.Value()
|
||||
}
|
||||
unwrapped[ns] = w
|
||||
}
|
||||
j, err := json.MarshalIndent(unwrapped, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to format the response as JSON: %w", err)
|
||||
}
|
||||
fmt.Println(string(j))
|
||||
} else {
|
||||
for ns, m := range response {
|
||||
fmt.Println(ns + ":")
|
||||
for key, v := range m {
|
||||
fmt.Printf("\t%s: %s\n", key, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !opts.AllowOtherBackends {
|
||||
is_running_self := false
|
||||
if m, found := response[SETTINGS_CANARY_NAMESPACE]; found {
|
||||
_, is_running_self = m[SETTINGS_CANARY_KEY]
|
||||
}
|
||||
if !is_running_self {
|
||||
err = fmt.Errorf("the settings did not come from the desktop-ui kitten. Some other portal backend is providing the service.")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var DataDirs = sync.OnceValue(func() (ans []string) {
|
||||
d := os.Getenv("XDG_DATA_DIRS")
|
||||
if d == "" {
|
||||
d = "/usr/local/share/:/usr/share/"
|
||||
}
|
||||
all := []string{os.Getenv("XDG_DATA_HOME")}
|
||||
all = append(all, strings.Split(d, ":")...)
|
||||
seen := map[string]bool{}
|
||||
for _, x := range all {
|
||||
if !seen[x] {
|
||||
seen[x] = true
|
||||
ans = append(ans, x)
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
|
||||
func IsDir(x string) bool {
|
||||
s, err := os.Stat(x)
|
||||
return err == nil && s.IsDir()
|
||||
}
|
||||
|
||||
var WritableDataDirs = sync.OnceValue(func() (ans []string) {
|
||||
for _, x := range DataDirs() {
|
||||
if err := os.MkdirAll(x, 0o755); err == nil && unix.Access(x, unix.W_OK) == nil {
|
||||
ans = append(ans, x)
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
|
||||
var AllPortalInterfaces = sync.OnceValue(func() (ans []string) {
|
||||
return []string{SETTINGS_INTERFACE, FILE_CHOOSER_INTERFACE}
|
||||
})
|
||||
|
||||
// enable-portal {{{
|
||||
func patch_portals_conf(text []byte) []byte {
|
||||
lines := []string{}
|
||||
in_preferred := false
|
||||
for _, line := range utils.Splitlines(utils.UnsafeBytesToString(text)) {
|
||||
sl := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(sl, "[") {
|
||||
in_preferred = sl == "[preferred]"
|
||||
lines = append(lines, line)
|
||||
for _, iface := range AllPortalInterfaces() {
|
||||
lines = append(lines, iface+"=kitty")
|
||||
}
|
||||
} else if in_preferred {
|
||||
remove := false
|
||||
for _, iface := range AllPortalInterfaces() {
|
||||
if strings.HasPrefix(sl, iface) {
|
||||
remove = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !remove {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
return utils.UnsafeStringToBytes(strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
func enable_portal() (err error) {
|
||||
if len(WritableDataDirs()) == 0 {
|
||||
return fmt.Errorf("Could not find any writable data directories. Make sure XDG_DATA_DIRS is set and contains at least one directory for which you have write permission")
|
||||
}
|
||||
portals_dir := ""
|
||||
for _, x := range WritableDataDirs() {
|
||||
q := filepath.Join(x, "xdg-desktop-portal", "portals")
|
||||
if unix.Access(q, unix.W_OK) == nil && IsDir(q) {
|
||||
portals_dir = q
|
||||
break
|
||||
}
|
||||
}
|
||||
if portals_dir == "" {
|
||||
for _, x := range WritableDataDirs() {
|
||||
q := filepath.Join(x, "xdg-desktop-portal", "portals")
|
||||
if err := os.MkdirAll(q, 0o755); err == nil {
|
||||
portals_dir = q
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if portals_dir == "" {
|
||||
return fmt.Errorf("Could not find any writable portals directories. Make sure XDG_DATA_HOME is set and point to a directory for which you have write permission.")
|
||||
}
|
||||
portals_defn := filepath.Join(portals_dir, "kitty.portal")
|
||||
if err = os.WriteFile(portals_defn, utils.UnsafeStringToBytes(fmt.Sprintf(
|
||||
`[portal]
|
||||
DBusName=%s
|
||||
Interfaces=%s;
|
||||
`, PORTAL_BUS_NAME, strings.Join(AllPortalInterfaces(), ";"))), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Wrote kitty portal definition to:", portals_defn)
|
||||
dbus_service_dir := ""
|
||||
for _, x := range WritableDataDirs() {
|
||||
q := filepath.Join(x, "dbus-1", "services")
|
||||
if err := os.MkdirAll(q, 0o755); err == nil {
|
||||
dbus_service_dir = q
|
||||
break
|
||||
}
|
||||
}
|
||||
if dbus_service_dir == "" {
|
||||
return fmt.Errorf("Could not find any writable portals directories. Make sure XDG_DATA_HOME is set and point to a directory for which you have write permission.")
|
||||
}
|
||||
dbus_service_defn := filepath.Join(dbus_service_dir, PORTAL_BUS_NAME+".service")
|
||||
exe_path, eerr := os.Executable()
|
||||
if eerr != nil {
|
||||
exe_path = utils.Which("kitten")
|
||||
}
|
||||
if exe_path, err = filepath.Abs(exe_path); eerr != nil {
|
||||
return fmt.Errorf("failed to get path to kitten executable with error: %w", err)
|
||||
}
|
||||
if err = os.WriteFile(dbus_service_defn, utils.UnsafeStringToBytes(fmt.Sprintf(
|
||||
`[D-BUS Service]
|
||||
Name=%s
|
||||
Exec=%s desktop-ui run-server
|
||||
`, PORTAL_BUS_NAME, exe_path)), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Wrote kitty DBUS activation service file to:", dbus_service_defn)
|
||||
|
||||
d := os.Getenv("XDG_CURRENT_DESKTOP")
|
||||
cf := os.Getenv("XDG_CONFIG_HOME")
|
||||
if cf == "" {
|
||||
cf = utils.Expanduser("~/.config")
|
||||
}
|
||||
cf = filepath.Join(cf, "xdg-desktop-portal")
|
||||
if err = os.MkdirAll(cf, 0o755); err != nil {
|
||||
return fmt.Errorf("failed to create %s to store the portals.conf file with error: %w", cf, err)
|
||||
}
|
||||
patched_file := ""
|
||||
desktops := utils.Filter(strings.Split(d, ":"), func(x string) bool { return x != "" })
|
||||
desktops = append(desktops, "")
|
||||
for _, x := range strings.Split(d, ":") {
|
||||
q := filepath.Join(cf, utils.IfElse(x == "", "portals.conf", fmt.Sprintf("%s-portals.conf", strings.ToLower(x))))
|
||||
if text, err := os.ReadFile(q); err == nil {
|
||||
text := patch_portals_conf(text)
|
||||
if err = os.WriteFile(q, text, 0o644); err == nil {
|
||||
patched_file = q
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if patched_file == "" {
|
||||
x := desktops[0]
|
||||
q := filepath.Join(cf, utils.IfElse(x == "", "portals.conf", fmt.Sprintf("%s-portals.conf", strings.ToLower(x))))
|
||||
text := patch_portals_conf([]byte{})
|
||||
if err = os.WriteFile(q, text, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
patched_file = q
|
||||
}
|
||||
fmt.Printf("Patched %s to use the kitty portals\n", patched_file)
|
||||
return
|
||||
}
|
||||
|
||||
// }}}
|
||||
|
||||
type SetOptions struct {
|
||||
Namespace, DataType string
|
||||
}
|
||||
|
||||
func set_variant_setting(namespace, key string, v dbus.Variant, remove_setting bool) (err error) {
|
||||
conn, err := dbus.SessionBus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to system bus with error: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
method := "ChangeSetting"
|
||||
var vals = []any{namespace, key}
|
||||
if remove_setting {
|
||||
method = "RemoveSetting"
|
||||
} else {
|
||||
vals = append(vals, v)
|
||||
}
|
||||
obj := conn.Object(PORTAL_BUS_NAME, dbus.ObjectPath(KITTY_OBJECT_PATH))
|
||||
call := obj.Call(CHANGE_SETTINGS_INTERFACE+"."+method, dbus.FlagNoAutoStart, vals...)
|
||||
if err = call.Store(); err != nil {
|
||||
return fmt.Errorf("failed to call %s with error: %w", method, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func set_setting(key, value string, opts *SetOptions) (err error) {
|
||||
remove_setting := false
|
||||
var v dbus.Variant
|
||||
if value == "" {
|
||||
remove_setting = true
|
||||
} else {
|
||||
if v, err = ParseValueWithSignature(value, opts.DataType); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return set_variant_setting(opts.Namespace, key, v, remove_setting)
|
||||
}
|
||||
|
||||
func set_color_scheme(which string) (err error) {
|
||||
conn, err := dbus.SessionBus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to system bus with error: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
val := NO_PREFERENCE
|
||||
var res ReadAllType
|
||||
if res, err = fetch_settings(conn, PORTAL_APPEARANCE_NAMESPACE); err != nil {
|
||||
return fmt.Errorf("failed to read existing color scheme setting with error: %w", err)
|
||||
}
|
||||
if m, found := res[PORTAL_APPEARANCE_NAMESPACE]; found {
|
||||
if v, found := m[PORTAL_COLOR_SCHEME_KEY]; found {
|
||||
v.Store(&val)
|
||||
}
|
||||
}
|
||||
nval := val
|
||||
switch which {
|
||||
case "toggle":
|
||||
switch val {
|
||||
case LIGHT:
|
||||
nval = DARK
|
||||
case DARK:
|
||||
nval = LIGHT
|
||||
}
|
||||
case "no-preference":
|
||||
nval = NO_PREFERENCE
|
||||
case "light":
|
||||
nval = LIGHT
|
||||
case "dark":
|
||||
nval = DARK
|
||||
default:
|
||||
return fmt.Errorf("%s is not a valid value of the color-scheme", which)
|
||||
}
|
||||
if val == nval {
|
||||
return
|
||||
}
|
||||
obj := conn.Object(PORTAL_BUS_NAME, dbus.ObjectPath(KITTY_OBJECT_PATH))
|
||||
call := obj.Call(CHANGE_SETTINGS_INTERFACE+".ChangeSetting", dbus.FlagNoAutoStart, PORTAL_APPEARANCE_NAMESPACE, PORTAL_COLOR_SCHEME_KEY, dbus.MakeVariant(nval))
|
||||
if err = call.Store(); err != nil {
|
||||
return fmt.Errorf("failed to call ChangeSetting with error: %w", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (self *Portal) ChangeSetting(namespace, key string, value dbus.Variant) *dbus.Error {
|
||||
self.lock.Lock()
|
||||
defer self.lock.Unlock()
|
||||
if self.settings[namespace] == nil {
|
||||
self.settings[namespace] = map[string]dbus.Variant{}
|
||||
}
|
||||
self.settings[namespace][key] = value
|
||||
|
||||
if e := self.bus.Emit(
|
||||
DESKTOP_OBJECT_PATH,
|
||||
SETTINGS_INTERFACE+".SettingChanged",
|
||||
namespace,
|
||||
key,
|
||||
value,
|
||||
); e != nil {
|
||||
log.Println("Couldn't emit signal:", e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Portal) RemoveSetting(namespace, key string) *dbus.Error {
|
||||
self.lock.Lock()
|
||||
defer self.lock.Unlock()
|
||||
existed := false
|
||||
if m := self.settings[namespace]; m != nil {
|
||||
_, existed = m[key]
|
||||
}
|
||||
if !existed {
|
||||
return nil
|
||||
}
|
||||
delete(self.settings[namespace], key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Portal) Read(namespace, key string) (dbus.Variant, *dbus.Error) {
|
||||
self.lock.Lock()
|
||||
defer self.lock.Unlock()
|
||||
if m, found := self.settings[namespace]; found {
|
||||
if v, found := m[key]; found {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
return dbus.Variant{}, dbus.NewError("org.freedesktop.portal.Error.NotFound", []any{fmt.Sprintf("the setting %s in the namespace %s is not supported", key, namespace)})
|
||||
}
|
||||
|
||||
type ReadAllType map[string]map[string]dbus.Variant
|
||||
|
||||
func (self *Portal) ReadAll(namespaces []string) (ReadAllType, *dbus.Error) {
|
||||
self.lock.Lock()
|
||||
defer self.lock.Unlock()
|
||||
var matched_namespaces = SettingsMap{}
|
||||
if len(namespaces) == 0 {
|
||||
matched_namespaces = self.settings
|
||||
} else {
|
||||
for _, namespace := range namespaces {
|
||||
if namespace == "" {
|
||||
matched_namespaces = self.settings
|
||||
break
|
||||
} else {
|
||||
if strings.HasSuffix(namespace, ".*") {
|
||||
namespace = namespace[:len(namespace)-1]
|
||||
for candidate := range self.settings {
|
||||
if strings.HasPrefix(candidate, namespace) {
|
||||
matched_namespaces[candidate] = map[string]dbus.Variant{}
|
||||
}
|
||||
}
|
||||
} else if _, found := self.settings[namespace]; found {
|
||||
matched_namespaces[namespace] = map[string]dbus.Variant{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
values := map[string]map[string]dbus.Variant{}
|
||||
for namespace := range matched_namespaces {
|
||||
values[namespace] = make(map[string]dbus.Variant, len(self.settings[namespace]))
|
||||
maps.Copy(values[namespace], self.settings[namespace])
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
type vmap map[string]dbus.Variant
|
||||
type Filter_expression struct {
|
||||
Ftype uint32
|
||||
Val string
|
||||
}
|
||||
type Filter struct {
|
||||
Name string
|
||||
Expressions []Filter_expression
|
||||
}
|
||||
|
||||
func (f Filter) Equal(o Filter) bool {
|
||||
return f.Name == o.Name && slices.Equal(f.Expressions, o.Expressions)
|
||||
}
|
||||
|
||||
type ChooseFilesData struct {
|
||||
Title string
|
||||
Mode string
|
||||
Cwd string
|
||||
SuggestedSaveFileName, SuggestedSaveFilePath string
|
||||
Handle dbus.ObjectPath
|
||||
Filters []Filter
|
||||
}
|
||||
|
||||
func (c *ChooseFilesData) set_filters(options vmap) {
|
||||
if v, found := options["filters"]; found {
|
||||
v.Store(&c.Filters)
|
||||
}
|
||||
if v, found := options["current_filter"]; found {
|
||||
var x Filter
|
||||
if err := v.Store(&x); err == nil {
|
||||
idx := slices.IndexFunc(c.Filters, func(q Filter) bool { return x.Equal(q) })
|
||||
if idx > -1 {
|
||||
c.Filters = slices.Delete(c.Filters, idx, idx+1)
|
||||
}
|
||||
c.Filters = slices.Insert(c.Filters, 0, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func get_matching_filter(name string, all_filters []Filter) (dbus.Variant, bool) {
|
||||
for _, x := range all_filters {
|
||||
if x.Name == name {
|
||||
return dbus.MakeVariant(x), true
|
||||
}
|
||||
}
|
||||
return dbus.Variant{}, false
|
||||
}
|
||||
|
||||
func (options vmap) get_bool(name string, defval bool) (ans bool) {
|
||||
if v, found := options[name]; found {
|
||||
if v.Store(&ans) == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return defval
|
||||
}
|
||||
|
||||
func (self *Portal) Cleanup() {
|
||||
self.lock.Lock()
|
||||
defer self.lock.Unlock()
|
||||
if self.file_chooser_first_instance != nil {
|
||||
self.file_chooser_first_instance.Process.Signal(unix.SIGTERM)
|
||||
ch := make(chan int)
|
||||
go func() {
|
||||
self.file_chooser_first_instance.Wait()
|
||||
ch <- 0
|
||||
}()
|
||||
select {
|
||||
case <-ch:
|
||||
case <-time.After(time.Second):
|
||||
self.file_chooser_first_instance.Process.Kill()
|
||||
self.file_chooser_first_instance.Wait()
|
||||
}
|
||||
self.file_chooser_first_instance = nil
|
||||
}
|
||||
}
|
||||
|
||||
type ChooserResponse struct {
|
||||
Paths []string `json:"paths"`
|
||||
Error string `json:"error"`
|
||||
Interrupted bool `json:"interrupted"`
|
||||
Current_filter string `json:"current_filter"`
|
||||
}
|
||||
|
||||
func (self *Portal) run_file_chooser(cfd ChooseFilesData) (response uint32, result_dict vmap) {
|
||||
response = RESPONSE_ENDED
|
||||
tdir, err := os.MkdirTemp("", "kitty-cfd")
|
||||
if err != nil {
|
||||
log.Println("cannot run file chooser as failed to create a temporary directory with error: ", err)
|
||||
return
|
||||
}
|
||||
pid_path := filepath.Join(tdir, "pid")
|
||||
var close_requested, child_killed atomic.Bool
|
||||
Close := func() *dbus.Error {
|
||||
close_requested.Store(true)
|
||||
if !child_killed.Load() {
|
||||
if raw, err := os.ReadFile(pid_path); err == nil {
|
||||
if pid, err := strconv.Atoi(string(raw)); err == nil {
|
||||
child_killed.Store(true)
|
||||
unix.Kill(pid, unix.SIGTERM)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
self.bus.ExportMethodTable(map[string]any{"Close": Close}, cfd.Handle, REQUEST_INTERFACE)
|
||||
defer func() {
|
||||
self.bus.ExportMethodTable(nil, cfd.Handle, REQUEST_INTERFACE)
|
||||
_ = os.RemoveAll(tdir)
|
||||
}()
|
||||
output_path := filepath.Join(tdir, "output.json")
|
||||
|
||||
cmd := func() *exec.Cmd {
|
||||
self.lock.Lock()
|
||||
defer self.lock.Unlock()
|
||||
args := []string{
|
||||
"+kitten", "panel", "--layer=overlay", "--edge=center", "--focus-policy=exclusive",
|
||||
"-o", "background_opacity=0.85", "--wait-for-single-instance-window-close",
|
||||
"--single-instance", "--instance-group", "cfp-" + strconv.Itoa(os.Getpid()),
|
||||
}
|
||||
for _, x := range self.opts.File_chooser_kitty_conf {
|
||||
args = append(args, `-c`, x)
|
||||
}
|
||||
for _, x := range self.opts.File_chooser_kitty_override {
|
||||
args = append(args, `-o`, x)
|
||||
}
|
||||
if self.file_chooser_first_instance == nil {
|
||||
fifo_path := filepath.Join(tdir, "fifo")
|
||||
if err := unix.Mkfifo(fifo_path, 0600); err != nil {
|
||||
log.Println("cannot run file chooser as failed to create a fifo directory with error: ", err)
|
||||
return nil
|
||||
}
|
||||
fa := slices.Clone(args)
|
||||
fa = append(fa, "--start-as-hidden", "sh", "-c", "echo a > '"+fifo_path+"'; read")
|
||||
cmd := exec.Command(utils.KittyExe(), fa...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Start()
|
||||
ch := make(chan int)
|
||||
go func() {
|
||||
f, err := os.OpenFile(fifo_path, os.O_RDONLY, os.ModeNamedPipe)
|
||||
if err != nil {
|
||||
log.Println("cannot run file chooser as failed to open fifo for read with error: ", err)
|
||||
}
|
||||
b := []byte{'a', 'b', 'c', 'd'}
|
||||
f.Read(b)
|
||||
ch <- 0
|
||||
}()
|
||||
select {
|
||||
case <-ch:
|
||||
self.file_chooser_first_instance = cmd
|
||||
case <-time.After(5 * time.Second):
|
||||
log.Println("cannot run file chooser as panel script timed out writing to fifo")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
args = append(args, "kitten", `choose-files`, `--mode`, cfd.Mode, `--write-output-to`, output_path, `--output-format=json`)
|
||||
if cfd.SuggestedSaveFileName != "" {
|
||||
args = append(args, `--suggested-save-file-name`, cfd.SuggestedSaveFileName)
|
||||
}
|
||||
if cfd.SuggestedSaveFilePath != "" {
|
||||
args = append(args, `--suggested-save-file-path`, cfd.SuggestedSaveFilePath)
|
||||
}
|
||||
if cfd.Title != "" {
|
||||
args = append(args, "--title", cfd.Title)
|
||||
}
|
||||
for _, fs := range cfd.Filters {
|
||||
for _, exp := range fs.Expressions {
|
||||
args = append(args, "--file-filter", fmt.Sprintf("%s:%s:%s", utils.IfElse(exp.Ftype == 0, "glob", "mime"), exp.Val, fs.Name))
|
||||
}
|
||||
}
|
||||
args = append(args, "--write-pid-to", pid_path)
|
||||
args = append(args, utils.IfElse(cfd.Cwd == "", "~", cfd.Cwd))
|
||||
cmd := exec.Command(utils.KittyExe(), args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd
|
||||
}()
|
||||
if cmd == nil || close_requested.Load() {
|
||||
return
|
||||
}
|
||||
log.Println("running file chooser with args:", cmd.Path, utils.Repr(cmd.Args))
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Println("running file chooser failed with error: ", err)
|
||||
return
|
||||
}
|
||||
if close_requested.Load() {
|
||||
return
|
||||
}
|
||||
raw, err := os.ReadFile(output_path)
|
||||
if err != nil {
|
||||
log.Println("running file chooser failed, could not read from output file with error: ", err)
|
||||
return
|
||||
}
|
||||
if close_requested.Load() {
|
||||
return
|
||||
}
|
||||
var result ChooserResponse
|
||||
if err = json.Unmarshal(raw, &result); err != nil {
|
||||
log.Println("running file chooser failed, invalid JSON response with error: ", err)
|
||||
return
|
||||
}
|
||||
if result.Error != "" {
|
||||
log.Println("running file chooser failed, with error: ", result.Error)
|
||||
return
|
||||
}
|
||||
if result.Interrupted {
|
||||
response = RESPONSE_CANCELED
|
||||
log.Println("running file chooser failed, interrupted by user.")
|
||||
return
|
||||
}
|
||||
response = RESPONSE_SUCCESS
|
||||
prefix := "file://" + utils.IfElse(runtime.GOOS == "windows", "/", "")
|
||||
uris := utils.Map(func(path string) string {
|
||||
path = filepath.ToSlash(path)
|
||||
u := url.URL{Path: path}
|
||||
return prefix + u.EscapedPath()
|
||||
}, result.Paths)
|
||||
result_dict = vmap{"uris": dbus.MakeVariant(uris)}
|
||||
if result.Current_filter != "" {
|
||||
if v, found := get_matching_filter(result.Current_filter, cfd.Filters); found {
|
||||
result_dict["current_filter"] = v
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (options vmap) get_bytearray(name string) string {
|
||||
if v, found := options[name]; found {
|
||||
var b []byte
|
||||
if v.Store(&b) == nil {
|
||||
// the FileChooser spec requires paths and filenames to be null
|
||||
// terminated, so remove trailing nulls.
|
||||
return string(bytes.TrimRight(b, "\x00"))
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (self *Portal) OpenFile(handle dbus.ObjectPath, app_id string, parent_window string, title string, options vmap) (uint32, vmap, *dbus.Error) {
|
||||
cfd := ChooseFilesData{Title: title, Cwd: options.get_bytearray("current_folder"), Handle: handle}
|
||||
cfd.set_filters(options)
|
||||
dir_only := options.get_bool("directory", false)
|
||||
multiple := options.get_bool("multiple", false)
|
||||
if dir_only {
|
||||
cfd.Mode = utils.IfElse(multiple, "dirs", "dir")
|
||||
} else {
|
||||
cfd.Mode = utils.IfElse(multiple, "files", "file")
|
||||
}
|
||||
response, result := self.run_file_chooser(cfd)
|
||||
return response, result, nil
|
||||
}
|
||||
|
||||
func (self *Portal) SaveFile(handle dbus.ObjectPath, app_id string, parent_window string, title string, options vmap) (uint32, vmap, *dbus.Error) {
|
||||
cfd := ChooseFilesData{
|
||||
Title: title, Cwd: options.get_bytearray("current_folder"), Handle: handle,
|
||||
SuggestedSaveFileName: options.get_bytearray("current_name"),
|
||||
SuggestedSaveFilePath: options.get_bytearray("current_file")}
|
||||
multiple := options.get_bool("multiple", false)
|
||||
cfd.set_filters(options)
|
||||
cfd.Mode = utils.IfElse(multiple, "save-files", "save-file")
|
||||
|
||||
response, result := self.run_file_chooser(cfd)
|
||||
return response, result, nil
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
"kitty/tools/utils"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
"kitty/tools/utils"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
"github.com/kovidgoyal/kitty/tools/utils/images"
|
||||
"kitty/tools/utils"
|
||||
"kitty/tools/utils/images"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/lexers"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/tui/graphics"
|
||||
"github.com/kovidgoyal/kitty/tools/tui/loop"
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
"github.com/kovidgoyal/kitty/tools/utils/images"
|
||||
"github.com/kovidgoyal/kitty/tools/utils/shm"
|
||||
"kitty/tools/tui/graphics"
|
||||
"kitty/tools/tui/loop"
|
||||
"kitty/tools/utils"
|
||||
"kitty/tools/utils/images"
|
||||
"kitty/tools/utils/shm"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
@@ -5,8 +5,8 @@ package icat
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/tui/graphics"
|
||||
"github.com/kovidgoyal/kitty/tools/utils/images"
|
||||
"kitty/tools/tui/graphics"
|
||||
"kitty/tools/utils/images"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
@@ -11,13 +11,13 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/cli"
|
||||
"github.com/kovidgoyal/kitty/tools/tty"
|
||||
"github.com/kovidgoyal/kitty/tools/tui"
|
||||
"github.com/kovidgoyal/kitty/tools/tui/graphics"
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
"github.com/kovidgoyal/kitty/tools/utils/images"
|
||||
"github.com/kovidgoyal/kitty/tools/utils/style"
|
||||
"kitty/tools/cli"
|
||||
"kitty/tools/tty"
|
||||
"kitty/tools/tui"
|
||||
"kitty/tools/tui/graphics"
|
||||
"kitty/tools/utils"
|
||||
"kitty/tools/utils/images"
|
||||
"kitty/tools/utils/style"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
@@ -4,13 +4,13 @@ package icat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/kovidgoyal/kitty/tools/tty"
|
||||
"github.com/kovidgoyal/kitty/tools/tui/graphics"
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
"github.com/kovidgoyal/kitty/tools/utils/images"
|
||||
"github.com/kovidgoyal/kitty/tools/utils/shm"
|
||||
"image"
|
||||
"image/gif"
|
||||
"kitty/tools/tty"
|
||||
"kitty/tools/tui/graphics"
|
||||
"kitty/tools/utils"
|
||||
"kitty/tools/utils/images"
|
||||
"kitty/tools/utils/shm"
|
||||
|
||||
"github.com/edwvee/exiffix"
|
||||
"github.com/kovidgoyal/imaging"
|
||||
|
||||
@@ -15,11 +15,11 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/tty"
|
||||
"github.com/kovidgoyal/kitty/tools/tui/graphics"
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
"github.com/kovidgoyal/kitty/tools/utils/images"
|
||||
"github.com/kovidgoyal/kitty/tools/utils/shm"
|
||||
"kitty/tools/tty"
|
||||
"kitty/tools/tui/graphics"
|
||||
"kitty/tools/utils"
|
||||
"kitty/tools/utils/images"
|
||||
"kitty/tools/utils/shm"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
@@ -8,20 +8,20 @@ import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/kovidgoyal/kitty"
|
||||
"io"
|
||||
"kitty"
|
||||
"math"
|
||||
not_rand "math/rand/v2"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/tui"
|
||||
"github.com/kovidgoyal/kitty/tools/tui/graphics"
|
||||
"github.com/kovidgoyal/kitty/tools/tui/loop"
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
"github.com/kovidgoyal/kitty/tools/utils/images"
|
||||
"github.com/kovidgoyal/kitty/tools/utils/shm"
|
||||
"kitty/tools/tui"
|
||||
"kitty/tools/tui/graphics"
|
||||
"kitty/tools/tui/loop"
|
||||
"kitty/tools/utils"
|
||||
"kitty/tools/utils/images"
|
||||
"kitty/tools/utils/shm"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
@@ -12,10 +12,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/cli"
|
||||
"github.com/kovidgoyal/kitty/tools/tty"
|
||||
"github.com/kovidgoyal/kitty/tools/tui/loop"
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
"kitty/tools/cli"
|
||||
"kitty/tools/tty"
|
||||
"kitty/tools/tui/loop"
|
||||
"kitty/tools/utils"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/simdstring"
|
||||
"kitty/tools/simdstring"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user