mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-05 15:41:39 +02:00
Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
436ab9a95a | ||
|
|
35093d2105 | ||
|
|
9212c08638 | ||
|
|
2dea3087b3 | ||
|
|
c48ed15007 | ||
|
|
a0f6152aee | ||
|
|
ef8079eb27 | ||
|
|
ed33e64101 | ||
|
|
167b254d97 | ||
|
|
dc90771780 | ||
|
|
4cdedc422e | ||
|
|
127459012a | ||
|
|
298daa4e83 | ||
|
|
9f2b22c4d6 | ||
|
|
a2255e979f | ||
|
|
9be66bfe4a | ||
|
|
2ac2c17929 | ||
|
|
c6582e9f51 | ||
|
|
4ec94c786d | ||
|
|
ceb712f791 | ||
|
|
aecf13302a | ||
|
|
d598157590 | ||
|
|
e61e95da3a | ||
|
|
37bd77f4a8 | ||
|
|
f067e9cd92 | ||
|
|
49d8b1a9d0 | ||
|
|
811b4fa127 | ||
|
|
c2e75ba466 | ||
|
|
16cdcf8cf8 | ||
|
|
a6335777d9 | ||
|
|
3d5200e1ce | ||
|
|
67ca9f5b7d | ||
|
|
6879432704 | ||
|
|
726c693edf | ||
|
|
a9f80fe05b | ||
|
|
fcccadc8f3 | ||
|
|
8d0fc86bb6 | ||
|
|
2babfa7ebf | ||
|
|
a76f32df2d | ||
|
|
8f91fcefbe | ||
|
|
fa808c3b10 | ||
|
|
9f9216457e | ||
|
|
f0040edff2 | ||
|
|
1f37f065ab | ||
|
|
4af95b3c51 | ||
|
|
224ccb170a | ||
|
|
328745cad9 | ||
|
|
5d1ce327e0 | ||
|
|
e8cfedee07 | ||
|
|
d3c5cb12c4 | ||
|
|
25e1b052b8 | ||
|
|
86698e0b17 | ||
|
|
77074d627d | ||
|
|
e9fc486473 | ||
|
|
a0699f5c9e | ||
|
|
88ec2d9793 | ||
|
|
5af47b4881 | ||
|
|
8d855a7eb4 | ||
|
|
e46a75ca57 | ||
|
|
fdf2c0725c | ||
|
|
da39257020 | ||
|
|
e21d2f5191 | ||
|
|
aa814748a1 | ||
|
|
4545aab5f6 | ||
|
|
9192f35132 | ||
|
|
270c598f2c | ||
|
|
2665a871c0 | ||
|
|
ccdc50007e | ||
|
|
9740861ec5 | ||
|
|
80a617a9ec | ||
|
|
56f26ed919 | ||
|
|
9a4b52f8b9 |
@@ -1,5 +1,7 @@
|
|||||||
= kitty - the fast, feature-rich, cross-platform, GPU based terminal
|
= kitty - the fast, feature-rich, cross-platform, GPU based terminal
|
||||||
|
|
||||||
|
If you live in the terminal, *kitty* is made for **you**!
|
||||||
|
|
||||||
See https://sw.kovidgoyal.net/kitty/[the kitty website].
|
See https://sw.kovidgoyal.net/kitty/[the kitty website].
|
||||||
|
|
||||||
image:https://github.com/kovidgoyal/kitty/workflows/CI/badge.svg["Build status", link="https://github.com/kovidgoyal/kitty/actions?query=workflow%3ACI"]
|
image:https://github.com/kovidgoyal/kitty/workflows/CI/badge.svg["Build status", link="https://github.com/kovidgoyal/kitty/actions?query=workflow%3ACI"]
|
||||||
|
|||||||
@@ -30,10 +30,10 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "openssl 3.5.2",
|
"name": "openssl 3.5.4",
|
||||||
"unix": {
|
"unix": {
|
||||||
"file_extension": "tar.gz",
|
"file_extension": "tar.gz",
|
||||||
"hash": "sha256:c53a47e5e441c930c3928cf7bf6fb00e5d129b630e0aa873b08258656e7345ec",
|
"hash": "sha256:967311f84955316969bdb1d8d4b983718ef42338639c621ec4c34fddef355e99",
|
||||||
"urls": ["https://www.openssl.org/source/{filename}"]
|
"urls": ["https://www.openssl.org/source/{filename}"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -134,6 +134,29 @@ consumption to do the same tasks.
|
|||||||
Detailed list of changes
|
Detailed list of changes
|
||||||
-------------------------------------
|
-------------------------------------
|
||||||
|
|
||||||
|
0.43.2 [future]
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
- Allow kitty to read a specified set of environment variables from your
|
||||||
|
login shell at startup using the :opt:`env` directive in kitty.conf
|
||||||
|
(:iss:`9042`)
|
||||||
|
|
||||||
|
- Fix a regression in 0.43.0 that caused a black flicker when closing a tab in
|
||||||
|
the presence of a background image (:iss:`9060`)
|
||||||
|
|
||||||
|
- Splits layout: Fix a bug that could cause a corrupted layout in some
|
||||||
|
circumstances (:iss:`9059`)
|
||||||
|
|
||||||
|
- Fix a regression in the previous release that broke ``goto_session -1``
|
||||||
|
|
||||||
|
- Fix rendering broken on ancient GPU drivers that do not support rendering to 16 bit textures (:iss:`9068`)
|
||||||
|
|
||||||
|
- Fix tab bar sometimes showing incorrect tabs when it is filtered to show only
|
||||||
|
tabs from the current session (:iss:`9079`)
|
||||||
|
|
||||||
|
- macOS: Workaround for bug in macOS Tahoe that caused OS Windows that are
|
||||||
|
fullscreen to crash kitty when returning from sleep on some machines (:iss:`8983`)
|
||||||
|
|
||||||
0.43.1 [2025-10-01]
|
0.43.1 [2025-10-01]
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|||||||
10
docs/faq.rst
10
docs/faq.rst
@@ -480,6 +480,16 @@ related to localization, such as :envvar:`LANG`, ``LC_*`` and loading of
|
|||||||
configuration files such as ``XDG_*``, :envvar:`KITTY_CONFIG_DIRECTORY` and,
|
configuration files such as ``XDG_*``, :envvar:`KITTY_CONFIG_DIRECTORY` and,
|
||||||
most importantly, ``PATH`` to locate binaries.
|
most importantly, ``PATH`` to locate binaries.
|
||||||
|
|
||||||
|
The simplest way to fix this is to have kitty load the environment variables
|
||||||
|
from your shell configuration at startup using the :opt:`env` directive,
|
||||||
|
adding the following to :file:`kitty.conf`::
|
||||||
|
|
||||||
|
env read_from_shell=PATH LANG LC_* XDG_* EDITOR VISUAL
|
||||||
|
|
||||||
|
This works for POSIX compliant shells and the fish shell. Note that it
|
||||||
|
does add significantly to kitty startup time, so use only if really necessary.
|
||||||
|
This feature was added in version ``0.43.2``.
|
||||||
|
|
||||||
To see the environment variables that kitty sees, you can add the following
|
To see the environment variables that kitty sees, you can add the following
|
||||||
mapping to :file:`kitty.conf`::
|
mapping to :file:`kitty.conf`::
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
kitty
|
kitty
|
||||||
==========================================================
|
==========================================================
|
||||||
|
|
||||||
*The fast, feature-rich, GPU based terminal emulator*
|
*If you live in the terminal, kitty is made for YOU!*
|
||||||
|
|
||||||
|
The fast, feature-rich, GPU based terminal emulator.
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:hidden:
|
:hidden:
|
||||||
@@ -20,7 +22,7 @@ kitty
|
|||||||
|
|
||||||
.. tab:: Fast
|
.. tab:: Fast
|
||||||
|
|
||||||
* Uses GPU and SIMD vector CPU instructions for :doc:`best in class <performance>`
|
* Uses GPU and SIMD vector CPU instructions for :doc:`best in class performance <performance>`
|
||||||
* Uses threaded rendering for :iss:`absolutely minimal latency <2701#issuecomment-636497270>`
|
* Uses threaded rendering for :iss:`absolutely minimal latency <2701#issuecomment-636497270>`
|
||||||
* Performance tradeoffs can be :ref:`tuned <conf-kitty-performance>`
|
* Performance tradeoffs can be :ref:`tuned <conf-kitty-performance>`
|
||||||
|
|
||||||
|
|||||||
@@ -122,9 +122,9 @@ the table below:
|
|||||||
.. table:: Types of input to kittens
|
.. table:: Types of input to kittens
|
||||||
:align: left
|
:align: left
|
||||||
|
|
||||||
=========================== =======================================================================================================
|
=========================== ========================================================================================
|
||||||
Keyword Type of :file:`STDIN` input
|
Keyword Type of :file:`STDIN` input
|
||||||
=========================== =======================================================================================================
|
=========================== ========================================================================================
|
||||||
``text`` Plain text of active window
|
``text`` Plain text of active window
|
||||||
``ansi`` Formatted text of active window
|
``ansi`` Formatted text of active window
|
||||||
``screen`` Plain text of active window with line wrap markers
|
``screen`` Plain text of active window with line wrap markers
|
||||||
@@ -141,7 +141,7 @@ the table below:
|
|||||||
``output-screen-ansi`` Formatted text of the output from the last run command with wrap markers
|
``output-screen-ansi`` Formatted text of the output from the last run command with wrap markers
|
||||||
|
|
||||||
``selection`` The text currently selected with the mouse
|
``selection`` The text currently selected with the mouse
|
||||||
=========================== =======================================================================================================
|
=========================== ========================================================================================
|
||||||
|
|
||||||
In addition to ``output``, that gets the output of the last run command,
|
In addition to ``output``, that gets the output of the last run command,
|
||||||
``last_visited_output`` gives the output of the command last jumped to
|
``last_visited_output`` gives the output of the command last jumped to
|
||||||
@@ -298,107 +298,6 @@ So if you run kitty from another kitty instance, the output will be visible
|
|||||||
in the first kitty instance.
|
in the first kitty instance.
|
||||||
|
|
||||||
|
|
||||||
Adding options to kittens
|
|
||||||
----------------------------
|
|
||||||
|
|
||||||
If you would like to use kitty's config framework to make your kittens
|
|
||||||
configurable, you will need some boilerplate. Put the following files in the
|
|
||||||
directory of your kitten.
|
|
||||||
|
|
||||||
:file:`kitten_options_definition.py`
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from kitty.conf.types import Action, Definition
|
|
||||||
|
|
||||||
definition = Definition(
|
|
||||||
'!kitten_options_utils',
|
|
||||||
Action(
|
|
||||||
'map', 'parse_map',
|
|
||||||
{'key_definitions': 'kitty.conf.utils.KittensKeyMap'},
|
|
||||||
['kitty.types.ParsedShortcut', 'kitty.conf.utils.KeyAction']
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
agr = definition.add_group
|
|
||||||
egr = definition.end_group
|
|
||||||
opt = definition.add_option
|
|
||||||
map = definition.add_map
|
|
||||||
|
|
||||||
# main options {{{
|
|
||||||
agr('main', 'Main')
|
|
||||||
|
|
||||||
opt('some_option', '33',
|
|
||||||
option_type='some_option_parser',
|
|
||||||
long_text='''
|
|
||||||
Help text for this option
|
|
||||||
'''
|
|
||||||
)
|
|
||||||
egr() # }}}
|
|
||||||
|
|
||||||
# shortcuts {{{
|
|
||||||
agr('shortcuts', 'Keyboard shortcuts')
|
|
||||||
|
|
||||||
map('Quit', 'quit q quit')
|
|
||||||
egr() # }}}
|
|
||||||
|
|
||||||
|
|
||||||
:file:`kitten_options_utils.py`
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from kitty.conf.utils import KittensKeyDefinition, key_func, parse_kittens_key
|
|
||||||
|
|
||||||
func_with_args, args_funcs = key_func()
|
|
||||||
FuncArgsType = Tuple[str, Sequence[Any]]
|
|
||||||
|
|
||||||
def some_option_parser(val: str) -> int:
|
|
||||||
return int(val) + 3000
|
|
||||||
|
|
||||||
def parse_map(val: str) -> Iterable[KittensKeyDefinition]:
|
|
||||||
x = parse_kittens_key(val, args_funcs)
|
|
||||||
if x is not None:
|
|
||||||
yield x
|
|
||||||
|
|
||||||
Then run::
|
|
||||||
|
|
||||||
kitty +runpy 'from kitty.conf.generate import main; main()' /path/to/kitten_options_definition.py
|
|
||||||
|
|
||||||
You can parse and read the options in your kitten using the following code:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from .kitten_options_types import Options, defaults
|
|
||||||
from kitty.conf.utils import load_config as _load_config, parse_config_base
|
|
||||||
from typing import Optional, Iterable, Dict, Any
|
|
||||||
|
|
||||||
def load_config(*paths: str, overrides: Optional[Iterable[str]] = None) -> Options:
|
|
||||||
from .kitten_options_parse import (
|
|
||||||
create_result_dict, merge_result_dicts, parse_conf_item
|
|
||||||
)
|
|
||||||
|
|
||||||
def parse_config(lines: Iterable[str]) -> Dict[str, Any]:
|
|
||||||
ans: Dict[str, Any] = create_result_dict()
|
|
||||||
parse_config_base(
|
|
||||||
lines,
|
|
||||||
parse_conf_item,
|
|
||||||
ans,
|
|
||||||
)
|
|
||||||
return ans
|
|
||||||
|
|
||||||
overrides = tuple(overrides) if overrides is not None else ()
|
|
||||||
opts_dict, found_paths = _load_config(defaults, parse_config, merge_result_dicts, *paths, overrides=overrides)
|
|
||||||
opts = Options(opts_dict)
|
|
||||||
opts.config_paths = found_paths
|
|
||||||
opts.all_config_paths = paths
|
|
||||||
opts.config_overrides = overrides
|
|
||||||
return opts
|
|
||||||
|
|
||||||
See `the code <https://github.com/kovidgoyal/kitty/tree/master/kittens/diff>`__
|
|
||||||
for the builtin :doc:`diff kitten </kittens/diff>` for examples of creating more
|
|
||||||
options and keyboard shortcuts.
|
|
||||||
|
|
||||||
|
|
||||||
Developing builtin kittens for inclusion with kitty
|
Developing builtin kittens for inclusion with kitty
|
||||||
----------------------------------------------------------
|
----------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -230,6 +230,15 @@ copying to clipboard. In the editor, you can map it to copy to the clipboard,
|
|||||||
thereby allowing use of a common shortcut both inside and outside the editor
|
thereby allowing use of a common shortcut both inside and outside the editor
|
||||||
for copying to clipboard.
|
for copying to clipboard.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
When using multi-key mappings, of the form :kbd:`k1>k2` or similar, the
|
||||||
|
condition applies to the first key and you can have only one condition per
|
||||||
|
key, the last in kitty.conf wins. In particular, this means you cannot have
|
||||||
|
multiple conditions applying to multi-key mappings with the same first key
|
||||||
|
and you cannot have mappings with and without conditions applying to multi-keys
|
||||||
|
with the same first key.
|
||||||
|
|
||||||
Sending arbitrary text or keys to the program running in kitty
|
Sending arbitrary text or keys to the program running in kitty
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -262,6 +262,12 @@ as shown below:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The payloads for the different remote control commands are documented in the
|
||||||
|
:doc:`remote control protocol specification <rc_protocol>`.
|
||||||
|
|
||||||
|
|
||||||
.. _rc_mapping:
|
.. _rc_mapping:
|
||||||
|
|
||||||
Mapping key presses to remote control commands
|
Mapping key presses to remote control commands
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import (
|
|||||||
|
|
||||||
var _ = fmt.Print
|
var _ = fmt.Print
|
||||||
|
|
||||||
|
//go:embed logo/kitty.png
|
||||||
|
var KittyLogoAsPNGData []byte
|
||||||
|
|
||||||
//go:embed kitty_tests/GraphemeBreakTest.json
|
//go:embed kitty_tests/GraphemeBreakTest.json
|
||||||
var grapheme_break_test_data []byte
|
var grapheme_break_test_data []byte
|
||||||
|
|
||||||
|
|||||||
@@ -661,7 +661,7 @@ var QueryNames = []string{{ {query_names} }}
|
|||||||
var CommentedOutDefaultConfig = "{serialize_as_go_string(commented_out_default_config())}"
|
var CommentedOutDefaultConfig = "{serialize_as_go_string(commented_out_default_config())}"
|
||||||
var KittyConfigDefaults = struct {{
|
var KittyConfigDefaults = struct {{
|
||||||
Term, Shell_integration, Select_by_word_characters, Url_excluded_characters, Shell string
|
Term, Shell_integration, Select_by_word_characters, Url_excluded_characters, Shell string
|
||||||
Wheel_scroll_multiplier int
|
Wheel_scroll_multiplier float64
|
||||||
Url_prefixes []string
|
Url_prefixes []string
|
||||||
}}{{
|
}}{{
|
||||||
Term: "{Options.term}", Shell_integration: "{' '.join(Options.shell_integration)}", Url_prefixes: []string{{ {url_prefixes} }},
|
Term: "{Options.term}", Shell_integration: "{' '.join(Options.shell_integration)}", Url_prefixes: []string{{ {url_prefixes} }},
|
||||||
|
|||||||
1
glfw/cocoa_platform.h
vendored
1
glfw/cocoa_platform.h
vendored
@@ -138,6 +138,7 @@ typedef struct _GLFWwindowNS
|
|||||||
int fbWidth, fbHeight;
|
int fbWidth, fbHeight;
|
||||||
float xscale, yscale;
|
float xscale, yscale;
|
||||||
int blur_radius;
|
int blur_radius;
|
||||||
|
bool live_resize_in_progress;
|
||||||
|
|
||||||
// The total sum of the distances the cursor has been warped
|
// The total sum of the distances the cursor has been warped
|
||||||
// since the last cursor motion event was processed
|
// since the last cursor motion event was processed
|
||||||
|
|||||||
@@ -537,6 +537,7 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
|
|||||||
if (self != nil) {
|
if (self != nil) {
|
||||||
window = initWindow;
|
window = initWindow;
|
||||||
_lastScreenStates = [self captureScreenStates];
|
_lastScreenStates = [self captureScreenStates];
|
||||||
|
window->ns.live_resize_in_progress = false;
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
@@ -566,7 +567,12 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
|
|||||||
(void)notification;
|
(void)notification;
|
||||||
NSArray<NSDictionary *> *currentScreenStates = [self captureScreenStates];
|
NSArray<NSDictionary *> *currentScreenStates = [self captureScreenStates];
|
||||||
const bool is_screen_change = ![_lastScreenStates isEqualToArray:currentScreenStates];
|
const bool is_screen_change = ![_lastScreenStates isEqualToArray:currentScreenStates];
|
||||||
debug_rendering("windowDidResize() called, is_screen_change: %d\n", is_screen_change);
|
NSWindowStyleMask sm = [window->ns.object styleMask];
|
||||||
|
const bool is_fullscreen = (sm & NSWindowStyleMaskFullScreen) != 0;
|
||||||
|
NSRect frame = [window->ns.object frame];
|
||||||
|
debug_rendering(
|
||||||
|
"windowDidResize() called, is_screen_change: %d is_fullscreen: %d live_resize_in_progress: %d frame: %.1fx%.1f@(%.1f, %.1f)\n",
|
||||||
|
is_screen_change, is_fullscreen, window->ns.live_resize_in_progress, frame.size.width, frame.size.height, frame.origin.x, frame.origin.y);
|
||||||
if (is_screen_change) {
|
if (is_screen_change) {
|
||||||
// This resize likely happened because a screen was added, removed, or changed resolution.
|
// This resize likely happened because a screen was added, removed, or changed resolution.
|
||||||
[_lastScreenStates release];
|
[_lastScreenStates release];
|
||||||
@@ -608,7 +614,8 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
|
|||||||
// Because of a bug in macOS Tahoe we cannot redraw the window in response
|
// Because of a bug in macOS Tahoe we cannot redraw the window in response
|
||||||
// to a resize event that was caused by a screen change as the OpenGL
|
// to a resize event that was caused by a screen change as the OpenGL
|
||||||
// context is not ready yet. See: https://github.com/kovidgoyal/kitty/issues/8983
|
// context is not ready yet. See: https://github.com/kovidgoyal/kitty/issues/8983
|
||||||
if (window->ns.resizeCallback && !is_screen_change) window->ns.resizeCallback((GLFWwindow*)window);
|
if (window->ns.resizeCallback && !is_screen_change && !is_fullscreen && window->ns.live_resize_in_progress)
|
||||||
|
window->ns.resizeCallback((GLFWwindow*)window);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)windowDidMove:(NSNotification *)notification
|
- (void)windowDidMove:(NSNotification *)notification
|
||||||
@@ -824,12 +831,14 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
|
|||||||
- (void) viewWillStartLiveResize
|
- (void) viewWillStartLiveResize
|
||||||
{
|
{
|
||||||
if (!window) return;
|
if (!window) return;
|
||||||
|
window->ns.live_resize_in_progress = true;
|
||||||
_glfwInputLiveResize(window, true);
|
_glfwInputLiveResize(window, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)viewDidEndLiveResize
|
- (void)viewDidEndLiveResize
|
||||||
{
|
{
|
||||||
if (!window) return;
|
if (!window) return;
|
||||||
|
window->ns.live_resize_in_progress = false;
|
||||||
_glfwInputLiveResize(window, false);
|
_glfwInputLiveResize(window, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -13,7 +13,7 @@ require (
|
|||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1
|
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1
|
||||||
github.com/kovidgoyal/exiffix v0.0.0-20250919160812-dbef770c2032
|
github.com/kovidgoyal/exiffix v0.0.0-20250919160812-dbef770c2032
|
||||||
github.com/kovidgoyal/imaging v1.6.4
|
github.com/kovidgoyal/imaging v1.6.5
|
||||||
github.com/seancfoley/ipaddress-go v1.7.1
|
github.com/seancfoley/ipaddress-go v1.7.1
|
||||||
github.com/shirou/gopsutil/v3 v3.24.5
|
github.com/shirou/gopsutil/v3 v3.24.5
|
||||||
github.com/zeebo/xxh3 v1.0.2
|
github.com/zeebo/xxh3 v1.0.2
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -28,8 +28,8 @@ github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1 h1:rMY/hWfcVzBm6BL
|
|||||||
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1/go.mod h1:RbNG3Q1g6GUy1/WzWVx+S24m7VKyvl57vV+cr2hpt50=
|
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1/go.mod h1:RbNG3Q1g6GUy1/WzWVx+S24m7VKyvl57vV+cr2hpt50=
|
||||||
github.com/kovidgoyal/exiffix v0.0.0-20250919160812-dbef770c2032 h1:TEV9lpo2a6fP1byiDsoJe2fXpvrj2itae41xMM+bEAg=
|
github.com/kovidgoyal/exiffix v0.0.0-20250919160812-dbef770c2032 h1:TEV9lpo2a6fP1byiDsoJe2fXpvrj2itae41xMM+bEAg=
|
||||||
github.com/kovidgoyal/exiffix v0.0.0-20250919160812-dbef770c2032/go.mod h1:VU38Nlbvb0lbyS5YkopCZMS5HuJ5QLVJBxRWyzq79q4=
|
github.com/kovidgoyal/exiffix v0.0.0-20250919160812-dbef770c2032/go.mod h1:VU38Nlbvb0lbyS5YkopCZMS5HuJ5QLVJBxRWyzq79q4=
|
||||||
github.com/kovidgoyal/imaging v1.6.4 h1:K0idhRPXnRrJBKnBYcTfI1HTWSNDeAn7hYDvf9I0dCk=
|
github.com/kovidgoyal/imaging v1.6.5 h1:Id9DKlz/ydl5Vxt9QG5IjGSiIcHcszSKXxDubdO49PQ=
|
||||||
github.com/kovidgoyal/imaging v1.6.4/go.mod h1:bEIgsaZmXlvFfkv/CUxr9rJook6AQkJnpB5EPosRfRY=
|
github.com/kovidgoyal/imaging v1.6.5/go.mod h1:mBprO214rATK/6OaPAUXmHbSMelPSFEmoBAt/IJdmno=
|
||||||
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik=
|
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik=
|
||||||
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
|
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
|||||||
322
kittens/choose_files/graphics.go
Normal file
322
kittens/choose_files/graphics.go
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
package choose_files
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = fmt.Print
|
||||||
|
|
||||||
|
type placement struct {
|
||||||
|
gc *graphics.GraphicsCommand
|
||||||
|
x, y, x_offset int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p placement) equal(o placement) bool {
|
||||||
|
return p.x == o.x && p.x_offset == o.x_offset && p.y == o.y
|
||||||
|
}
|
||||||
|
|
||||||
|
type GraphicsHandler struct {
|
||||||
|
running_in_tmux bool
|
||||||
|
image_id_counter, detection_file_id uint32
|
||||||
|
files_to_delete []string
|
||||||
|
files_supported atomic.Bool
|
||||||
|
last_rendered_image struct {
|
||||||
|
p *ImagePreview
|
||||||
|
width, height int
|
||||||
|
image_width, image_height int
|
||||||
|
}
|
||||||
|
image_transmitted uint32
|
||||||
|
current_placement, last_transmitted_placement placement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *GraphicsHandler) Cleanup() {
|
||||||
|
for _, f := range self.files_to_delete {
|
||||||
|
_ = os.Remove(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *GraphicsHandler) new_graphics_command() *graphics.GraphicsCommand {
|
||||||
|
gc := graphics.GraphicsCommand{}
|
||||||
|
if self.running_in_tmux {
|
||||||
|
gc.WrapPrefix = "\033Ptmux;"
|
||||||
|
gc.WrapSuffix = "\033\\"
|
||||||
|
gc.EncodeSerializedDataFunc = func(x string) string { return strings.ReplaceAll(x, "\033", "\033\033") }
|
||||||
|
}
|
||||||
|
return &gc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *GraphicsHandler) Initialize(lp *loop.Loop) error {
|
||||||
|
tmux := tui.TmuxSocketAddress()
|
||||||
|
if tmux != "" && tui.TmuxAllowPassthrough() == nil {
|
||||||
|
self.running_in_tmux = true
|
||||||
|
}
|
||||||
|
if !self.running_in_tmux {
|
||||||
|
g := func(t graphics.GRT_t, payload string) uint32 {
|
||||||
|
self.image_id_counter++
|
||||||
|
g1 := self.new_graphics_command()
|
||||||
|
g1.SetTransmission(t).SetAction(graphics.GRT_action_query).SetImageId(self.image_id_counter).SetDataWidth(1).SetDataHeight(1).SetFormat(
|
||||||
|
graphics.GRT_format_rgb).SetDataSize(uint64(len(payload)))
|
||||||
|
_ = g1.WriteWithPayloadToLoop(lp, utils.UnsafeStringToBytes(payload))
|
||||||
|
return self.image_id_counter
|
||||||
|
}
|
||||||
|
tf, err := images.CreateTempInRAM()
|
||||||
|
if err == nil {
|
||||||
|
if _, err = tf.Write([]byte{1, 2, 3}); err == nil {
|
||||||
|
self.detection_file_id = g(graphics.GRT_transmission_tempfile, tf.Name())
|
||||||
|
self.files_to_delete = append(self.files_to_delete, tf.Name())
|
||||||
|
}
|
||||||
|
tf.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
self.image_id_counter++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *GraphicsHandler) free_image_from_terminal(lp *loop.Loop) {
|
||||||
|
if self.image_transmitted > 0 {
|
||||||
|
self.new_graphics_command().SetAction(graphics.GRT_action_delete).SetDelete(graphics.GRT_free_by_id).SetImageId(self.image_transmitted).WriteWithPayloadToLoop(lp, nil)
|
||||||
|
self.image_transmitted = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *GraphicsHandler) Finalize(lp *loop.Loop) {
|
||||||
|
self.free_image_from_terminal(lp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *GraphicsHandler) ClearPlacements(lp *loop.Loop) {
|
||||||
|
self.current_placement.gc = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *GraphicsHandler) ApplyPlacements(lp *loop.Loop) {
|
||||||
|
if self.current_placement.gc == nil {
|
||||||
|
g := self.new_graphics_command()
|
||||||
|
g.SetAction(graphics.GRT_action_delete).SetDelete(graphics.GRT_delete_by_id).SetImageId(self.image_transmitted)
|
||||||
|
_ = g.WriteWithPayloadToLoop(lp, nil)
|
||||||
|
self.last_transmitted_placement.gc = nil
|
||||||
|
} else {
|
||||||
|
if self.last_transmitted_placement.gc == nil || !self.current_placement.equal(self.last_transmitted_placement) {
|
||||||
|
lp.MoveCursorTo(self.current_placement.x, self.current_placement.y)
|
||||||
|
_ = self.current_placement.gc.WriteWithPayloadToLoop(lp, nil)
|
||||||
|
self.last_transmitted_placement = self.current_placement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *GraphicsHandler) HandleGraphicsCommand(gc *graphics.GraphicsCommand) error {
|
||||||
|
switch gc.ImageId() {
|
||||||
|
case self.detection_file_id:
|
||||||
|
if gc.ResponseMessage() == "OK" {
|
||||||
|
self.files_supported.Store(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *GraphicsHandler) cache_resized_image(cdir, cache_key string, img *images.ImageData) (m *images.SerializableImageMetadata, cached_data map[string]string, err error) {
|
||||||
|
s, frames := img.Serialize()
|
||||||
|
sd, err := json.Marshal(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
path := filepath.Join(cdir, fmt.Sprintf("rsz-%s-metadata.json", cache_key))
|
||||||
|
if err = os.WriteFile(path, sd, 0o600); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to write resized frame metadata to cache: %w", err)
|
||||||
|
}
|
||||||
|
cached_data = make(map[string]string, len(frames)+1)
|
||||||
|
cached_data[IMAGE_METADATA_KEY] = path
|
||||||
|
for i, f := range frames {
|
||||||
|
path := filepath.Join(cdir, fmt.Sprintf("rsz-%s-%d", cache_key, i))
|
||||||
|
key := IMAGE_DATA_PREFIX + strconv.Itoa(i)
|
||||||
|
if err = os.WriteFile(path, f, 0o600); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to write resized frame %d data to cache: %w", i, err)
|
||||||
|
}
|
||||||
|
cached_data[key] = path
|
||||||
|
}
|
||||||
|
m = &s
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *GraphicsHandler) cached_resized_image(cdir, cache_key string) (m *images.SerializableImageMetadata, cached_data map[string]string) {
|
||||||
|
path := filepath.Join(cdir, fmt.Sprintf("rsz-%s-metadata.json", cache_key))
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var s images.SerializableImageMetadata
|
||||||
|
if err = json.Unmarshal(b, &s); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m = &s
|
||||||
|
cached_data = make(map[string]string, len(s.Frames)+1)
|
||||||
|
cached_data[IMAGE_METADATA_KEY] = path
|
||||||
|
for i := range len(s.Frames) {
|
||||||
|
path := filepath.Join(cdir, fmt.Sprintf("rsz-%s-%d", cache_key, i))
|
||||||
|
key := IMAGE_DATA_PREFIX + strconv.Itoa(i)
|
||||||
|
cached_data[key] = path
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func transmit_by_escape_code(lp *loop.Loop, frame []byte, gc *graphics.GraphicsCommand) {
|
||||||
|
atomic := lp.IsAtomicUpdateActive()
|
||||||
|
lp.EndAtomicUpdate()
|
||||||
|
gc.SetTransmission(graphics.GRT_transmission_direct)
|
||||||
|
_ = gc.WriteWithPayloadToLoop(lp, frame)
|
||||||
|
if atomic {
|
||||||
|
lp.StartAtomicUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func transmit_by_file(lp *loop.Loop, frame_path []byte, gc *graphics.GraphicsCommand) {
|
||||||
|
gc.SetTransmission(graphics.GRT_transmission_file)
|
||||||
|
_ = gc.WriteWithPayloadToLoop(lp, frame_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *GraphicsHandler) transmit(lp *loop.Loop, img *images.ImageData, m *images.SerializableImageMetadata, cached_data map[string]string) {
|
||||||
|
if m == nil {
|
||||||
|
s := img.SerializeOnlyMetadata()
|
||||||
|
m = &s
|
||||||
|
}
|
||||||
|
self.image_transmitted = self.image_id_counter
|
||||||
|
self.last_transmitted_placement.gc = nil
|
||||||
|
self.last_rendered_image.image_width = m.Width
|
||||||
|
self.last_rendered_image.image_height = m.Height
|
||||||
|
is_animated := len(m.Frames) > 0
|
||||||
|
frame_control_cmd := self.new_graphics_command()
|
||||||
|
frame_control_cmd.SetAction(graphics.GRT_action_animate).SetImageId(self.image_transmitted)
|
||||||
|
for frame_num, frame := range m.Frames {
|
||||||
|
gc := self.new_graphics_command()
|
||||||
|
gc.SetImageId(self.image_transmitted)
|
||||||
|
gc.SetDataWidth(uint64(frame.Width)).SetDataHeight(uint64(frame.Height))
|
||||||
|
gc.SetFormat(utils.IfElse(frame.Is_opaque, graphics.GRT_format_rgb, graphics.GRT_format_rgba))
|
||||||
|
switch frame_num {
|
||||||
|
case 0:
|
||||||
|
gc.SetAction(graphics.GRT_action_transmit)
|
||||||
|
gc.SetCursorMovement(graphics.GRT_cursor_static)
|
||||||
|
default:
|
||||||
|
gc.SetAction(graphics.GRT_action_frame)
|
||||||
|
gc.SetGap(int32(frame.Delay_ms))
|
||||||
|
if frame.Compose_onto > 0 {
|
||||||
|
gc.SetOverlaidFrame(uint64(frame.Compose_onto))
|
||||||
|
}
|
||||||
|
gc.SetLeftEdge(uint64(frame.Left)).SetTopEdge(uint64(frame.Top))
|
||||||
|
}
|
||||||
|
if cached_data == nil {
|
||||||
|
transmit_by_escape_code(lp, img.Frames[frame_num].Data(), gc)
|
||||||
|
} else {
|
||||||
|
path := cached_data[IMAGE_DATA_PREFIX+strconv.Itoa(frame_num)]
|
||||||
|
transmit_by_file(lp, utils.UnsafeStringToBytes(path), gc)
|
||||||
|
}
|
||||||
|
if is_animated {
|
||||||
|
switch frame_num {
|
||||||
|
case 0:
|
||||||
|
// set gap for the first frame and number of loops for the animation
|
||||||
|
c := frame_control_cmd
|
||||||
|
c.SetTargetFrame(uint64(frame.Number))
|
||||||
|
c.SetGap(int32(frame.Delay_ms))
|
||||||
|
c.SetNumberOfLoops(1)
|
||||||
|
_ = c.WriteWithPayloadToLoop(lp, nil)
|
||||||
|
case 1:
|
||||||
|
c := frame_control_cmd
|
||||||
|
c.SetAnimationControl(2) // set animation to loading mode
|
||||||
|
_ = c.WriteWithPayloadToLoop(lp, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if is_animated {
|
||||||
|
c := frame_control_cmd
|
||||||
|
c.SetAnimationControl(3) // set animation to normal mode
|
||||||
|
_ = c.WriteWithPayloadToLoop(lp, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *GraphicsHandler) place_image(x, y, px_width, y_offset int, sz ScreenSize) {
|
||||||
|
gc := self.new_graphics_command()
|
||||||
|
gc.SetAction(graphics.GRT_action_display).SetImageId(self.image_transmitted).SetPlacementId(1).SetCursorMovement(graphics.GRT_cursor_static)
|
||||||
|
if extra := px_width - self.last_rendered_image.image_width; extra > 1 {
|
||||||
|
extra /= 2
|
||||||
|
x += extra / sz.cell_width
|
||||||
|
self.current_placement.x_offset = extra % sz.cell_width
|
||||||
|
gc.SetXOffset(uint64(self.current_placement.x_offset))
|
||||||
|
}
|
||||||
|
gc.SetYOffset(uint64(y_offset))
|
||||||
|
self.current_placement.x, self.current_placement.y = x, y
|
||||||
|
self.current_placement.gc = gc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *GraphicsHandler) RenderImagePreview(h *Handler, p *ImagePreview, x, y, width, height int) {
|
||||||
|
sz := h.screen_size
|
||||||
|
px_width, px_height := width*sz.cell_width, height*sz.cell_height
|
||||||
|
y_offset := sz.cell_height / 2
|
||||||
|
px_height -= y_offset
|
||||||
|
var err error
|
||||||
|
defer func() {
|
||||||
|
self.last_rendered_image.p = p
|
||||||
|
self.last_rendered_image.width, self.last_rendered_image.height = width, height
|
||||||
|
if err != nil {
|
||||||
|
NewErrorPreview(fmt.Errorf("Failed to render image: %w", err)).Render(h, x, y, width, height)
|
||||||
|
} else if self.image_transmitted > 0 {
|
||||||
|
self.place_image(x, y, px_width, y_offset, sz)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if self.last_rendered_image.p == p && self.last_rendered_image.width == width && self.last_rendered_image.height == height {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
files_supported := self.files_supported.Load()
|
||||||
|
|
||||||
|
if p.img_metadata.Width <= px_width && p.img_metadata.Height <= px_height {
|
||||||
|
if files_supported {
|
||||||
|
self.transmit(h.lp, nil, p.img_metadata, p.cached_data)
|
||||||
|
} else {
|
||||||
|
if err = p.ensure_source_image(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.transmit(h.lp, p.source_img, p.img_metadata, nil)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cache_key := fmt.Sprintf("%d-%d-%p", width, height, p)
|
||||||
|
img_metadata, cached_data := self.cached_resized_image(p.disk_cache.ResultsDir(), cache_key)
|
||||||
|
var img *images.ImageData
|
||||||
|
if len(cached_data) == 0 {
|
||||||
|
if err = p.ensure_source_image(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
img = p.source_img
|
||||||
|
final_width, final_height := images.FitImage(img.Width, img.Height, px_width, px_height)
|
||||||
|
if final_width != img.Width || final_height != img.Height {
|
||||||
|
x_frac, y_frac := float64(final_width)/float64(img.Width), float64(final_height)/float64(img.Height)
|
||||||
|
img = img.Resize(x_frac, y_frac)
|
||||||
|
}
|
||||||
|
if img_metadata, cached_data, err = self.cache_resized_image(p.disk_cache.ResultsDir(), cache_key, img); err != nil {
|
||||||
|
err = fmt.Errorf("failed to cache resized image: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if files_supported {
|
||||||
|
self.transmit(h.lp, img, img_metadata, cached_data)
|
||||||
|
} else {
|
||||||
|
if img == nil {
|
||||||
|
if img, err = load_image(cached_data); err != nil {
|
||||||
|
err = fmt.Errorf("failed to load resized image from cache: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.transmit(h.lp, img, nil, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
247
kittens/choose_files/image_preview.go
Normal file
247
kittens/choose_files/image_preview.go
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
package choose_files
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/kovidgoyal/kitty/tools/disk_cache"
|
||||||
|
"github.com/kovidgoyal/kitty/tools/icons"
|
||||||
|
"github.com/kovidgoyal/kitty/tools/utils"
|
||||||
|
"github.com/kovidgoyal/kitty/tools/utils/humanize"
|
||||||
|
"github.com/kovidgoyal/kitty/tools/utils/images"
|
||||||
|
)
|
||||||
|
|
||||||
|
const IMAGE_METADATA_KEY = "image-metadata.json"
|
||||||
|
const IMAGE_DATA_PREFIX = "image-data-"
|
||||||
|
|
||||||
|
var dc_size atomic.Int64
|
||||||
|
var _ = fmt.Print
|
||||||
|
|
||||||
|
var preview_cache = sync.OnceValues(func() (*disk_cache.DiskCache, error) {
|
||||||
|
cdir := utils.CacheDir()
|
||||||
|
cdir = filepath.Join(cdir, "choose-files")
|
||||||
|
return disk_cache.NewDiskCache(cdir, dc_size.Load())
|
||||||
|
})
|
||||||
|
|
||||||
|
type ShowData struct {
|
||||||
|
abspath string
|
||||||
|
metadata fs.FileInfo
|
||||||
|
x, y, width, height int
|
||||||
|
cached_data map[string]string
|
||||||
|
img_metadata *images.SerializableImageMetadata
|
||||||
|
}
|
||||||
|
|
||||||
|
type PreviewRenderer interface {
|
||||||
|
Render(string) (map[string][]byte, *images.ImageData, error)
|
||||||
|
ShowMetadata(h *Handler, s ShowData) int
|
||||||
|
}
|
||||||
|
|
||||||
|
type render_data struct {
|
||||||
|
cached_data map[string]string
|
||||||
|
img *images.ImageData
|
||||||
|
img_metadata *images.SerializableImageMetadata
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImagePreview struct {
|
||||||
|
abspath string
|
||||||
|
metadata fs.FileInfo
|
||||||
|
disk_cache *disk_cache.DiskCache
|
||||||
|
cached_data map[string]string
|
||||||
|
render_err Preview
|
||||||
|
render_channel chan render_data
|
||||||
|
source_img *images.ImageData
|
||||||
|
img_metadata *images.SerializableImageMetadata
|
||||||
|
renderer PreviewRenderer
|
||||||
|
file_metadata_preview Preview
|
||||||
|
WakeupMainThread func() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ImagePreview) IsValidForColorScheme(bool) bool { return true }
|
||||||
|
|
||||||
|
func (p *ImagePreview) Unload() {
|
||||||
|
p.source_img = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func load_image(cached_data map[string]string) (img *images.ImageData, err error) {
|
||||||
|
fp := cached_data[IMAGE_METADATA_KEY]
|
||||||
|
if fp == "" {
|
||||||
|
return nil, fmt.Errorf("missing cached image metadata")
|
||||||
|
}
|
||||||
|
b, err := os.ReadFile(fp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read cached image metadata: %w", err)
|
||||||
|
}
|
||||||
|
var m images.SerializableImageMetadata
|
||||||
|
if err = json.Unmarshal(b, &m); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode cached image metadata: %w", err)
|
||||||
|
}
|
||||||
|
frames := make([][]byte, len(m.Frames))
|
||||||
|
for i := range m.Frames {
|
||||||
|
path := cached_data[IMAGE_DATA_PREFIX+strconv.Itoa(i)]
|
||||||
|
if path == "" {
|
||||||
|
return nil, fmt.Errorf("missing cached data for frame: %d", i)
|
||||||
|
}
|
||||||
|
d, e := os.ReadFile(path)
|
||||||
|
if e != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read cached image frame %d data: %w", i, e)
|
||||||
|
}
|
||||||
|
m.Frames[i].Size = len(d)
|
||||||
|
frames[i] = d
|
||||||
|
}
|
||||||
|
return images.ImageFromSerialized(m, frames)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ImagePreview) ensure_source_image() (err error) {
|
||||||
|
if p.source_img != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
p.render_err = NewErrorPreview(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
p.source_img, err = load_image(p.cached_data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ImagePreview) render_image(h *Handler, x, y, width, height int) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
text, _ := utils.Format_stacktrace_on_panic(r)
|
||||||
|
h.err_chan <- fmt.Errorf("%s", text)
|
||||||
|
p.WakeupMainThread()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
offset := p.renderer.ShowMetadata(h, ShowData{
|
||||||
|
abspath: p.abspath, metadata: p.metadata, x: x, y: y, width: width, height: height, cached_data: p.cached_data,
|
||||||
|
img_metadata: p.img_metadata,
|
||||||
|
})
|
||||||
|
h.graphics_handler.RenderImagePreview(h, p, x, y+offset, width, height-offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ImagePreview) Render(h *Handler, x, y, width, height int) {
|
||||||
|
if p.render_channel == nil {
|
||||||
|
if p.render_err == nil {
|
||||||
|
p.render_image(h, x, y, width, height)
|
||||||
|
} else {
|
||||||
|
p.render_err.Render(h, x, y, width, height)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case hd := <-p.render_channel:
|
||||||
|
p.render_channel = nil
|
||||||
|
p.cached_data = hd.cached_data
|
||||||
|
p.source_img = hd.img
|
||||||
|
p.img_metadata = hd.img_metadata
|
||||||
|
if hd.err != nil {
|
||||||
|
p.render_err = NewErrorPreview(fmt.Errorf("Failed to render the preview with error: %w", hd.err))
|
||||||
|
}
|
||||||
|
p.Render(h, x, y, width, height)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
if p.file_metadata_preview == nil {
|
||||||
|
p.file_metadata_preview = NewFileMetadataPreview(p.abspath, p.metadata)
|
||||||
|
m := p.file_metadata_preview.(*MessagePreview)
|
||||||
|
m.trailers = append(m.trailers, "", "Rendering image preview, please wait…")
|
||||||
|
}
|
||||||
|
p.file_metadata_preview.Render(h, x, y, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ImagePreview) start_rendering() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
text, _ := utils.Format_stacktrace_on_panic(r)
|
||||||
|
p.render_channel <- render_data{err: fmt.Errorf("%s", text)}
|
||||||
|
}
|
||||||
|
close(p.render_channel)
|
||||||
|
p.WakeupMainThread()
|
||||||
|
}()
|
||||||
|
key, ans, err := p.disk_cache.GetPath(p.abspath)
|
||||||
|
if err != nil {
|
||||||
|
p.render_channel <- render_data{err: err}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(ans) > 0 {
|
||||||
|
if d := ans[IMAGE_METADATA_KEY]; d != "" {
|
||||||
|
if b, err := os.ReadFile(d); err == nil {
|
||||||
|
var m images.SerializableImageMetadata
|
||||||
|
if err = json.Unmarshal(b, &m); err == nil {
|
||||||
|
p.render_channel <- render_data{cached_data: ans, img_metadata: &m}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rdata, img, err := p.renderer.Render(p.abspath)
|
||||||
|
if err != nil {
|
||||||
|
p.render_channel <- render_data{err: err}
|
||||||
|
} else {
|
||||||
|
ans, err = p.disk_cache.AddPath(p.abspath, key, rdata)
|
||||||
|
if err == nil {
|
||||||
|
m := img.SerializeOnlyMetadata()
|
||||||
|
p.render_channel <- render_data{cached_data: ans, img_metadata: &m, img: img}
|
||||||
|
} else {
|
||||||
|
p.render_channel <- render_data{err: err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImagePreviewRenderer uint
|
||||||
|
|
||||||
|
func (p ImagePreviewRenderer) Render(abspath string) (ans map[string][]byte, img *images.ImageData, err error) {
|
||||||
|
if img, err = images.OpenImageFromPath(abspath); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
m, data := img.Serialize()
|
||||||
|
ans = make(map[string][]byte, len(data)+1)
|
||||||
|
metadata, err := json.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
ans[IMAGE_METADATA_KEY] = metadata
|
||||||
|
for i, d := range data {
|
||||||
|
key := IMAGE_DATA_PREFIX + strconv.Itoa(i)
|
||||||
|
ans[key] = d
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ImagePreviewRenderer) ShowMetadata(h *Handler, s ShowData) int {
|
||||||
|
text := ""
|
||||||
|
offset := 0
|
||||||
|
if s.img_metadata != nil {
|
||||||
|
text = fmt.Sprintf("%s: %dx%d %s", s.img_metadata.Format_uppercase, s.img_metadata.Width, s.img_metadata.Height, humanize.Bytes(uint64(s.metadata.Size())))
|
||||||
|
icon := icons.IconForPath("/a.gif")
|
||||||
|
text = icon + " " + text
|
||||||
|
offset += h.render_wrapped_text_in_region(text, s.x, s.y, s.width, s.height, true)
|
||||||
|
}
|
||||||
|
offset += h.render_wrapped_text_in_region(humanize.Time(s.metadata.ModTime()), s.x, s.y+offset, s.width, s.height-offset, true)
|
||||||
|
return offset
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewImagePreview(
|
||||||
|
abspath string, metadata fs.FileInfo, opts Settings, WakeupMainThread func() bool, r PreviewRenderer,
|
||||||
|
) (Preview, error) {
|
||||||
|
dc_size.Store(opts.DiskCacheSize())
|
||||||
|
ans := &ImagePreview{
|
||||||
|
abspath: abspath, metadata: metadata, render_channel: make(chan render_data, 1),
|
||||||
|
WakeupMainThread: WakeupMainThread, renderer: r,
|
||||||
|
}
|
||||||
|
if dc, err := preview_cache(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
ans.disk_cache = dc
|
||||||
|
}
|
||||||
|
go ans.start_rendering()
|
||||||
|
return ans, nil
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/kovidgoyal/kitty/tools/ignorefiles"
|
"github.com/kovidgoyal/kitty/tools/ignorefiles"
|
||||||
"github.com/kovidgoyal/kitty/tools/tty"
|
"github.com/kovidgoyal/kitty/tools/tty"
|
||||||
"github.com/kovidgoyal/kitty/tools/tui"
|
"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/loop"
|
||||||
"github.com/kovidgoyal/kitty/tools/tui/readline"
|
"github.com/kovidgoyal/kitty/tools/tui/readline"
|
||||||
"github.com/kovidgoyal/kitty/tools/utils"
|
"github.com/kovidgoyal/kitty/tools/utils"
|
||||||
@@ -123,6 +124,7 @@ type State struct {
|
|||||||
display_title bool
|
display_title bool
|
||||||
pygments_style, dark_pygments_style string
|
pygments_style, dark_pygments_style string
|
||||||
syntax_aliases map[string]string
|
syntax_aliases map[string]string
|
||||||
|
max_disk_cache_size int64
|
||||||
|
|
||||||
selections []string
|
selections []string
|
||||||
current_idx CollectionIndex
|
current_idx CollectionIndex
|
||||||
@@ -131,6 +133,7 @@ type State struct {
|
|||||||
redraw_needed bool
|
redraw_needed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s State) DiskCacheSize() int64 { return s.max_disk_cache_size }
|
||||||
func (s State) HighlightStyles() (string, string) { return s.pygments_style, s.dark_pygments_style }
|
func (s State) HighlightStyles() (string, string) { return s.pygments_style, s.dark_pygments_style }
|
||||||
func (s State) SyntaxAliases() map[string]string { return s.syntax_aliases }
|
func (s State) SyntaxAliases() map[string]string { return s.syntax_aliases }
|
||||||
func (s State) DisplayTitle() bool { return s.display_title }
|
func (s State) DisplayTitle() bool { return s.display_title }
|
||||||
@@ -204,16 +207,29 @@ type ScreenSize struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
state State
|
state State
|
||||||
screen_size ScreenSize
|
screen_size ScreenSize
|
||||||
result_manager *ResultManager
|
result_manager *ResultManager
|
||||||
lp *loop.Loop
|
lp *loop.Loop
|
||||||
rl *readline.Readline
|
rl *readline.Readline
|
||||||
err_chan chan error
|
err_chan chan error
|
||||||
shortcut_tracker config.ShortcutTracker
|
shortcut_tracker config.ShortcutTracker
|
||||||
msg_printer *message.Printer
|
msg_printer *message.Printer
|
||||||
spinner *tui.Spinner
|
spinner *tui.Spinner
|
||||||
preview_manager *PreviewManager
|
preview_manager *PreviewManager
|
||||||
|
last_rendered_preview Preview
|
||||||
|
graphics_handler GraphicsHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Handler) on_escape_code(etype loop.EscapeCodeType, payload []byte) error {
|
||||||
|
switch etype {
|
||||||
|
case loop.APC:
|
||||||
|
gc := graphics.GraphicsCommandFromAPC(payload)
|
||||||
|
if gc != nil {
|
||||||
|
return self.graphics_handler.HandleGraphicsCommand(gc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) draw_screen() (err error) {
|
func (h *Handler) draw_screen() (err error) {
|
||||||
@@ -222,15 +238,8 @@ func (h *Handler) draw_screen() (err error) {
|
|||||||
defer func() {
|
defer func() {
|
||||||
h.state.mouse_state.UpdateHoveredIds()
|
h.state.mouse_state.UpdateHoveredIds()
|
||||||
h.state.mouse_state.ApplyHoverStyles(h.lp)
|
h.state.mouse_state.ApplyHoverStyles(h.lp)
|
||||||
h.lp.EndAtomicUpdate()
|
h.graphics_handler.ApplyPlacements(h.lp)
|
||||||
}()
|
if h.state.screen == NORMAL { // so that the cursor ends up in the right place
|
||||||
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.lp.MoveCursorTo(1, 1)
|
||||||
if h.state.DisplayTitle() {
|
if h.state.DisplayTitle() {
|
||||||
h.lp.Println(h.state.WindowTitle())
|
h.lp.Println(h.state.WindowTitle())
|
||||||
@@ -238,7 +247,16 @@ func (h *Handler) draw_screen() (err error) {
|
|||||||
} else {
|
} else {
|
||||||
h.draw_search_bar(0)
|
h.draw_search_bar(0)
|
||||||
}
|
}
|
||||||
}()
|
}
|
||||||
|
h.lp.EndAtomicUpdate()
|
||||||
|
}()
|
||||||
|
h.lp.ClearScreenButNotGraphics()
|
||||||
|
h.graphics_handler.ClearPlacements(h.lp)
|
||||||
|
h.state.mouse_state.ClearCellRegions()
|
||||||
|
switch h.state.screen {
|
||||||
|
case NORMAL:
|
||||||
|
matches, is_complete := h.get_results()
|
||||||
|
h.lp.SetWindowTitle(h.state.WindowTitle())
|
||||||
y := SEARCH_BAR_HEIGHT + utils.IfElse(h.state.DisplayTitle(), 1, 0)
|
y := SEARCH_BAR_HEIGHT + utils.IfElse(h.state.DisplayTitle(), 1, 0)
|
||||||
footer_height, err := h.draw_footer()
|
footer_height, err := h.draw_footer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -292,6 +310,7 @@ func (h *Handler) OnInitialize() (ans string, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
err = h.graphics_handler.Initialize(h.lp)
|
||||||
h.result_manager.set_root_dir()
|
h.result_manager.set_root_dir()
|
||||||
h.draw_screen()
|
h.draw_screen()
|
||||||
return
|
return
|
||||||
@@ -718,6 +737,7 @@ func (h *Handler) set_state_from_config(conf *Config, opts *Options) (err error)
|
|||||||
h.state.pygments_style = conf.Pygments_style
|
h.state.pygments_style = conf.Pygments_style
|
||||||
h.state.dark_pygments_style = conf.Dark_pygments_style
|
h.state.dark_pygments_style = conf.Dark_pygments_style
|
||||||
h.state.syntax_aliases = conf.Syntax_aliases
|
h.state.syntax_aliases = conf.Syntax_aliases
|
||||||
|
h.state.max_disk_cache_size = int64(conf.Cache_size * (1024 * 1024 * 1024))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -775,6 +795,7 @@ func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) {
|
|||||||
lp.MouseTrackingMode(loop.FULL_MOUSE_TRACKING)
|
lp.MouseTrackingMode(loop.FULL_MOUSE_TRACKING)
|
||||||
lp.ColorSchemeChangeNotifications()
|
lp.ColorSchemeChangeNotifications()
|
||||||
handler := Handler{lp: lp, err_chan: make(chan error, 8), msg_printer: message.NewPrinter(utils.LanguageTag()), spinner: tui.NewSpinner("dots")}
|
handler := Handler{lp: lp, err_chan: make(chan error, 8), msg_printer: message.NewPrinter(utils.LanguageTag()), spinner: tui.NewSpinner("dots")}
|
||||||
|
defer handler.graphics_handler.Cleanup()
|
||||||
handler.rl = readline.New(lp, readline.RlInit{
|
handler.rl = readline.New(lp, readline.RlInit{
|
||||||
Prompt: "> ", ContinuationPrompt: ". ", Completer: FilePromptCompleter(handler.state.CurrentDir),
|
Prompt: "> ", ContinuationPrompt: ". ", Completer: FilePromptCompleter(handler.state.CurrentDir),
|
||||||
})
|
})
|
||||||
@@ -810,6 +831,10 @@ func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) {
|
|||||||
lp.RequestCurrentColorScheme()
|
lp.RequestCurrentColorScheme()
|
||||||
return handler.OnInitialize()
|
return handler.OnInitialize()
|
||||||
}
|
}
|
||||||
|
lp.OnFinalize = func() string {
|
||||||
|
handler.graphics_handler.Finalize(lp)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
lp.OnResize = func(old, new_size loop.ScreenSize) (err error) {
|
lp.OnResize = func(old, new_size loop.ScreenSize) (err error) {
|
||||||
handler.init_sizes(new_size)
|
handler.init_sizes(new_size)
|
||||||
return handler.draw_screen()
|
return handler.draw_screen()
|
||||||
@@ -826,6 +851,7 @@ func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) {
|
|||||||
lp.OnKeyEvent = handler.OnKeyEvent
|
lp.OnKeyEvent = handler.OnKeyEvent
|
||||||
lp.OnText = handler.OnText
|
lp.OnText = handler.OnText
|
||||||
lp.OnMouseEvent = handler.OnMouseEvent
|
lp.OnMouseEvent = handler.OnMouseEvent
|
||||||
|
lp.OnEscapeCode = handler.on_escape_code
|
||||||
lp.OnWakeup = func() (err error) {
|
lp.OnWakeup = func() (err error) {
|
||||||
select {
|
select {
|
||||||
case err = <-handler.err_chan:
|
case err = <-handler.err_chan:
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ builtin styles <https://pygments.org/styles/>` for a list of schemes.
|
|||||||
This sets the colors used for dark color schemes, use :opt:`pygments_style` to change the
|
This sets the colors used for dark color schemes, use :opt:`pygments_style` to change the
|
||||||
colors for light color schemes.''')
|
colors for light color schemes.''')
|
||||||
|
|
||||||
|
opt('cache_size', '0.5', option_type='positive_float', long_text='''
|
||||||
|
The maximum size of the disk cache, in gigabytes, used for previews. Zero or negative values
|
||||||
|
mean no limit.
|
||||||
|
''')
|
||||||
|
|
||||||
opt('syntax_aliases', 'pyj:py pyi:py recipe:py', ctype='strdict_ _:', option_type='syntax_aliases',
|
opt('syntax_aliases', 'pyj:py pyi:py recipe:py', ctype='strdict_ _:', option_type='syntax_aliases',
|
||||||
long_text='''
|
long_text='''
|
||||||
File extension aliases for syntax highlight. For example, to syntax highlight
|
File extension aliases for syntax highlight. For example, to syntax highlight
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ var _ = fmt.Print
|
|||||||
type Preview interface {
|
type Preview interface {
|
||||||
Render(h *Handler, x, y, width, height int)
|
Render(h *Handler, x, y, width, height int)
|
||||||
IsValidForColorScheme(light bool) bool
|
IsValidForColorScheme(light bool) bool
|
||||||
|
Unload()
|
||||||
}
|
}
|
||||||
|
|
||||||
type PreviewManager struct {
|
type PreviewManager struct {
|
||||||
@@ -80,6 +81,7 @@ type MessagePreview struct {
|
|||||||
|
|
||||||
func (p MessagePreview) IsValidForColorScheme(bool) bool { return true }
|
func (p MessagePreview) IsValidForColorScheme(bool) bool { return true }
|
||||||
|
|
||||||
|
func (p MessagePreview) Unload() {}
|
||||||
func (p MessagePreview) Render(h *Handler, x, y, width, height int) {
|
func (p MessagePreview) Render(h *Handler, x, y, width, height int) {
|
||||||
offset := 0
|
offset := 0
|
||||||
if p.title != "" {
|
if p.title != "" {
|
||||||
@@ -189,6 +191,8 @@ type TextFilePreview struct {
|
|||||||
|
|
||||||
func (p TextFilePreview) IsValidForColorScheme(light bool) bool { return p.light == light }
|
func (p TextFilePreview) IsValidForColorScheme(light bool) bool { return p.light == light }
|
||||||
|
|
||||||
|
func (p *TextFilePreview) Unload() {}
|
||||||
|
|
||||||
func (p *TextFilePreview) Render(h *Handler, x, y, width, height int) {
|
func (p *TextFilePreview) Render(h *Handler, x, y, width, height int) {
|
||||||
if p.highlighted_chan != nil {
|
if p.highlighted_chan != nil {
|
||||||
select {
|
select {
|
||||||
@@ -255,13 +259,19 @@ func (pm *PreviewManager) highlight_file_async(path string, output chan highligh
|
|||||||
s := style_resolver{light: use_light_colors, syntax_aliases: pm.settings.SyntaxAliases()}
|
s := style_resolver{light: use_light_colors, syntax_aliases: pm.settings.SyntaxAliases()}
|
||||||
s.light_style, s.dark_style = pm.settings.HighlightStyles()
|
s.light_style, s.dark_style = pm.settings.HighlightStyles()
|
||||||
go func() {
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
text, _ := utils.Format_stacktrace_on_panic(r)
|
||||||
|
debugprintln(fmt.Sprintf("Failed to highlight: %s with panic: %s", path, text))
|
||||||
|
}
|
||||||
|
close(output)
|
||||||
|
pm.WakeupMainThread()
|
||||||
|
}()
|
||||||
highlighted, err := pm.highlighter.HighlightFile(path, &s)
|
highlighted, err := pm.highlighter.HighlightFile(path, &s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debugprintln(fmt.Sprintf("Failed to highlight: %s with error: %s", path, err))
|
debugprintln(fmt.Sprintf("Failed to highlight: %s with error: %s", path, err))
|
||||||
}
|
}
|
||||||
output <- highlighed_data{text: highlighted, err: err, light: s.light}
|
output <- highlighed_data{text: highlighted, err: err, light: s.light}
|
||||||
close(output)
|
|
||||||
pm.WakeupMainThread()
|
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,6 +307,14 @@ func (pm *PreviewManager) preview_for(abspath string, ftype fs.FileMode) (ans Pr
|
|||||||
pm.highlight_file_async(abspath, ch)
|
pm.highlight_file_async(abspath, ch)
|
||||||
return NewTextFilePreview(abspath, s, ch, pm.highlighter.Sanitize)
|
return NewTextFilePreview(abspath, s, ch, pm.highlighter.Sanitize)
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(mt, "image/") {
|
||||||
|
var r ImagePreviewRenderer
|
||||||
|
if ans, err := NewImagePreview(abspath, s, pm.settings, pm.WakeupMainThread, r); err == nil {
|
||||||
|
return ans
|
||||||
|
} else {
|
||||||
|
return NewErrorPreview(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
return NewFileMetadataPreview(abspath, s)
|
return NewFileMetadataPreview(abspath, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,9 +326,14 @@ func (h *Handler) draw_preview_content(x, y, width, height int) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
abspath := filepath.Join(h.state.CurrentDir(), r.text)
|
abspath := filepath.Join(h.state.CurrentDir(), r.text)
|
||||||
|
if h.last_rendered_preview != nil {
|
||||||
|
h.last_rendered_preview.Unload()
|
||||||
|
h.last_rendered_preview = nil
|
||||||
|
}
|
||||||
if p := h.preview_manager.preview_for(abspath, r.ftype); p == nil {
|
if p := h.preview_manager.preview_for(abspath, r.ftype); p == nil {
|
||||||
h.render_wrapped_text_in_region("No preview available", x, y, width, height, false)
|
h.render_wrapped_text_in_region("No preview available", x, y, width, height, false)
|
||||||
} else {
|
} else {
|
||||||
|
h.last_rendered_preview = p
|
||||||
p.Render(h, x, y, width, height)
|
p.Render(h, x, y, width, height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,8 +235,8 @@ func (h *Handler) draw_list_of_results(matches *SortedResults, y, height int) (n
|
|||||||
}
|
}
|
||||||
if num_matches > height {
|
if num_matches > height {
|
||||||
col_width = BASE_COL_WIDTH
|
col_width = BASE_COL_WIDTH
|
||||||
num_cols = available_width / col_width
|
num_cols = max(1, available_width/col_width)
|
||||||
for num_cols > 0 && height*(num_cols-1) >= num_matches {
|
for num_cols > 1 && height*(num_cols-1) >= num_matches {
|
||||||
num_cols--
|
num_cols--
|
||||||
}
|
}
|
||||||
col_width = available_width / num_cols
|
col_width = available_width / num_cols
|
||||||
|
|||||||
@@ -634,6 +634,7 @@ type Settings interface {
|
|||||||
GlobalIgnores() ignorefiles.IgnoreFile
|
GlobalIgnores() ignorefiles.IgnoreFile
|
||||||
HighlightStyles() (string, string)
|
HighlightStyles() (string, string)
|
||||||
SyntaxAliases() map[string]string
|
SyntaxAliases() map[string]string
|
||||||
|
DiskCacheSize() int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResultManager struct {
|
type ResultManager struct {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ var highlighter = sync.OnceValue(func() highlight.Highlighter {
|
|||||||
func highlight_all(paths []string, light bool) {
|
func highlight_all(paths []string, light bool) {
|
||||||
ctx := images.Context{}
|
ctx := images.Context{}
|
||||||
srd := prefer_light_colors(light)
|
srd := prefer_light_colors(light)
|
||||||
ctx.Parallel(0, len(paths), func(nums <-chan int) {
|
if err := ctx.SafeParallel(0, len(paths), func(nums <-chan int) {
|
||||||
for i := range nums {
|
for i := range nums {
|
||||||
path := paths[i]
|
path := paths[i]
|
||||||
raw, err := highlighter().HighlightFile(path, &srd)
|
raw, err := highlighter().HighlightFile(path, &srd)
|
||||||
@@ -45,5 +45,7 @@ func highlight_all(paths []string, light bool) {
|
|||||||
dark_highlighted_lines_cache.Set(path, text_to_lines(raw))
|
dark_highlighted_lines_cache.Set(path, text_to_lines(raw))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package diff
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -20,7 +21,7 @@ import (
|
|||||||
var _ = fmt.Print
|
var _ = fmt.Print
|
||||||
|
|
||||||
type KittyOpts struct {
|
type KittyOpts struct {
|
||||||
Wheel_scroll_multiplier int
|
Wheel_scroll_multiplier float64
|
||||||
Copy_on_select bool
|
Copy_on_select bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ func read_relevant_kitty_opts() KittyOpts {
|
|||||||
handle_line := func(key, val string) error {
|
handle_line := func(key, val string) error {
|
||||||
switch key {
|
switch key {
|
||||||
case "wheel_scroll_multiplier":
|
case "wheel_scroll_multiplier":
|
||||||
v, err := strconv.Atoi(val)
|
v, err := strconv.ParseFloat(val, 64)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
ans.Wheel_scroll_multiplier = v
|
ans.Wheel_scroll_multiplier = v
|
||||||
}
|
}
|
||||||
@@ -47,7 +48,10 @@ var RelevantKittyOpts = sync.OnceValue(func() KittyOpts {
|
|||||||
})
|
})
|
||||||
|
|
||||||
func (self *Handler) handle_wheel_event(up bool) {
|
func (self *Handler) handle_wheel_event(up bool) {
|
||||||
amt := RelevantKittyOpts().Wheel_scroll_multiplier
|
amt := int(math.Round(RelevantKittyOpts().Wheel_scroll_multiplier))
|
||||||
|
if amt == 0 {
|
||||||
|
amt = 1
|
||||||
|
}
|
||||||
if up {
|
if up {
|
||||||
amt *= -1
|
amt *= -1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -358,14 +358,16 @@ func diff(jobs []diff_job, context_count int) (ans map[string]*Patch, err error)
|
|||||||
patch *Patch
|
patch *Patch
|
||||||
}
|
}
|
||||||
results := make(chan result, len(jobs))
|
results := make(chan result, len(jobs))
|
||||||
ctx.Parallel(0, len(jobs), func(nums <-chan int) {
|
if err := ctx.SafeParallel(0, len(jobs), func(nums <-chan int) {
|
||||||
for i := range nums {
|
for i := range nums {
|
||||||
job := jobs[i]
|
job := jobs[i]
|
||||||
r := result{file1: job.file1, file2: job.file2}
|
r := result{file1: job.file1, file2: job.file2}
|
||||||
r.patch, r.err = do_diff(job.file1, job.file2, context_count)
|
r.patch, r.err = do_diff(job.file1, job.file2, context_count)
|
||||||
results <- r
|
results <- r
|
||||||
}
|
}
|
||||||
})
|
}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
close(results)
|
close(results)
|
||||||
for r := range results {
|
for r := range results {
|
||||||
if r.err != nil {
|
if r.err != nil {
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ func (self *Search) search(logical_lines *LogicalLines) {
|
|||||||
self.matches = make(map[ScrollPos][]Span)
|
self.matches = make(map[ScrollPos][]Span)
|
||||||
ctx := images.Context{}
|
ctx := images.Context{}
|
||||||
mutex := sync.Mutex{}
|
mutex := sync.Mutex{}
|
||||||
ctx.Parallel(0, logical_lines.Len(), func(nums <-chan int) {
|
if err := ctx.SafeParallel(0, logical_lines.Len(), func(nums <-chan int) {
|
||||||
for i := range nums {
|
for i := range nums {
|
||||||
line := logical_lines.At(i)
|
line := logical_lines.At(i)
|
||||||
if line.line_type == EMPTY_LINE || line.line_type == IMAGE_LINE {
|
if line.line_type == EMPTY_LINE || line.line_type == IMAGE_LINE {
|
||||||
@@ -121,7 +121,9 @@ func (self *Search) search(logical_lines *LogicalLines) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
for _, spans := range self.matches {
|
for _, spans := range self.matches {
|
||||||
slices.SortFunc(spans, func(a, b Span) int { return a.start - b.start })
|
slices.SortFunc(spans, func(a, b Span) int { return a.start - b.start })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,8 +299,12 @@ func process_arg(arg input_arg) {
|
|||||||
}
|
}
|
||||||
err = render_image_with_go(&imgd, &f)
|
err = render_image_with_go(&imgd, &f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
report_error(arg.value, "Could not render image to RGB", err)
|
merr := render_image_with_magick(&imgd, &f)
|
||||||
return
|
if merr != nil {
|
||||||
|
report_error(arg.value, "Could not render image to RGB", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = nil
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err = render_image_with_magick(&imgd, &f)
|
err = render_image_with_magick(&imgd, &f)
|
||||||
|
|||||||
@@ -76,14 +76,18 @@ vec3 color_to_vec(uint c) {
|
|||||||
return vec3(gamma_lut[r], gamma_lut[g], gamma_lut[b]);
|
return vec3(gamma_lut[r], gamma_lut[g], gamma_lut[b]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#define one_if_equal_zero_otherwise(a, b) (1.0f - zero_or_one(abs(float(a) - float(b))))
|
float one_if_equal_zero_otherwise(float a, float b) { return (1.0f - zero_or_one(abs(float(a) - float(b)))); }
|
||||||
|
// Wee need an integer variant to accommodate GPU driver bugs, see
|
||||||
|
// https://github.com/kovidgoyal/kitty/issues/9072
|
||||||
|
uint one_if_equal_zero_otherwise(int a, int b) { return (1u - uint(zero_or_one(abs(float(a) - float(b))))); }
|
||||||
|
uint one_if_equal_zero_otherwise(uint a, uint b) { return (1u - uint(zero_or_one(abs(float(a) - float(b))))); }
|
||||||
|
|
||||||
|
|
||||||
uint resolve_color(uint c, uint defval) {
|
uint resolve_color(uint c, uint defval) {
|
||||||
// Convert a cell color to an actual color based on the color table
|
// Convert a cell color to an actual color based on the color table
|
||||||
int t = int(c & BYTE_MASK);
|
int t = int(c & BYTE_MASK);
|
||||||
uint is_one = uint(one_if_equal_zero_otherwise(t, 1));
|
uint is_one = one_if_equal_zero_otherwise(t, 1);
|
||||||
uint is_two = uint(one_if_equal_zero_otherwise(t, 2));
|
uint is_two = one_if_equal_zero_otherwise(t, 2);
|
||||||
uint is_neither_one_nor_two = 1u - is_one - is_two;
|
uint is_neither_one_nor_two = 1u - is_one - is_two;
|
||||||
return is_one * color_table[(c >> 8) & BYTE_MASK] + is_two * (c >> 8) + is_neither_one_nor_two * defval;
|
return is_one * color_table[(c >> 8) & BYTE_MASK] + is_two * (c >> 8) + is_neither_one_nor_two * defval;
|
||||||
}
|
}
|
||||||
@@ -94,7 +98,7 @@ vec3 to_color(uint c, uint defval) {
|
|||||||
|
|
||||||
vec3 resolve_dynamic_color(uint c, vec3 special_val, vec3 defval) {
|
vec3 resolve_dynamic_color(uint c, vec3 special_val, vec3 defval) {
|
||||||
float type = float((c >> 24) & BYTE_MASK);
|
float type = float((c >> 24) & BYTE_MASK);
|
||||||
#define q(which, val) one_if_equal_zero_otherwise(type, which) * val
|
#define q(which, val) one_if_equal_zero_otherwise(type, float(which)) * val
|
||||||
return (
|
return (
|
||||||
q(COLOR_IS_RGB, color_to_vec(c)) + q(COLOR_IS_INDEX, color_to_vec(color_table[c & BYTE_MASK])) +
|
q(COLOR_IS_RGB, color_to_vec(c)) + q(COLOR_IS_INDEX, color_to_vec(color_table[c & BYTE_MASK])) +
|
||||||
q(COLOR_IS_SPECIAL, special_val) + q(COLOR_NOT_SET, defval)
|
q(COLOR_IS_SPECIAL, special_val) + q(COLOR_NOT_SET, defval)
|
||||||
@@ -185,7 +189,7 @@ uvec2 get_decorations_indices(uint in_url /* [0, 1] */, uint text_attrs) {
|
|||||||
return uvec2(strike_idx, has_underline * (decorations_idx + underline_style));
|
return uvec2(strike_idx, has_underline * (decorations_idx + underline_style));
|
||||||
}
|
}
|
||||||
|
|
||||||
float is_cursor(uint x, uint y) {
|
uint is_cursor(uint x, uint y) {
|
||||||
uint clamped_x = clamp(x, cursor_x1, cursor_x2);
|
uint clamped_x = clamp(x, cursor_x1, cursor_x2);
|
||||||
uint clamped_y = clamp(y, cursor_y1, cursor_y2);
|
uint clamped_y = clamp(y, cursor_y1, cursor_y2);
|
||||||
return one_if_equal_zero_otherwise(x, clamped_x) * one_if_equal_zero_otherwise(y, clamped_y);
|
return one_if_equal_zero_otherwise(x, clamped_x) * one_if_equal_zero_otherwise(y, clamped_y);
|
||||||
@@ -217,7 +221,7 @@ CellData set_vertex_position(vec3 cell_fg, vec3 cell_bg) {
|
|||||||
colored_sprite = float((sprite_idx[0] & SPRITE_COLORED_MASK) >> SPRITE_COLORED_SHIFT);
|
colored_sprite = float((sprite_idx[0] & SPRITE_COLORED_MASK) >> SPRITE_COLORED_SHIFT);
|
||||||
#endif
|
#endif
|
||||||
// Cursor shape and colors
|
// Cursor shape and colors
|
||||||
float has_main_cursor = is_cursor(column, row);
|
float has_main_cursor = float(is_cursor(column, row));
|
||||||
float multicursor_shape = float((is_selected >> 2) & 3u);
|
float multicursor_shape = float((is_selected >> 2) & 3u);
|
||||||
float multicursor_uses_main_cursor_shape = float((is_selected >> 4) & BIT_MASK);
|
float multicursor_uses_main_cursor_shape = float((is_selected >> 4) & BIT_MASK);
|
||||||
multicursor_shape = if_one_then(multicursor_uses_main_cursor_shape, cursor_shape, multicursor_shape);
|
multicursor_shape = if_one_then(multicursor_uses_main_cursor_shape, cursor_shape, multicursor_shape);
|
||||||
|
|||||||
@@ -491,7 +491,7 @@ def go_type_data(parser_func: ParserFuncType, ctype: str, is_multiple: bool = Fa
|
|||||||
if p == 'positive_int':
|
if p == 'positive_int':
|
||||||
return 'uint64', 'strconv.ParseUint(val, 10, 64)'
|
return 'uint64', 'strconv.ParseUint(val, 10, 64)'
|
||||||
if p == 'positive_float':
|
if p == 'positive_float':
|
||||||
return 'float64', 'config.PositiveFloat(val, 10, 64)'
|
return 'float64', 'config.PositiveFloat(val)'
|
||||||
if p == 'unit_float':
|
if p == 'unit_float':
|
||||||
return 'float64', 'config.UnitFloat(val)'
|
return 'float64', 'config.UnitFloat(val)'
|
||||||
if p == 'python_string':
|
if p == 'python_string':
|
||||||
|
|||||||
@@ -28,5 +28,9 @@ static inline void* disk_cache_malloc_allocator(void *x, size_t sz) {
|
|||||||
static inline bool
|
static inline bool
|
||||||
read_from_disk_cache_simple(PyObject *self_, const void *key, size_t key_sz, void **data, size_t *data_sz, bool store_in_ram) {
|
read_from_disk_cache_simple(PyObject *self_, const void *key, size_t key_sz, void **data, size_t *data_sz, bool store_in_ram) {
|
||||||
*data = read_from_disk_cache(self_, key, key_sz, disk_cache_malloc_allocator, data_sz, store_in_ram);
|
*data = read_from_disk_cache(self_, key, key_sz, disk_cache_malloc_allocator, data_sz, store_in_ram);
|
||||||
return PyErr_Occurred() == NULL;
|
if (PyErr_Occurred()) {
|
||||||
|
PyErr_Clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return *data != NULL;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ gl_init(void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static const char*
|
const char*
|
||||||
check_framebuffer_status(void) {
|
check_framebuffer_status(void) {
|
||||||
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -100,12 +100,6 @@ check_framebuffer_status(void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
|
||||||
check_framebuffer_status_or_die(void) {
|
|
||||||
const char *err = check_framebuffer_status();
|
|
||||||
if (err != NULL) fatal("Framebuffer not complete with error: %s", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
free_texture(GLuint *tex_id) {
|
free_texture(GLuint *tex_id) {
|
||||||
glDeleteTextures(1, tex_id);
|
glDeleteTextures(1, tex_id);
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ void unbind_program(void);
|
|||||||
GLuint compile_shaders(GLenum shader_type, GLsizei count, const GLchar * const * string);
|
GLuint compile_shaders(GLenum shader_type, GLsizei count, const GLchar * const * string);
|
||||||
void save_viewport_using_top_left_origin(GLsizei x, GLsizei y, GLsizei width, GLsizei height, GLsizei full_framebuffer_height);
|
void save_viewport_using_top_left_origin(GLsizei x, GLsizei y, GLsizei width, GLsizei height, GLsizei full_framebuffer_height);
|
||||||
void save_viewport_using_bottom_left_origin(GLsizei x, GLsizei y, GLsizei width, GLsizei height);
|
void save_viewport_using_bottom_left_origin(GLsizei x, GLsizei y, GLsizei width, GLsizei height);
|
||||||
void check_framebuffer_status_or_die(void);
|
const char* check_framebuffer_status(void);
|
||||||
void restore_viewport(void);
|
void restore_viewport(void);
|
||||||
void bind_framebuffer_for_output(unsigned fbid);
|
void bind_framebuffer_for_output(unsigned fbid);
|
||||||
void set_framebuffer_to_use_for_output(unsigned fbid);
|
void set_framebuffer_to_use_for_output(unsigned fbid);
|
||||||
|
|||||||
@@ -511,24 +511,21 @@ class Splits(Layout):
|
|||||||
|
|
||||||
def do_layout(self, all_windows: WindowList) -> None:
|
def do_layout(self, all_windows: WindowList) -> None:
|
||||||
groups = tuple(all_windows.iter_all_layoutable_groups())
|
groups = tuple(all_windows.iter_all_layoutable_groups())
|
||||||
window_count = len(groups)
|
|
||||||
root = self.pairs_root
|
root = self.pairs_root
|
||||||
all_present_window_ids = frozenset(w.id for w in groups)
|
all_present_group_ids = {g.id for g in groups}
|
||||||
already_placed_window_ids = frozenset(root.all_window_ids())
|
already_placed_group_ids = frozenset(root.all_window_ids())
|
||||||
windows_to_remove = already_placed_window_ids - all_present_window_ids
|
if groups_to_remove := already_placed_group_ids - all_present_group_ids:
|
||||||
if windows_to_remove:
|
self.remove_windows(*groups_to_remove)
|
||||||
self.remove_windows(*windows_to_remove)
|
if groups_to_add := all_present_group_ids - already_placed_group_ids:
|
||||||
id_window_map = {w.id: w for w in groups}
|
id_idx_map = {g.id: i for i, g in enumerate(groups)}
|
||||||
id_idx_map = {w.id: i for i, w in enumerate(groups)}
|
for gid in sorted(groups_to_add, key=id_idx_map.__getitem__):
|
||||||
windows_to_add = all_present_window_ids - already_placed_window_ids
|
root.balanced_add(gid)
|
||||||
if windows_to_add:
|
|
||||||
for wid in sorted(windows_to_add, key=id_idx_map.__getitem__):
|
|
||||||
root.balanced_add(wid)
|
|
||||||
|
|
||||||
if window_count == 1:
|
if len(groups) == 1:
|
||||||
self.layout_single_window_group(groups[0])
|
self.layout_single_window_group(groups[0])
|
||||||
else:
|
else:
|
||||||
root.layout_pair(lgd.central.left, lgd.central.top, lgd.central.width, lgd.central.height, id_window_map, self)
|
id_group_map = {g.id: g for g in groups}
|
||||||
|
root.layout_pair(lgd.central.left, lgd.central.top, lgd.central.width, lgd.central.height, id_group_map, self)
|
||||||
|
|
||||||
def add_non_overlay_window(
|
def add_non_overlay_window(
|
||||||
self,
|
self,
|
||||||
@@ -563,7 +560,9 @@ class Splits(Layout):
|
|||||||
parent_pair.bias = bias if parent_pair.one == target_group.id else (1 - bias)
|
parent_pair.bias = bias if parent_pair.one == target_group.id else (1 - bias)
|
||||||
return
|
return
|
||||||
all_windows.add_window(window)
|
all_windows.add_window(window)
|
||||||
p = self.pairs_root.balanced_add(window.id)
|
g = all_windows.group_for_window(window)
|
||||||
|
assert g is not None
|
||||||
|
p = self.pairs_root.balanced_add(g.id)
|
||||||
if bias is not None:
|
if bias is not None:
|
||||||
p.bias = bias
|
p.bias = bias
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ from .utils import (
|
|||||||
get_custom_window_icon,
|
get_custom_window_icon,
|
||||||
log_error,
|
log_error,
|
||||||
parse_os_window_state,
|
parse_os_window_state,
|
||||||
|
read_shell_environment,
|
||||||
safe_mtime,
|
safe_mtime,
|
||||||
startup_notification_handler,
|
startup_notification_handler,
|
||||||
)
|
)
|
||||||
@@ -478,6 +479,17 @@ def setup_environment(opts: Options, cli_opts: CLIOptions) -> None:
|
|||||||
from_config_file = True
|
from_config_file = True
|
||||||
if cli_opts.listen_on:
|
if cli_opts.listen_on:
|
||||||
cli_opts.listen_on = expand_listen_on(cli_opts.listen_on, from_config_file)
|
cli_opts.listen_on = expand_listen_on(cli_opts.listen_on, from_config_file)
|
||||||
|
if vars := opts.env.pop('read_from_shell', ''):
|
||||||
|
import fnmatch
|
||||||
|
import re
|
||||||
|
senv = read_shell_environment(opts)
|
||||||
|
patterns = tuple(re.compile(fnmatch.translate(x.strip())) for x in vars.split() if x.strip())
|
||||||
|
if patterns:
|
||||||
|
for k, v in senv.items():
|
||||||
|
for pat in patterns:
|
||||||
|
if pat.match(k) is not None:
|
||||||
|
opts.env[k] = v
|
||||||
|
break
|
||||||
env = opts.env.copy()
|
env = opts.env.copy()
|
||||||
ensure_kitty_in_path()
|
ensure_kitty_in_path()
|
||||||
ensure_kitten_in_path()
|
ensure_kitten_in_path()
|
||||||
|
|||||||
@@ -3254,7 +3254,9 @@ Changing this option by reloading the config is not supported.
|
|||||||
'''
|
'''
|
||||||
)
|
)
|
||||||
|
|
||||||
opt('+env', '',
|
opt(
|
||||||
|
'+env',
|
||||||
|
'',
|
||||||
option_type='env',
|
option_type='env',
|
||||||
add_to_default=False,
|
add_to_default=False,
|
||||||
long_text='''
|
long_text='''
|
||||||
@@ -3268,8 +3270,17 @@ recursively, for example::
|
|||||||
env VAR2=${HOME}/${VAR1}/b
|
env VAR2=${HOME}/${VAR1}/b
|
||||||
|
|
||||||
The value of :code:`VAR2` will be :code:`<path to home directory>/a/b`.
|
The value of :code:`VAR2` will be :code:`<path to home directory>/a/b`.
|
||||||
'''
|
|
||||||
)
|
Use the special
|
||||||
|
value :code:`read_from_shell` to have kitty read the specified variables from
|
||||||
|
your :opt:`login shell <shell>` configuration.
|
||||||
|
Useful if your shell startup files setup a bunch of environment variables that you want available to kitty and
|
||||||
|
in kitty session files. Each variable name is treated as a glob pattern to match. For example:
|
||||||
|
:code:`env read_from_shell=PATH LANG LC_* XDG_* EDITOR VISUAL`. Note that these variables are only
|
||||||
|
read after the configuration is fully processed, thus they are not available for recursive expansion and
|
||||||
|
they will override any variables set by other :opt:`env` directives.
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
|
||||||
opt('+filter_notification', '', option_type='filter_notification', add_to_default=False, long_text='''
|
opt('+filter_notification', '', option_type='filter_notification', add_to_default=False, long_text='''
|
||||||
Specify rules to filter out notifications sent by applications running in kitty.
|
Specify rules to filter out notifications sent by applications running in kitty.
|
||||||
|
|||||||
@@ -82,8 +82,7 @@ CmdGenerator = Iterator[CmdReturnType]
|
|||||||
PayloadType = Optional[Union[CmdReturnType, CmdGenerator]]
|
PayloadType = Optional[Union[CmdReturnType, CmdGenerator]]
|
||||||
PayloadGetType = PayloadGetter
|
PayloadGetType = PayloadGetter
|
||||||
ArgsType = list[str]
|
ArgsType = list[str]
|
||||||
ImageCompletion = CompletionSpec.from_string('type:file group:"Images"')._replace(
|
ImageCompletion = CompletionSpec.from_string('type:file group:"Images" ext:png,jpg,jpeg,webp,gif,bmp,tiff')
|
||||||
extensions=('png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'tiff'))
|
|
||||||
SUPPORTED_IMAGE_FORMATS = tuple(x.upper() for x in ImageCompletion.extensions if x != 'jpg')
|
SUPPORTED_IMAGE_FORMATS = tuple(x.upper() for x in ImageCompletion.extensions if x != 'jpg')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
import sys
|
import sys
|
||||||
from collections.abc import Callable, Generator, Iterator, Mapping
|
from collections.abc import Callable, Generator, Iterator, Mapping
|
||||||
@@ -64,7 +65,7 @@ class Tab:
|
|||||||
self.pending_resize_spec: ResizeSpec | None = None
|
self.pending_resize_spec: ResizeSpec | None = None
|
||||||
self.pending_focus_matching_window: str = ''
|
self.pending_focus_matching_window: str = ''
|
||||||
self.name = name.strip()
|
self.name = name.strip()
|
||||||
self.active_window_idx = 0
|
self.active_window_idx = -1
|
||||||
self.enabled_layouts = opts.enabled_layouts
|
self.enabled_layouts = opts.enabled_layouts
|
||||||
self.layout = (self.enabled_layouts or ['tall'])[0]
|
self.layout = (self.enabled_layouts or ['tall'])[0]
|
||||||
self.layout_state: dict[str, Any] | None = None
|
self.layout_state: dict[str, Any] | None = None
|
||||||
@@ -518,7 +519,6 @@ def parse_goto_session_cmdline(args: list[str]) -> tuple[GotoSessionOptions, lis
|
|||||||
return ans, leftover_args
|
return ans, leftover_args
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def goto_session_options() -> str:
|
def goto_session_options() -> str:
|
||||||
return '''
|
return '''
|
||||||
--sort-by
|
--sort-by
|
||||||
@@ -528,7 +528,23 @@ When interactively choosing sessions from a list, how to sort the list.
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def goto_previous_session(boss: BossType, idx: int) -> None:
|
||||||
|
if boss.active_session:
|
||||||
|
nidx = max(0, len(goto_session_history) - 1 + idx)
|
||||||
|
if nidx < len(goto_session_history):
|
||||||
|
switch_to_session(boss, goto_session_history[nidx])
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
if goto_session_history:
|
||||||
|
switch_to_session(boss, goto_session_history[-1])
|
||||||
|
return
|
||||||
|
boss.ring_bell_if_allowed()
|
||||||
|
|
||||||
|
|
||||||
def goto_session(boss: BossType, cmdline: Sequence[str]) -> None:
|
def goto_session(boss: BossType, cmdline: Sequence[str]) -> None:
|
||||||
|
if len(cmdline) == 1 and re.match(r'-\d+', cmdline[0]) is not None:
|
||||||
|
# special case for backwards compat goto_session -1
|
||||||
|
return goto_previous_session(boss, int(cmdline[0]))
|
||||||
try:
|
try:
|
||||||
opts, cmdline = parse_goto_session_cmdline(list(cmdline))
|
opts, cmdline = parse_goto_session_cmdline(list(cmdline))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -539,28 +555,13 @@ def goto_session(boss: BossType, cmdline: Sequence[str]) -> None:
|
|||||||
choose_session(boss, opts)
|
choose_session(boss, opts)
|
||||||
return
|
return
|
||||||
path = cmdline[0]
|
path = cmdline[0]
|
||||||
if len(cmdline) == 1:
|
if len(cmdline) == 1: # goto_session -- -1
|
||||||
try:
|
try:
|
||||||
idx = int(path)
|
idx = int(path)
|
||||||
except Exception:
|
except Exception:
|
||||||
idx = 0
|
idx = 0
|
||||||
if idx < 0:
|
if idx < 0:
|
||||||
if boss.active_session:
|
return goto_previous_session(boss, idx)
|
||||||
nidx = max(0, len(goto_session_history) - 1 + idx)
|
|
||||||
if nidx < len(goto_session_history):
|
|
||||||
switch_to_session(boss, goto_session_history[nidx])
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
if goto_session_history:
|
|
||||||
switch_to_session(boss, goto_session_history[-1])
|
|
||||||
return
|
|
||||||
boss.ring_bell_if_allowed()
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
for x in cmdline:
|
|
||||||
if not x.startswith('-'):
|
|
||||||
path = x
|
|
||||||
break
|
|
||||||
path, session_name = resolve_session_path_and_name(path)
|
path, session_name = resolve_session_path_and_name(path)
|
||||||
if not session_name:
|
if not session_name:
|
||||||
boss.show_error(_('Invalid session'), _('{} is not a valid path for a session').format(path))
|
boss.show_error(_('Invalid session'), _('{} is not a valid path for a session').format(path))
|
||||||
|
|||||||
@@ -656,9 +656,29 @@ setup_texture_as_render_target(unsigned width, unsigned height, GLuint *texture_
|
|||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
// We use GL_RGBA16 to avoid incorrect colors due to quantization loss when
|
// We use GL_RGBA16 to avoid incorrect colors due to quantization loss when
|
||||||
// blending, see https://github.com/kovidgoyal/kitty/issues/8953
|
// blending, see https://github.com/kovidgoyal/kitty/issues/8953
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
|
static struct { bool ok; int fmt; } status = { false, GL_RGBA16};
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, status.fmt, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
|
||||||
bind_framebuffer_for_output(*framebuffer_id);
|
bind_framebuffer_for_output(*framebuffer_id);
|
||||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, *texture_id, 0);
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, *texture_id, 0);
|
||||||
|
if (!status.ok) {
|
||||||
|
if (check_framebuffer_status() == NULL) {
|
||||||
|
status.ok = true;
|
||||||
|
} else {
|
||||||
|
if (status.fmt == GL_RGBA16) {
|
||||||
|
// Driver does not support 16 bit FBO so let it choose the
|
||||||
|
// format. It will probably end up choosing 8 bit but
|
||||||
|
// inaccurate colors are better than completely broken rendering.
|
||||||
|
// See https://github.com/kovidgoyal/kitty/issues/9068
|
||||||
|
status.fmt = GL_RGBA;
|
||||||
|
free_framebuffer(framebuffer_id);
|
||||||
|
free_texture(texture_id);
|
||||||
|
setup_texture_as_render_target(width, height, texture_id, framebuffer_id);
|
||||||
|
log_error("WARNING: Your GPU driver does not support 16bit textures as framebuffer targets, some colors may be slightly inaccurate.");
|
||||||
|
} else {
|
||||||
|
fatal("Your GPU driver does not support indirect rendering to a GL_RGBA texture via a framebuffer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
@@ -1241,14 +1261,13 @@ draw_cursor_trail(CursorTrail *trail, Window *active_window) {
|
|||||||
|
|
||||||
// OSWindow {{{
|
// OSWindow {{{
|
||||||
static void
|
static void
|
||||||
draw_bg_image(OSWindow *os_window) {
|
draw_bg_image(OSWindow *os_window, Tab *tab) {
|
||||||
if (!has_bgimage(os_window)) return;
|
if (!has_bgimage(os_window)) return;
|
||||||
BackgroundImageRenderSettings s = {
|
BackgroundImageRenderSettings s = {
|
||||||
.os_window.width = os_window->viewport_width, .os_window.height = os_window->viewport_height,
|
.os_window.width = os_window->viewport_width, .os_window.height = os_window->viewport_height,
|
||||||
.instance_id = os_window->bgimage->id, .layout=OPT(background_image_layout),
|
.instance_id = os_window->bgimage->id, .layout=OPT(background_image_layout),
|
||||||
.linear=OPT(background_image_linear), .bgcolor=OPT(background), .opacity=effective_os_window_alpha(os_window),
|
.linear=OPT(background_image_linear), .bgcolor=OPT(background), .opacity=effective_os_window_alpha(os_window),
|
||||||
};
|
};
|
||||||
bind_program(BGIMAGE_PROGRAM);
|
|
||||||
GLfloat iwidth = os_window->bgimage->width, iheight = os_window->bgimage->height;
|
GLfloat iwidth = os_window->bgimage->width, iheight = os_window->bgimage->height;
|
||||||
GLfloat vwidth = s.os_window.width, vheight = s.os_window.height;
|
GLfloat vwidth = s.os_window.width, vheight = s.os_window.height;
|
||||||
if (CENTER_SCALED == OPT(background_image_layout)) {
|
if (CENTER_SCALED == OPT(background_image_layout)) {
|
||||||
@@ -1278,6 +1297,9 @@ draw_bg_image(OSWindow *os_window) {
|
|||||||
bottom += hfrac;
|
bottom += hfrac;
|
||||||
} break;
|
} break;
|
||||||
}
|
}
|
||||||
|
bind_program(BGIMAGE_PROGRAM);
|
||||||
|
// altough we dont use this VO we need to ensure *some* VAO is bound at this point.
|
||||||
|
bind_vertex_array(tab->border_rects.vao_idx);
|
||||||
glUniform4f(bgimage_program_layout.uniforms.sizes, vwidth, vheight, iwidth, iheight);
|
glUniform4f(bgimage_program_layout.uniforms.sizes, vwidth, vheight, iwidth, iheight);
|
||||||
glUniform1f(bgimage_program_layout.uniforms.tiled, tiled);
|
glUniform1f(bgimage_program_layout.uniforms.tiled, tiled);
|
||||||
glUniform4f(bgimage_program_layout.uniforms.positions, left, top, right, bottom);
|
glUniform4f(bgimage_program_layout.uniforms.positions, left, top, right, bottom);
|
||||||
@@ -1339,7 +1361,7 @@ blank_os_window(OSWindow *osw) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
start_os_window_rendering(OSWindow *os_window) {
|
start_os_window_rendering(OSWindow *os_window, Tab *tab) {
|
||||||
if (os_window->live_resize.in_progress) {
|
if (os_window->live_resize.in_progress) {
|
||||||
blank_os_window(os_window);
|
blank_os_window(os_window);
|
||||||
save_viewport_using_bottom_left_origin(0, 0, os_window->viewport_width, os_window->viewport_height);
|
save_viewport_using_bottom_left_origin(0, 0, os_window->viewport_width, os_window->viewport_height);
|
||||||
@@ -1357,7 +1379,7 @@ start_os_window_rendering(OSWindow *os_window) {
|
|||||||
set_framebuffer_to_use_for_output(os_window->indirect_output.framebuffer_id);
|
set_framebuffer_to_use_for_output(os_window->indirect_output.framebuffer_id);
|
||||||
bind_framebuffer_for_output(0);
|
bind_framebuffer_for_output(0);
|
||||||
clear_current_framebuffer();
|
clear_current_framebuffer();
|
||||||
draw_bg_image(os_window);
|
draw_bg_image(os_window, tab);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1382,7 +1404,7 @@ stop_os_window_rendering(OSWindow *os_window, Tab *tab, Window *active_window) {
|
|||||||
|
|
||||||
void
|
void
|
||||||
setup_os_window_for_rendering(OSWindow *os_window, Tab *tab, Window *active_window, bool start) {
|
setup_os_window_for_rendering(OSWindow *os_window, Tab *tab, Window *active_window, bool start) {
|
||||||
if (start) start_os_window_rendering(os_window);
|
if (start) start_os_window_rendering(os_window, tab);
|
||||||
else stop_os_window_rendering(os_window, tab, active_window);
|
else stop_os_window_rendering(os_window, tab, active_window);
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
|
|||||||
@@ -251,10 +251,14 @@ class Tab: # {{{
|
|||||||
def _startup(self, session_tab: SessionTab) -> None:
|
def _startup(self, session_tab: SessionTab) -> None:
|
||||||
target_tab = self
|
target_tab = self
|
||||||
boss = get_boss()
|
boss = get_boss()
|
||||||
for window in session_tab.windows:
|
active_window_id = 0
|
||||||
|
did_focus_matching_spec = False
|
||||||
|
first_window_id = 0
|
||||||
|
for i, window in enumerate(session_tab.windows):
|
||||||
spec = window.launch_spec
|
spec = window.launch_spec
|
||||||
|
launched_window: Window | None = None
|
||||||
if isinstance(spec, SpecialWindowInstance):
|
if isinstance(spec, SpecialWindowInstance):
|
||||||
self.new_special_window(spec)
|
launched_window = self.new_special_window(spec)
|
||||||
else:
|
else:
|
||||||
from .launch import launch
|
from .launch import launch
|
||||||
spec.opts.add_to_session = self.created_in_session_name
|
spec.opts.add_to_session = self.created_in_session_name
|
||||||
@@ -263,6 +267,12 @@ class Tab: # {{{
|
|||||||
startup_command_via_shell_integration=window.run_command_at_shell_startup)
|
startup_command_via_shell_integration=window.run_command_at_shell_startup)
|
||||||
if launched_window is not None:
|
if launched_window is not None:
|
||||||
launched_window.serialized_id = window.serialized_id
|
launched_window.serialized_id = window.serialized_id
|
||||||
|
if launched_window is not None:
|
||||||
|
if not first_window_id:
|
||||||
|
first_window_id = launched_window.id
|
||||||
|
if session_tab.active_window_idx == i:
|
||||||
|
active_window_id = launched_window.id
|
||||||
|
did_focus_matching_spec = False
|
||||||
if window.resize_spec is not None:
|
if window.resize_spec is not None:
|
||||||
self.resize_window(*window.resize_spec)
|
self.resize_window(*window.resize_spec)
|
||||||
if window.focus_matching_window_spec:
|
if window.focus_matching_window_spec:
|
||||||
@@ -275,6 +285,8 @@ class Tab: # {{{
|
|||||||
):
|
):
|
||||||
tab = w.tabref()
|
tab = w.tabref()
|
||||||
if tab:
|
if tab:
|
||||||
|
did_focus_matching_spec = True
|
||||||
|
active_window_id = 0
|
||||||
target_tab = tab or self
|
target_tab = tab or self
|
||||||
tm = tab.tab_manager_ref()
|
tm = tab.tab_manager_ref()
|
||||||
if tm and boss.active_tab is not target_tab:
|
if tm and boss.active_tab is not target_tab:
|
||||||
@@ -283,8 +295,10 @@ class Tab: # {{{
|
|||||||
target_tab.set_active_window(w)
|
target_tab.set_active_window(w)
|
||||||
boss.focus_os_window(w.os_window_id)
|
boss.focus_os_window(w.os_window_id)
|
||||||
|
|
||||||
with suppress(IndexError):
|
if not did_focus_matching_spec and not active_window_id:
|
||||||
self.windows.set_active_window_group_for(self.windows.all_windows[session_tab.active_window_idx])
|
active_window_id = first_window_id
|
||||||
|
if active_window_id and not did_focus_matching_spec:
|
||||||
|
self.windows.set_active_window_group_for(active_window_id)
|
||||||
if session_tab.layout_state:
|
if session_tab.layout_state:
|
||||||
self.current_layout.unserialize(session_tab.layout_state, self.windows)
|
self.current_layout.unserialize(session_tab.layout_state, self.windows)
|
||||||
|
|
||||||
@@ -1374,12 +1388,21 @@ class TabManager: # {{{
|
|||||||
while self.active_tab_history and self.active_tab_history[-1] == tab.id:
|
while self.active_tab_history and self.active_tab_history[-1] == tab.id:
|
||||||
self.active_tab_history.pop()
|
self.active_tab_history.pop()
|
||||||
|
|
||||||
|
def previous_active_tab() -> Tab | None:
|
||||||
|
while self.active_tab_history:
|
||||||
|
tab_id = self.active_tab_history.pop()
|
||||||
|
if tab_id != removed_tab.id:
|
||||||
|
if (ans := self.tab_for_id(tab_id)) is not None:
|
||||||
|
return ans
|
||||||
|
return self.tabs[0] if self.tabs else None
|
||||||
|
|
||||||
if active_tab_before_removal is removed_tab:
|
if active_tab_before_removal is removed_tab:
|
||||||
if len(self.tabs) == 0:
|
if len(tabs) == 0 or (len(tabs) == 1 and removed_tab is tabs[0]):
|
||||||
self._active_tab_idx = 0
|
tab = previous_active_tab()
|
||||||
elif len(self.tabs) == 1:
|
if tab is None:
|
||||||
remove_from_end_of_active_history(self.tabs[0])
|
self._active_tab_idx = 0
|
||||||
self._set_active_tab(0, store_in_history=False)
|
else:
|
||||||
|
self._set_active_tab(self.tabs.index(tab), store_in_history=False)
|
||||||
else:
|
else:
|
||||||
next_active_tab: Tab | None = None
|
next_active_tab: Tab | None = None
|
||||||
match get_options().tab_switch_strategy:
|
match get_options().tab_switch_strategy:
|
||||||
@@ -1387,6 +1410,8 @@ class TabManager: # {{{
|
|||||||
while self.active_tab_history and next_active_tab is None:
|
while self.active_tab_history and next_active_tab is None:
|
||||||
tab_id = self.active_tab_history.pop()
|
tab_id = self.active_tab_history.pop()
|
||||||
next_active_tab = self.tab_for_id(tab_id)
|
next_active_tab = self.tab_for_id(tab_id)
|
||||||
|
if next_active_tab not in tabs:
|
||||||
|
next_active_tab = None
|
||||||
case 'left':
|
case 'left':
|
||||||
next_active_tab = tabs[(tabs.index(active_tab_before_removal) - 1 + len(tabs)) % len(tabs)]
|
next_active_tab = tabs[(tabs.index(active_tab_before_removal) - 1 + len(tabs)) % len(tabs)]
|
||||||
remove_from_end_of_active_history(next_active_tab)
|
remove_from_end_of_active_history(next_active_tab)
|
||||||
|
|||||||
126
kitty/utils.py
126
kitty/utils.py
@@ -11,11 +11,13 @@ from collections.abc import Callable, Generator, Iterable, Iterator, Mapping, Se
|
|||||||
from contextlib import contextmanager, suppress
|
from contextlib import contextmanager, suppress
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from re import Match, Pattern
|
from re import Match, Pattern
|
||||||
|
from types import MappingProxyType
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
BinaryIO,
|
BinaryIO,
|
||||||
NamedTuple,
|
NamedTuple,
|
||||||
|
NoReturn,
|
||||||
Optional,
|
Optional,
|
||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
@@ -540,7 +542,8 @@ def get_editor_from_env(env: Mapping[str, str]) -> str | None:
|
|||||||
|
|
||||||
|
|
||||||
def get_editor_from_env_vars(opts: Options | None = None) -> list[str]:
|
def get_editor_from_env_vars(opts: Options | None = None) -> list[str]:
|
||||||
editor = get_editor_from_env(os.environ)
|
from .child import default_env
|
||||||
|
editor = get_editor_from_env(default_env())
|
||||||
if not editor:
|
if not editor:
|
||||||
shell_env = read_shell_environment(opts)
|
shell_env = read_shell_environment(opts)
|
||||||
editor = get_editor_from_env(shell_env)
|
editor = get_editor_from_env(shell_env)
|
||||||
@@ -578,6 +581,15 @@ def get_editor(opts: Options | None = None, path_to_edit: str = '', line_number:
|
|||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|
||||||
|
def edit_file(path: str = '') -> NoReturn:
|
||||||
|
' This exists for: map whatever launch kitty +runpy "from kitty.utils import *; edit_file()" to edit kitty config '
|
||||||
|
from .config import prepare_config_file_for_editing
|
||||||
|
editor = get_editor()
|
||||||
|
path = path or prepare_config_file_for_editing()
|
||||||
|
editor.append(path)
|
||||||
|
os.execlp(editor[0], *editor)
|
||||||
|
|
||||||
|
|
||||||
def is_path_in_temp_dir(path: str) -> bool:
|
def is_path_in_temp_dir(path: str) -> bool:
|
||||||
if not path:
|
if not path:
|
||||||
return False
|
return False
|
||||||
@@ -743,62 +755,72 @@ def which(name: str, only_system: bool = False) -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def read_shell_environment(opts: Options | None = None) -> dict[str, str]:
|
@lru_cache(4)
|
||||||
ans: dict[str, str] | None = getattr(read_shell_environment, 'ans', None)
|
def read_resolved_shell_environment(shell: tuple[str, ...]) -> MappingProxyType[str, str]:
|
||||||
if ans is None:
|
import subprocess
|
||||||
from .child import openpty
|
cmdline = list(shell)
|
||||||
ans = {}
|
if '-l' not in cmdline and '--login' not in cmdline:
|
||||||
setattr(read_shell_environment, 'ans', ans)
|
cmdline += ['-l']
|
||||||
import subprocess
|
if '-i' not in cmdline and '--interactive' not in cmdline:
|
||||||
shell = resolved_shell(opts)
|
cmdline += ['-i']
|
||||||
master, slave = openpty()
|
q = os.path.basename(cmdline[0]).lower()
|
||||||
os.set_blocking(master, False)
|
has_builtin = q in ('bash', 'zsh')
|
||||||
if '-l' not in shell and '--login' not in shell:
|
cmd = 'builtin command env -0' if has_builtin else 'command env -0'
|
||||||
shell += ['-l']
|
ans: MappingProxyType[str, str] = MappingProxyType({})
|
||||||
if '-i' not in shell and '--interactive' not in shell:
|
|
||||||
shell += ['-i']
|
from .child import openpty
|
||||||
try:
|
master, slave = openpty()
|
||||||
p = subprocess.Popen(
|
os.set_blocking(master, False)
|
||||||
shell + ['-c', 'env'], stdout=slave, stdin=slave, stderr=slave, start_new_session=True, close_fds=True,
|
try:
|
||||||
preexec_fn=clear_handled_signals)
|
p = subprocess.Popen(
|
||||||
except FileNotFoundError:
|
cmdline + ['-c', cmd], stdout=slave, stdin=slave, stderr=slave, start_new_session=True, close_fds=True,
|
||||||
log_error('Could not find shell to read environment')
|
preexec_fn=clear_handled_signals)
|
||||||
return ans
|
except FileNotFoundError:
|
||||||
with os.fdopen(master, 'rb') as stdout, os.fdopen(slave, 'wb'):
|
log_error(f'Could not find shell {cmdline[0]} to read environment')
|
||||||
raw = b''
|
return ans
|
||||||
from time import monotonic
|
with os.fdopen(master, 'rb') as stdout, os.fdopen(slave, 'wb'):
|
||||||
start_time = monotonic()
|
raw = b''
|
||||||
while monotonic() - start_time < 1.5:
|
from time import monotonic
|
||||||
|
start_time = monotonic()
|
||||||
|
ret: int | None = None
|
||||||
|
while monotonic() - start_time < 1.5:
|
||||||
|
try:
|
||||||
|
ret = p.wait(0.01)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
ret = None
|
||||||
|
with suppress(Exception):
|
||||||
|
raw += stdout.read()
|
||||||
|
if ret is not None:
|
||||||
|
break
|
||||||
|
if ret is None:
|
||||||
|
log_error(f'Timed out waiting for shell {cmdline} to quit while reading shell environment')
|
||||||
|
p.kill()
|
||||||
|
elif ret == 0:
|
||||||
|
while True:
|
||||||
try:
|
try:
|
||||||
ret: int | None = p.wait(0.01)
|
x = stdout.read()
|
||||||
except subprocess.TimeoutExpired:
|
except Exception:
|
||||||
ret = None
|
|
||||||
with suppress(Exception):
|
|
||||||
raw += stdout.read()
|
|
||||||
if ret is not None:
|
|
||||||
break
|
break
|
||||||
if cast(Optional[int], p.returncode) is None:
|
if not x:
|
||||||
log_error('Timed out waiting for shell to quit while reading shell environment')
|
break
|
||||||
p.kill()
|
raw += x
|
||||||
elif p.returncode == 0:
|
draw = raw.decode('utf-8', 'replace')
|
||||||
while True:
|
env = {}
|
||||||
try:
|
for line in draw.split('\0'):
|
||||||
x = stdout.read()
|
k, sep, v = line.partition('=')
|
||||||
except Exception:
|
if k and v and sep:
|
||||||
break
|
env[k] = v
|
||||||
if not x:
|
ans = MappingProxyType(env)
|
||||||
break
|
else:
|
||||||
raw += x
|
log_error(f'Failed to run shell {cmdline} to read its environment')
|
||||||
draw = raw.decode('utf-8', 'replace')
|
|
||||||
for line in draw.splitlines():
|
|
||||||
k, v = line.partition('=')[::2]
|
|
||||||
if k and v:
|
|
||||||
ans[k] = v
|
|
||||||
else:
|
|
||||||
log_error('Failed to run shell to read its environment')
|
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|
||||||
|
def read_shell_environment(opts: Options | None = None) -> MappingProxyType[str, str]:
|
||||||
|
shell = resolved_shell(opts)
|
||||||
|
return read_resolved_shell_environment(tuple(shell))
|
||||||
|
|
||||||
|
|
||||||
def parse_uri_list(text: str) -> Generator[str, None, None]:
|
def parse_uri_list(text: str) -> Generator[str, None, None]:
|
||||||
' Get paths from file:// URLs '
|
' Get paths from file:// URLs '
|
||||||
from urllib.parse import unquote, urlparse
|
from urllib.parse import unquote, urlparse
|
||||||
|
|||||||
@@ -718,12 +718,12 @@ class Window:
|
|||||||
self.title_stack: Deque[str] = deque(maxlen=10)
|
self.title_stack: Deque[str] = deque(maxlen=10)
|
||||||
self.user_vars: dict[str, str] = {}
|
self.user_vars: dict[str, str] = {}
|
||||||
self.id: int = add_window(tab.os_window_id, tab.id, self.title)
|
self.id: int = add_window(tab.os_window_id, tab.id, self.title)
|
||||||
|
if not self.id:
|
||||||
|
raise Exception(f'No tab with id: {tab.id} in OS Window: {tab.os_window_id} was found, or the window counter wrapped')
|
||||||
self.clipboard_request_manager = ClipboardRequestManager(self.id)
|
self.clipboard_request_manager = ClipboardRequestManager(self.id)
|
||||||
self.margin = EdgeWidths()
|
self.margin = EdgeWidths()
|
||||||
self.padding = EdgeWidths()
|
self.padding = EdgeWidths()
|
||||||
self.kitten_result: dict[str, Any] | None = None
|
self.kitten_result: dict[str, Any] | None = None
|
||||||
if not self.id:
|
|
||||||
raise Exception(f'No tab with id: {tab.id} in OS Window: {tab.os_window_id} was found, or the window counter wrapped')
|
|
||||||
self.tab_id = tab.id
|
self.tab_id = tab.id
|
||||||
self.os_window_id = tab.os_window_id
|
self.os_window_id = tab.os_window_id
|
||||||
self.tabref: Callable[[], TabType | None] = weakref.ref(tab)
|
self.tabref: Callable[[], TabType | None] = weakref.ref(tab)
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ class WindowGroup:
|
|||||||
self.windows: list[WindowType] = []
|
self.windows: list[WindowType] = []
|
||||||
self.id = next(group_id_counter)
|
self.id = next(group_id_counter)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f'WindowGroup(id={self.id}, windows={", ".join(str(w.id) for w in self.windows)})'
|
||||||
|
|
||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
return len(self.windows)
|
return len(self.windows)
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type Entry struct {
|
|||||||
|
|
||||||
type Metadata struct {
|
type Metadata struct {
|
||||||
TotalSize int64
|
TotalSize int64
|
||||||
|
PathMap map[string]string
|
||||||
SortedEntries []*Entry
|
SortedEntries []*Entry
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,35 +25,55 @@ type DiskCache struct {
|
|||||||
Path string
|
Path string
|
||||||
MaxSize int64
|
MaxSize int64
|
||||||
|
|
||||||
lock_file *os.File
|
lock_file *os.File
|
||||||
lock_mutex sync.Mutex
|
lock_mutex sync.Mutex
|
||||||
entries Metadata
|
entries Metadata
|
||||||
entry_map map[string]*Entry
|
entry_map map[string]*Entry
|
||||||
entries_mod_time time.Time
|
entries_last_read_state *file_state
|
||||||
get_dir string
|
entries_dirty bool
|
||||||
|
get_dir string
|
||||||
|
read_count int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDiskCache(path string, max_size int64) (dc *DiskCache, err error) {
|
func NewDiskCache(path string, max_size int64) (dc *DiskCache, err error) {
|
||||||
return new_disk_cache(path, max_size)
|
return new_disk_cache(path, max_size)
|
||||||
}
|
}
|
||||||
|
|
||||||
func KeyForPath(path string) (key string, err error) {
|
func (dc *DiskCache) ResultsDir() string {
|
||||||
return key_for_path(path)
|
return dc.get_dir
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dc *DiskCache) Get(key string, items ...string) map[string]string {
|
func (dc *DiskCache) Get(key string, items ...string) (map[string]string, error) {
|
||||||
dc.lock()
|
dc.lock()
|
||||||
defer dc.unlock()
|
defer dc.unlock()
|
||||||
return dc.get(key, items)
|
return dc.get(key, items)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (dc *DiskCache) GetPath(path string, items ...string) (string, map[string]string, error) {
|
||||||
|
dc.lock()
|
||||||
|
defer dc.unlock()
|
||||||
|
key, err := key_for_path(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
ans, err := dc.get(key, items)
|
||||||
|
return key, ans, err
|
||||||
|
}
|
||||||
|
|
||||||
func (dc *DiskCache) Remove(key string) (err error) {
|
func (dc *DiskCache) Remove(key string) (err error) {
|
||||||
dc.lock()
|
dc.lock()
|
||||||
defer dc.unlock()
|
defer dc.unlock()
|
||||||
return dc.remove(key)
|
return dc.remove(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dc *DiskCache) Add(key string, items map[string][]byte) (err error) {
|
func (dc *DiskCache) AddPath(path, key string, items map[string][]byte) (ans map[string]string, err error) {
|
||||||
|
dc.lock()
|
||||||
|
defer dc.unlock()
|
||||||
|
ans, err = dc.add_path(path, key, items)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *DiskCache) Add(key string, items map[string][]byte) (ans map[string]string, err error) {
|
||||||
dc.lock()
|
dc.lock()
|
||||||
defer dc.unlock()
|
defer dc.unlock()
|
||||||
return dc.add(key, items)
|
return dc.add(key, items)
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"maps"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kovidgoyal/kitty/tools/utils"
|
"github.com/kovidgoyal/kitty/tools/utils"
|
||||||
@@ -15,6 +18,48 @@ import (
|
|||||||
|
|
||||||
var _ = fmt.Print
|
var _ = fmt.Print
|
||||||
|
|
||||||
|
type file_state struct {
|
||||||
|
Size int64
|
||||||
|
ModTime time.Time
|
||||||
|
Inode uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s file_state) String() string {
|
||||||
|
return fmt.Sprintf("fs{Size: %d, Inode: %d, ModTime: %s}", s.Size, s.Inode, s.ModTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *file_state) equal(o *file_state) bool {
|
||||||
|
return o != nil && s.Size == o.Size && s.ModTime.Equal(o.ModTime) && s.Inode == o.Inode
|
||||||
|
}
|
||||||
|
|
||||||
|
func get_file_state(fi fs.FileInfo) *file_state {
|
||||||
|
// The Sys() method returns the underlying data source (can be nil).
|
||||||
|
// For Unix-like systems, it's a *syscall.Stat_t.
|
||||||
|
stat, ok := fi.Sys().(*syscall.Stat_t)
|
||||||
|
if !ok {
|
||||||
|
// For non-Unix systems, you might not have an inode.
|
||||||
|
// In that case, you can fall back to using only size and mod time.
|
||||||
|
return &file_state{
|
||||||
|
Size: fi.Size(),
|
||||||
|
ModTime: fi.ModTime(),
|
||||||
|
Inode: 0, // Inode not available
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &file_state{
|
||||||
|
Size: fi.Size(),
|
||||||
|
ModTime: fi.ModTime(),
|
||||||
|
Inode: stat.Ino,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func get_file_state_from_path(path string) (*file_state, error) {
|
||||||
|
if s, err := os.Stat(path); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return get_file_state(s), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func new_disk_cache(path string, max_size int64) (dc *DiskCache, err error) {
|
func new_disk_cache(path string, max_size int64) (dc *DiskCache, err error) {
|
||||||
if path, err = filepath.Abs(path); err != nil {
|
if path, err = filepath.Abs(path); err != nil {
|
||||||
return
|
return
|
||||||
@@ -28,12 +73,13 @@ func new_disk_cache(path string, max_size int64) (dc *DiskCache, err error) {
|
|||||||
if err = ans.ensure_entries(); err != nil {
|
if err = ans.ensure_entries(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if pruned, err := ans.prune(); err != nil {
|
defer func() {
|
||||||
return nil, err
|
if we := ans.write_entries_if_dirty(); we != nil && err == nil {
|
||||||
} else if pruned {
|
err = we
|
||||||
if err = ans.write_entries(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
if _, err := ans.prune(); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
if ans.get_dir, err = os.MkdirTemp(ans.Path, "getdir-*"); err != nil {
|
if ans.get_dir, err = os.MkdirTemp(ans.Path, "getdir-*"); err != nil {
|
||||||
return
|
return
|
||||||
@@ -85,19 +131,50 @@ func (dc *DiskCache) unlock() {
|
|||||||
|
|
||||||
func (dc *DiskCache) entries_path() string { return filepath.Join(dc.Path, "entries.json") }
|
func (dc *DiskCache) entries_path() string { return filepath.Join(dc.Path, "entries.json") }
|
||||||
|
|
||||||
func (dc *DiskCache) write_entries() (err error) {
|
func (dc *DiskCache) write_entries_if_dirty() (err error) {
|
||||||
|
if !dc.entries_dirty {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path := dc.entries_path()
|
||||||
|
defer func() {
|
||||||
|
if err == nil {
|
||||||
|
dc.entries_dirty = false
|
||||||
|
if s, serr := get_file_state_from_path(path); serr == nil {
|
||||||
|
dc.entries_last_read_state = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
if d, err := json.Marshal(dc.entries); err != nil {
|
if d, err := json.Marshal(dc.entries); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
return os.WriteFile(dc.entries_path(), d, 0o600)
|
// use a rename so that the inode number changes
|
||||||
|
// dont bother with full utils.AtomicWriteFile() as it is slower
|
||||||
|
temp := path + ".temp"
|
||||||
|
removed := false
|
||||||
|
defer func() {
|
||||||
|
if !removed {
|
||||||
|
_ = os.Remove(temp)
|
||||||
|
removed = true
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err = os.WriteFile(temp, d, 0o600); err == nil {
|
||||||
|
if err = os.Rename(temp, path); err == nil {
|
||||||
|
removed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dc *DiskCache) rebuild_entries() error {
|
func (e Entry) String() string {
|
||||||
|
return fmt.Sprintf("Entry{Key: %s, Size: %d, LastUsed: %s}", e.Key, e.Size, e.LastUsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *DiskCache) entries_from_folders() (total_size int64, ans map[string]*Entry, sorted []*Entry, err error) {
|
||||||
if entries, err := os.ReadDir(dc.Path); err != nil {
|
if entries, err := os.ReadDir(dc.Path); err != nil {
|
||||||
return err
|
return 0, nil, nil, err
|
||||||
} else {
|
} else {
|
||||||
ans := make(map[string]*Entry)
|
ans = make(map[string]*Entry)
|
||||||
var total int64
|
var total int64
|
||||||
for _, x := range entries {
|
for _, x := range entries {
|
||||||
if x.IsDir() {
|
if x.IsDir() {
|
||||||
@@ -105,7 +182,7 @@ func (dc *DiskCache) rebuild_entries() error {
|
|||||||
key := sub_entries[0].Name()
|
key := sub_entries[0].Name()
|
||||||
path := dc.folder_for_key(key)
|
path := dc.folder_for_key(key)
|
||||||
if file_entries, err := os.ReadDir(path); err == nil {
|
if file_entries, err := os.ReadDir(path); err == nil {
|
||||||
e := Entry{}
|
e := Entry{Key: key}
|
||||||
for _, f := range file_entries {
|
for _, f := range file_entries {
|
||||||
if fi, err := f.Info(); err == nil {
|
if fi, err := f.Info(); err == nil {
|
||||||
e.Size += fi.Size()
|
e.Size += fi.Size()
|
||||||
@@ -120,37 +197,58 @@ func (dc *DiskCache) rebuild_entries() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sorted := utils.Values(ans)
|
sorted = utils.Values(ans)
|
||||||
slices.SortFunc(sorted, func(a, b *Entry) int {
|
slices.SortFunc(sorted, func(a, b *Entry) int {
|
||||||
return a.LastUsed.Compare(b.LastUsed)
|
return a.LastUsed.Compare(b.LastUsed)
|
||||||
})
|
})
|
||||||
dc.entries = Metadata{TotalSize: total, SortedEntries: sorted}
|
return total, ans, sorted, nil
|
||||||
dc.entry_map = ans
|
|
||||||
}
|
}
|
||||||
return nil
|
}
|
||||||
|
|
||||||
|
func (dc *DiskCache) rebuild_entries() error {
|
||||||
|
total, ans, sorted, err := dc.entries_from_folders()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dc.entries = Metadata{TotalSize: total, SortedEntries: sorted, PathMap: make(map[string]string)}
|
||||||
|
dc.entry_map = ans
|
||||||
|
dc.entries_dirty = true
|
||||||
|
return dc.write_entries_if_dirty()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dc *DiskCache) ensure_entries() error {
|
func (dc *DiskCache) ensure_entries() error {
|
||||||
needed := dc.entry_map == nil
|
needed := dc.entry_map == nil || dc.entries_last_read_state == nil
|
||||||
path := dc.entries_path()
|
path := dc.entries_path()
|
||||||
|
var fstate *file_state
|
||||||
if !needed {
|
if !needed {
|
||||||
if s, err := os.Stat(path); err == nil && s.ModTime().After(dc.entries_mod_time) {
|
if s, err := get_file_state_from_path(path); err == nil {
|
||||||
needed = true
|
fstate = s
|
||||||
|
if !s.equal(dc.entries_last_read_state) {
|
||||||
|
needed = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if needed {
|
if needed {
|
||||||
if data, err := os.ReadFile(path); err != nil {
|
if data, err := os.ReadFile(path); err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
dc.entry_map = make(map[string]*Entry)
|
dc.entry_map = make(map[string]*Entry)
|
||||||
dc.entries = Metadata{SortedEntries: make([]*Entry, 0)}
|
dc.entries = Metadata{SortedEntries: make([]*Entry, 0), PathMap: make(map[string]string)}
|
||||||
} else {
|
} else {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dc.entries = Metadata{SortedEntries: make([]*Entry, 0)}
|
dc.read_count += 1
|
||||||
|
dc.entries = Metadata{SortedEntries: make([]*Entry, 0), PathMap: make(map[string]string)}
|
||||||
if err := json.Unmarshal(data, &dc.entries); err != nil {
|
if err := json.Unmarshal(data, &dc.entries); err != nil {
|
||||||
// corrupted data
|
// corrupted data
|
||||||
dc.rebuild_entries()
|
dc.rebuild_entries()
|
||||||
|
} else {
|
||||||
|
if fstate == nil {
|
||||||
|
if s, err := get_file_state_from_path(path); err == nil {
|
||||||
|
fstate = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dc.entries_last_read_state = fstate
|
||||||
}
|
}
|
||||||
dc.entry_map = make(map[string]*Entry)
|
dc.entry_map = make(map[string]*Entry)
|
||||||
for _, e := range dc.entries.SortedEntries {
|
for _, e := range dc.entries.SortedEntries {
|
||||||
@@ -170,43 +268,68 @@ func (dc *DiskCache) folder_for_key(key string) (ans string) {
|
|||||||
return filepath.Join(dc.Path, ans)
|
return filepath.Join(dc.Path, ans)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dc *DiskCache) update_last_used(key string) {
|
func (dc *DiskCache) export_to_get_dir(key, path string) (string, error) {
|
||||||
if dc.ensure_entries() == nil {
|
dest := filepath.Join(dc.get_dir, key+"-"+filepath.Base(path))
|
||||||
dc.update_timestamp(key)
|
if err := os.Link(path, dest); err != nil {
|
||||||
|
os.Remove(dest)
|
||||||
|
if err := os.Link(path, dest); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return dest, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dc *DiskCache) get(key string, items []string) map[string]string {
|
func (dc *DiskCache) get(key string, items []string) (map[string]string, error) {
|
||||||
ans := make(map[string]string, len(items))
|
if err := dc.ensure_entries(); err != nil {
|
||||||
base := dc.folder_for_key(key)
|
return nil, err
|
||||||
if s, err := os.Stat(base); err != nil || !s.IsDir() {
|
|
||||||
return ans
|
|
||||||
}
|
}
|
||||||
|
base := dc.folder_for_key(key)
|
||||||
|
if len(items) == 0 {
|
||||||
|
if entries, err := os.ReadDir(base); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
for _, e := range entries {
|
||||||
|
items = append(items, e.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if s, err := os.Stat(base); err != nil || !s.IsDir() {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ans := make(map[string]string, len(items))
|
||||||
for _, x := range items {
|
for _, x := range items {
|
||||||
p := filepath.Join(base, x)
|
p := filepath.Join(base, x)
|
||||||
if s, err := os.Stat(p); err != nil || s.IsDir() {
|
if s, err := os.Stat(p); err != nil || s.IsDir() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dest := filepath.Join(dc.get_dir, key+"-"+x)
|
dest, _ := dc.export_to_get_dir(key, p)
|
||||||
if err := os.Link(p, dest); err != nil {
|
|
||||||
os.Remove(dest)
|
|
||||||
if err := os.Link(p, dest); err != nil {
|
|
||||||
dest = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if dest != "" {
|
if dest != "" {
|
||||||
ans[x] = dest
|
ans[x] = dest
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dc.update_last_used(key)
|
if len(items) > 0 {
|
||||||
return ans
|
dc.update_timestamp(key)
|
||||||
|
}
|
||||||
|
return ans, dc.write_entries_if_dirty()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dc *DiskCache) remove(key string) (err error) {
|
func (dc *DiskCache) remove(key string) (err error) {
|
||||||
if err = dc.ensure_entries(); err != nil {
|
if err = dc.ensure_entries(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
|
if we := dc.write_entries_if_dirty(); we != nil && err == nil {
|
||||||
|
err = we
|
||||||
|
}
|
||||||
|
}()
|
||||||
base := dc.folder_for_key(key)
|
base := dc.folder_for_key(key)
|
||||||
if err = os.RemoveAll(base); err == nil {
|
if err = os.RemoveAll(base); err == nil {
|
||||||
t := dc.entry_map[key]
|
t := dc.entry_map[key]
|
||||||
@@ -214,7 +337,7 @@ func (dc *DiskCache) remove(key string) (err error) {
|
|||||||
delete(dc.entry_map, key)
|
delete(dc.entry_map, key)
|
||||||
dc.entries.TotalSize = max(0, dc.entries.TotalSize-t.Size)
|
dc.entries.TotalSize = max(0, dc.entries.TotalSize-t.Size)
|
||||||
dc.entries.SortedEntries = utils.Filter(dc.entries.SortedEntries, func(x *Entry) bool { return x.Key != key })
|
dc.entries.SortedEntries = utils.Filter(dc.entries.SortedEntries, func(x *Entry) bool { return x.Key != key })
|
||||||
return dc.write_entries()
|
dc.entries_dirty = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -229,8 +352,10 @@ func (dc *DiskCache) prune() (bool, error) {
|
|||||||
if err := os.RemoveAll(base); err == nil {
|
if err := os.RemoveAll(base); err == nil {
|
||||||
t := dc.entries.SortedEntries[0]
|
t := dc.entries.SortedEntries[0]
|
||||||
delete(dc.entry_map, t.Key)
|
delete(dc.entry_map, t.Key)
|
||||||
|
maps.DeleteFunc(dc.entries.PathMap, func(path, key string) bool { return key == t.Key })
|
||||||
dc.entries.TotalSize = max(0, dc.entries.TotalSize-t.Size)
|
dc.entries.TotalSize = max(0, dc.entries.TotalSize-t.Size)
|
||||||
dc.entries.SortedEntries = dc.entries.SortedEntries[1:]
|
dc.entries.SortedEntries = dc.entries.SortedEntries[1:]
|
||||||
|
dc.entries_dirty = true
|
||||||
} else {
|
} else {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -244,6 +369,7 @@ func (dc *DiskCache) update_timestamp(key string) {
|
|||||||
idx := slices.Index(dc.entries.SortedEntries, t)
|
idx := slices.Index(dc.entries.SortedEntries, t)
|
||||||
copy(dc.entries.SortedEntries[idx:], dc.entries.SortedEntries[idx+1:])
|
copy(dc.entries.SortedEntries[idx:], dc.entries.SortedEntries[idx+1:])
|
||||||
dc.entries.SortedEntries[len(dc.entries.SortedEntries)-1] = t
|
dc.entries.SortedEntries[len(dc.entries.SortedEntries)-1] = t
|
||||||
|
dc.entries_dirty = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dc *DiskCache) update_accounting(key string, changed int64) (err error) {
|
func (dc *DiskCache) update_accounting(key string, changed int64) (err error) {
|
||||||
@@ -257,25 +383,52 @@ func (dc *DiskCache) update_accounting(key string, changed int64) (err error) {
|
|||||||
t.Size += changed
|
t.Size += changed
|
||||||
t.Size = max(0, t.Size)
|
t.Size = max(0, t.Size)
|
||||||
dc.entries.TotalSize += t.Size - old_size
|
dc.entries.TotalSize += t.Size - old_size
|
||||||
|
dc.entries_dirty = true
|
||||||
dc.update_timestamp(key)
|
dc.update_timestamp(key)
|
||||||
dc.prune()
|
dc.prune()
|
||||||
return dc.write_entries()
|
return dc.write_entries_if_dirty()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dc *DiskCache) keys() (ans []string, err error) {
|
func (dc *DiskCache) keys() (ans []string, err error) {
|
||||||
if err = dc.ensure_entries(); err != nil {
|
if err = dc.ensure_entries(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return utils.Keys(dc.entry_map), nil
|
ans = make([]string, len(dc.entries.SortedEntries))
|
||||||
|
for i, e := range dc.entries.SortedEntries {
|
||||||
|
ans[i] = e.Key
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dc *DiskCache) add(key string, items map[string][]byte) (err error) {
|
func (dc *DiskCache) add_path(path, key string, items map[string][]byte) (ans map[string]string, err error) {
|
||||||
|
if err = dc.ensure_entries(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if we := dc.write_entries_if_dirty(); we != nil && err == nil {
|
||||||
|
err = we
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if existing := dc.entries.PathMap[path]; existing != "" && existing != key {
|
||||||
|
delete(dc.entries.PathMap, path)
|
||||||
|
dc.entries_dirty = true
|
||||||
|
if err = dc.remove(existing); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dc.entries.PathMap[path] = key
|
||||||
|
dc.entries_dirty = true
|
||||||
|
return dc.add(key, items)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *DiskCache) add(key string, items map[string][]byte) (ans map[string]string, err error) {
|
||||||
if err = dc.ensure_entries(); err != nil {
|
if err = dc.ensure_entries(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
base := dc.folder_for_key(key)
|
base := dc.folder_for_key(key)
|
||||||
if err = os.MkdirAll(base, 0o700); err != nil {
|
if err = os.MkdirAll(base, 0o700); err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
var changed int64
|
var changed int64
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -284,22 +437,40 @@ func (dc *DiskCache) add(key string, items map[string][]byte) (err error) {
|
|||||||
err = e
|
err = e
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
ans = make(map[string]string, len(items))
|
||||||
for x, data := range items {
|
for x, data := range items {
|
||||||
p := filepath.Join(base, x)
|
p := filepath.Join(base, x)
|
||||||
var before int64
|
var before int64
|
||||||
|
exists := false
|
||||||
if s, err := os.Stat(p); err == nil {
|
if s, err := os.Stat(p); err == nil {
|
||||||
before = s.Size()
|
before = s.Size()
|
||||||
|
exists = true
|
||||||
}
|
}
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
if err = os.Remove(p); err != nil {
|
if exists {
|
||||||
return
|
if err = os.Remove(p); err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
changed -= before
|
||||||
}
|
}
|
||||||
changed -= before
|
|
||||||
} else {
|
} else {
|
||||||
|
// unlink the file so that writing to it does not change a
|
||||||
|
// previously linked copy created by get()
|
||||||
|
if exists {
|
||||||
|
_ = os.Remove(p)
|
||||||
|
}
|
||||||
if err = os.WriteFile(p, data, 0o700); err != nil {
|
if err = os.WriteFile(p, data, 0o700); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
changed += int64(len(data)) - before
|
changed += int64(len(data)) - before
|
||||||
|
if dest, err := dc.export_to_get_dir(key, p); err != nil {
|
||||||
|
return ans, err
|
||||||
|
} else {
|
||||||
|
ans[x] = dest
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ package disk_cache
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"path/filepath"
|
||||||
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -23,16 +24,45 @@ func TestDiskCache(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
ensure_entries := func() {
|
||||||
|
for _, x := range []*DiskCache{dc, dc2} {
|
||||||
|
if err = x.ensure_entries(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m := dc.Get("missing", "one", "two")
|
|
||||||
if diff := cmp.Diff(m, make(map[string]string)); diff != "" {
|
|
||||||
t.Fatalf("Unexpected return from missing: %s", diff)
|
|
||||||
}
|
}
|
||||||
dc.Add("k1", map[string][]byte{"1": []byte("abcd"), "2": []byte("efgh")})
|
arc := func(counts ...int) {
|
||||||
|
ensure_entries()
|
||||||
|
if diff := cmp.Diff(counts, []int{dc.read_count, dc2.read_count}); diff != "" {
|
||||||
|
t.Fatalf("disk cache has unexpected read count\n%s\n%s", diff, debug.Stack())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add := func(dc *DiskCache, key string, data map[string]string) {
|
||||||
|
d := make(map[string][]byte, len(data))
|
||||||
|
for k, v := range data {
|
||||||
|
d[k] = []byte(v)
|
||||||
|
}
|
||||||
|
if _, err := dc.Add(key, d); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ensure_entries()
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := dc.Get("missing", "one", "two")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(m) > 0 {
|
||||||
|
t.Fatalf("Unexpected return from missing: %s", m)
|
||||||
|
}
|
||||||
|
|
||||||
ad := func(key string, expected map[string]string) {
|
ad := func(key string, expected map[string]string) {
|
||||||
for _, x := range []*DiskCache{dc, dc2} {
|
for _, x := range []*DiskCache{dc, dc2} {
|
||||||
actual := x.Get(key, utils.Keys(expected)...)
|
actual, err := x.Get(key, utils.Keys(expected)...)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
for k, path := range actual {
|
for k, path := range actual {
|
||||||
d, err := os.ReadFile(path)
|
d, err := os.ReadFile(path)
|
||||||
@@ -44,35 +74,78 @@ func TestDiskCache(t *testing.T) {
|
|||||||
if diff := cmp.Diff(expected, actual); diff != "" {
|
if diff := cmp.Diff(expected, actual); diff != "" {
|
||||||
t.Fatalf("Data for %s not equal: %s", key, diff)
|
t.Fatalf("Data for %s not equal: %s", key, diff)
|
||||||
}
|
}
|
||||||
|
ensure_entries()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ak := func(keys ...string) {
|
ak := func(keys ...string) {
|
||||||
for _, x := range []*DiskCache{dc, dc2} {
|
for i, x := range []*DiskCache{dc, dc2} {
|
||||||
kk, err := x.keys()
|
kk, err := x.keys()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
slices.Sort(kk)
|
|
||||||
slices.Sort(keys)
|
|
||||||
if diff := cmp.Diff(keys, kk); diff != "" {
|
if diff := cmp.Diff(keys, kk); diff != "" {
|
||||||
t.Fatalf("Unexpected keys: %s", diff)
|
t.Fatalf("wrong keys in %d: %s", i+1, diff)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ensure_entries()
|
||||||
}
|
}
|
||||||
|
add(dc, "k1", map[string]string{"1": "abcd", "2": "efgh"})
|
||||||
|
arc(0, 1)
|
||||||
ad("k1", map[string]string{"1": "abcd", "2": "efgh"})
|
ad("k1", map[string]string{"1": "abcd", "2": "efgh"})
|
||||||
dc.Add("k1", map[string][]byte{"3": []byte("ijk"), "4": []byte("lmo")})
|
arc(1, 2) // the two gets cause two updates
|
||||||
dc2.Add("k2", map[string][]byte{"1": []byte("123456789")})
|
add(dc, "k1", map[string]string{"3": "ijk", "4": "lmo"})
|
||||||
|
arc(1, 3) // dc.Add() causes re-read in dc2
|
||||||
|
ak("k1")
|
||||||
|
arc(1, 3)
|
||||||
|
add(dc2, "k2", map[string]string{"1": "123456789"})
|
||||||
|
arc(2, 3) // dc2.Add() causes re-read in dc
|
||||||
|
ak("k1", "k2")
|
||||||
|
arc(2, 3)
|
||||||
ad("k1", map[string]string{"1": "abcd", "2": "efgh", "3": "ijk"})
|
ad("k1", map[string]string{"1": "abcd", "2": "efgh", "3": "ijk"})
|
||||||
if dc.entries.TotalSize != 14+9 {
|
if dc.entries.TotalSize != 14+9 {
|
||||||
t.Fatalf("TotalSize: %d != %d", dc.entries.TotalSize, 14+9)
|
t.Fatalf("TotalSize: %d != %d", dc.entries.TotalSize, 14+9)
|
||||||
}
|
}
|
||||||
|
arc(3, 4) // the two gets cause two updates
|
||||||
|
ak("k2", "k1")
|
||||||
|
dc.Get("k2")
|
||||||
|
arc(3, 5) // dc.Get() causes dc2 to read
|
||||||
ak("k1", "k2")
|
ak("k1", "k2")
|
||||||
dc.Add("k3", map[string][]byte{"1": []byte(strings.Repeat("a", int(dc.MaxSize)-10))})
|
add(dc, "k3", map[string]string{"1": strings.Repeat("a", int(dc.MaxSize)-10)})
|
||||||
ak("k3", "k2")
|
arc(3, 6) // dc.Add() causes dc2 to read
|
||||||
|
ak("k2", "k3")
|
||||||
// check that creating a new disk cache prunes
|
// check that creating a new disk cache prunes
|
||||||
_, err = NewDiskCache(tdir, dc.MaxSize-8)
|
_, err = NewDiskCache(tdir, dc.MaxSize-8)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
ak("k3")
|
ak("k3")
|
||||||
|
arc(4, 7) // NewDiskCache()
|
||||||
|
|
||||||
|
// test the path api
|
||||||
|
path := filepath.Join(tdir, "source")
|
||||||
|
if err = os.WriteFile(path, []byte("abcdfjrof"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
key, _, err := dc.GetPath(path)
|
||||||
|
if _, err = dc.AddPath(path, key, map[string][]byte{"1": []byte("1")}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, _, entries_before, _ := dc.entries_from_folders()
|
||||||
|
if diff := cmp.Diff(1, len(dc.entries.PathMap)); diff != "" {
|
||||||
|
t.Fatalf("unexpected pathmap count: %s", diff)
|
||||||
|
}
|
||||||
|
if err = os.WriteFile(path, []byte("changed contents"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err = dc.AddPath(path, key, map[string][]byte{"1": []byte("2")}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(1, len(dc.entries.PathMap)); diff != "" {
|
||||||
|
t.Fatalf("unexpected pathmap count: %s", diff)
|
||||||
|
}
|
||||||
|
_, _, entries_after, _ := dc.entries_from_folders()
|
||||||
|
if len(entries_before) != len(entries_after) {
|
||||||
|
t.Fatalf("unexpected entries: %s", entries_after)
|
||||||
|
}
|
||||||
|
arc(4, 8) // dc.AddPath() causes dc2 to read
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ func DownloadFileWithProgress(destpath, url string, kill_if_signaled bool) (err
|
|||||||
}
|
}
|
||||||
|
|
||||||
do_download := func() {
|
do_download := func() {
|
||||||
|
lp.RecoverFromPanicInGoRoutine()
|
||||||
dl_data.mutex.Lock()
|
dl_data.mutex.Lock()
|
||||||
dl_data.download_started = true
|
dl_data.download_started = true
|
||||||
dl_data.mutex.Unlock()
|
dl_data.mutex.Unlock()
|
||||||
|
|||||||
@@ -136,14 +136,16 @@ func (self *ImageCollection) ResizeForPageSize(width, height int) {
|
|||||||
|
|
||||||
ctx := images.Context{}
|
ctx := images.Context{}
|
||||||
keys := utils.Keys(self.images)
|
keys := utils.Keys(self.images)
|
||||||
ctx.Parallel(0, len(keys), func(nums <-chan int) {
|
if err := ctx.SafeParallel(0, len(keys), func(nums <-chan int) {
|
||||||
for i := range nums {
|
for i := range nums {
|
||||||
img := self.images[keys[i]]
|
img := self.images[keys[i]]
|
||||||
if img.src.loaded && img.err == nil {
|
if img.src.loaded && img.err == nil {
|
||||||
img.ResizeForPageSize(width, height)
|
img.ResizeForPageSize(width, height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *ImageCollection) DeleteAllVisiblePlacements(lp *loop.Loop) {
|
func (self *ImageCollection) DeleteAllVisiblePlacements(lp *loop.Loop) {
|
||||||
@@ -294,7 +296,7 @@ func (self *ImageCollection) LoadAll() {
|
|||||||
defer self.mutex.Unlock()
|
defer self.mutex.Unlock()
|
||||||
ctx := images.Context{}
|
ctx := images.Context{}
|
||||||
all := utils.Values(self.images)
|
all := utils.Values(self.images)
|
||||||
ctx.Parallel(0, len(self.images), func(nums <-chan int) {
|
if err := ctx.SafeParallel(0, len(self.images), func(nums <-chan int) {
|
||||||
for i := range nums {
|
for i := range nums {
|
||||||
img := all[i]
|
img := all[i]
|
||||||
if !img.src.loaded {
|
if !img.src.loaded {
|
||||||
@@ -305,7 +307,9 @@ func (self *ImageCollection) LoadAll() {
|
|||||||
img.src.loaded = true
|
img.src.loaded = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewImageCollection(paths ...string) *ImageCollection {
|
func NewImageCollection(paths ...string) *ImageCollection {
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ func read_ignoring_temporary_errors(f *tty.Term, buf []byte) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func read_from_tty(pipe_r *os.File, term *tty.Term, results_channel chan<- []byte, err_channel chan<- error, quit_channel <-chan byte, leftover_channel chan<- []byte) {
|
func read_from_tty(pipe_r *os.File, term *tty.Term, results_channel chan<- []byte, err_channel chan<- error, quit_channel <-chan byte, leftover_channel chan<- []byte) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
text, _ := utils.Format_stacktrace_on_panic(r)
|
||||||
|
err_channel <- fmt.Errorf("%s", text)
|
||||||
|
}
|
||||||
|
}()
|
||||||
keep_going := true
|
keep_going := true
|
||||||
pipe_fd := int(pipe_r.Fd())
|
pipe_fd := int(pipe_r.Fd())
|
||||||
tty_fd := term.Fd()
|
tty_fd := term.Fd()
|
||||||
@@ -115,6 +121,12 @@ func read_until_primary_device_attributes_response(term *tty.Term, initial_bytes
|
|||||||
}
|
}
|
||||||
received := make(chan error)
|
received := make(chan error)
|
||||||
go func() {
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
text, _ := utils.Format_stacktrace_on_panic(r)
|
||||||
|
received <- fmt.Errorf("%s", text)
|
||||||
|
}
|
||||||
|
}()
|
||||||
buf := make([]byte, 1024)
|
buf := make([]byte, 1024)
|
||||||
n, err := read_ignoring_temporary_errors(term, buf)
|
n, err := read_ignoring_temporary_errors(term, buf)
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
|
|||||||
@@ -168,6 +168,12 @@ func write_to_tty(
|
|||||||
pipe_r *os.File, term *tty.Term,
|
pipe_r *os.File, term *tty.Term,
|
||||||
job_channel <-chan write_msg, err_channel chan<- error, write_done_channel chan<- IdType,
|
job_channel <-chan write_msg, err_channel chan<- error, write_done_channel chan<- IdType,
|
||||||
) {
|
) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
text, _ := utils.Format_stacktrace_on_panic(r)
|
||||||
|
err_channel <- fmt.Errorf("%s", text)
|
||||||
|
}
|
||||||
|
}()
|
||||||
keep_going := true
|
keep_going := true
|
||||||
defer func() {
|
defer func() {
|
||||||
pipe_r.Close()
|
pipe_r.Close()
|
||||||
|
|||||||
@@ -99,6 +99,9 @@ func find_matching_codepoints(prefix string) (ans mark_set) {
|
|||||||
ans.AddItems(marks...)
|
ans.AddItems(marks...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ans == nil {
|
||||||
|
ans = utils.NewSet[uint16](0)
|
||||||
|
}
|
||||||
return ans
|
return ans
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,11 +110,13 @@ func marks_for_query(query string) (ans mark_set) {
|
|||||||
prefixes := strings.Split(strings.ToLower(query), " ")
|
prefixes := strings.Split(strings.ToLower(query), " ")
|
||||||
results := make(chan mark_set, len(prefixes))
|
results := make(chan mark_set, len(prefixes))
|
||||||
ctx := images.Context{}
|
ctx := images.Context{}
|
||||||
ctx.Parallel(0, len(prefixes), func(nums <-chan int) {
|
if err := ctx.SafeParallel(0, len(prefixes), func(nums <-chan int) {
|
||||||
for i := range nums {
|
for i := range nums {
|
||||||
results <- find_matching_codepoints(prefixes[i])
|
results <- find_matching_codepoints(prefixes[i])
|
||||||
}
|
}
|
||||||
})
|
}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
close(results)
|
close(results)
|
||||||
for x := range results {
|
for x := range results {
|
||||||
if ans == nil {
|
if ans == nil {
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
|
"image/color"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kovidgoyal/kitty/tools/cli"
|
"github.com/kovidgoyal/kitty/tools/cli"
|
||||||
@@ -17,9 +19,9 @@ var _ = fmt.Print
|
|||||||
|
|
||||||
func encode_rgba(output io.Writer, img image.Image) (err error) {
|
func encode_rgba(output io.Writer, img image.Image) (err error) {
|
||||||
var final_img *image.NRGBA
|
var final_img *image.NRGBA
|
||||||
switch img.(type) {
|
switch ti := img.(type) {
|
||||||
case *image.NRGBA:
|
case *image.NRGBA:
|
||||||
final_img = img.(*image.NRGBA)
|
final_img = ti
|
||||||
default:
|
default:
|
||||||
b := img.Bounds()
|
b := img.Bounds()
|
||||||
final_img = image.NewNRGBA(image.Rect(0, 0, b.Dx(), b.Dy()))
|
final_img = image.NewNRGBA(image.Rect(0, 0, b.Dx(), b.Dy()))
|
||||||
@@ -68,6 +70,48 @@ func convert_image(input io.ReadSeeker, output io.Writer, format string) (err er
|
|||||||
return Encode(output, img, mt)
|
return Encode(output, img, mt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PalettedToNRGBA(paletted *image.Paletted) *image.NRGBA {
|
||||||
|
bounds := paletted.Bounds()
|
||||||
|
nrgba := image.NewNRGBA(bounds)
|
||||||
|
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||||
|
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||||
|
c := color.NRGBAModel.Convert(paletted.At(x, y))
|
||||||
|
nrgba.Set(x, y, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nrgba
|
||||||
|
}
|
||||||
|
|
||||||
|
func develop_serialize(input_data []byte) (err error) {
|
||||||
|
img, err := OpenNativeImageFromReader(bytes.NewReader(input_data))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m, b := img.Serialize()
|
||||||
|
rimg, err := ImageFromSerialized(m, b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := range img.Frames {
|
||||||
|
a, b := img.Frames[i], rimg.Frames[i]
|
||||||
|
if a.Img.Bounds() != b.Img.Bounds() {
|
||||||
|
return fmt.Errorf("bounds of frame %d not equal: %v != %v", i, a.Img.Bounds(), b.Img.Bounds())
|
||||||
|
}
|
||||||
|
for y := a.Img.Bounds().Min.Y; y < a.Img.Bounds().Max.Y; y++ {
|
||||||
|
for x := a.Img.Bounds().Min.X; x < a.Img.Bounds().Max.X; x++ {
|
||||||
|
or, og, ob, oa := a.Img.At(x, y).RGBA()
|
||||||
|
nr, ng, nb, na := b.Img.At(x, y).RGBA()
|
||||||
|
a, b := []uint32{or, og, ob, oa}, []uint32{nr, ng, nb, na}
|
||||||
|
if !slices.Equal(a, b) {
|
||||||
|
return fmt.Errorf("pixel at %dx%d differs: %v != %v", x, y, a, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func ConvertEntryPoint(root *cli.Command) {
|
func ConvertEntryPoint(root *cli.Command) {
|
||||||
root.AddSubCommand(&cli.Command{
|
root.AddSubCommand(&cli.Command{
|
||||||
Name: "__convert_image__",
|
Name: "__convert_image__",
|
||||||
@@ -82,6 +126,11 @@ func ConvertEntryPoint(root *cli.Command) {
|
|||||||
if _, err = io.Copy(buf, os.Stdin); err != nil {
|
if _, err = io.Copy(buf, os.Stdin); err != nil {
|
||||||
return 1, err
|
return 1, err
|
||||||
}
|
}
|
||||||
|
if format == "develop-serialize" {
|
||||||
|
err = develop_serialize(buf.Bytes())
|
||||||
|
rc = utils.IfElse(err == nil, 0, 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
if err = convert_image(bytes.NewReader(buf.Bytes()), os.Stdout, format); err != nil {
|
if err = convert_image(bytes.NewReader(buf.Bytes()), os.Stdout, format); err != nil {
|
||||||
rc = 1
|
rc = 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"image/gif"
|
"image/gif"
|
||||||
|
"image/png"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -53,6 +54,27 @@ type ImageFrame struct {
|
|||||||
Img image.Image
|
Img image.Image
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SerializableImageFrame struct {
|
||||||
|
Width, Height, Left, Top int
|
||||||
|
Number int // 1-based number
|
||||||
|
Compose_onto int // number of frame to compose onto
|
||||||
|
Delay_ms int // negative for gapless frame, zero ignored, positive is number of ms
|
||||||
|
Is_opaque bool
|
||||||
|
Size int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SerializableImageFrame) NeededSize() int {
|
||||||
|
return utils.IfElse(s.Is_opaque, 3, 4) * s.Width * s.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ImageFrame) Serialize() SerializableImageFrame {
|
||||||
|
return SerializableImageFrame{
|
||||||
|
Width: s.Width, Height: s.Height, Left: s.Left, Top: s.Top,
|
||||||
|
Number: s.Number, Compose_onto: s.Compose_onto, Delay_ms: int(s.Delay_ms),
|
||||||
|
Is_opaque: s.Is_opaque,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (self *ImageFrame) DataAsSHM(pattern string) (ans shm.MMap, err error) {
|
func (self *ImageFrame) DataAsSHM(pattern string) (ans shm.MMap, err error) {
|
||||||
bytes_per_pixel := 4
|
bytes_per_pixel := 4
|
||||||
if self.Is_opaque {
|
if self.Is_opaque {
|
||||||
@@ -122,12 +144,82 @@ func (self *ImageFrame) Data() (ans []byte) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ImageFrameFromSerialized(s SerializableImageFrame, data []byte) (aa *ImageFrame, err error) {
|
||||||
|
ans := ImageFrame{
|
||||||
|
Width: s.Width, Height: s.Height, Left: s.Left, Top: s.Top,
|
||||||
|
Number: s.Number, Compose_onto: s.Compose_onto, Delay_ms: int32(s.Delay_ms),
|
||||||
|
Is_opaque: s.Is_opaque,
|
||||||
|
}
|
||||||
|
bytes_per_pixel := utils.IfElse(s.Is_opaque, 3, 4)
|
||||||
|
if expected := bytes_per_pixel * s.Width * s.Height; len(data) != expected {
|
||||||
|
return nil, fmt.Errorf("serialized image data has size: %d != %d", len(data), expected)
|
||||||
|
}
|
||||||
|
if s.Is_opaque {
|
||||||
|
ans.Img, err = NewNRGBWithContiguousRGBPixels(data, s.Left, s.Top, s.Width, s.Height)
|
||||||
|
} else {
|
||||||
|
ans.Img, err = NewNRGBAWithContiguousRGBAPixels(data, s.Left, s.Top, s.Width, s.Height)
|
||||||
|
}
|
||||||
|
return &ans, err
|
||||||
|
}
|
||||||
|
|
||||||
type ImageData struct {
|
type ImageData struct {
|
||||||
Width, Height int
|
Width, Height int
|
||||||
Format_uppercase string
|
Format_uppercase string
|
||||||
Frames []*ImageFrame
|
Frames []*ImageFrame
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SerializableImageMetadata struct {
|
||||||
|
Version int
|
||||||
|
Width, Height int
|
||||||
|
Format_uppercase string
|
||||||
|
Frames []SerializableImageFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
const SERIALIZE_VERSION = 1
|
||||||
|
|
||||||
|
func (self *ImageFrame) SaveAsUncompressedPNG(output io.Writer) error {
|
||||||
|
encoder := png.Encoder{CompressionLevel: png.NoCompression}
|
||||||
|
return encoder.Encode(output, self.Img)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *ImageData) SerializeOnlyMetadata() SerializableImageMetadata {
|
||||||
|
f := make([]SerializableImageFrame, len(self.Frames))
|
||||||
|
for i, s := range self.Frames {
|
||||||
|
f[i] = s.Serialize()
|
||||||
|
}
|
||||||
|
return SerializableImageMetadata{Version: SERIALIZE_VERSION, Width: self.Width, Height: self.Height, Format_uppercase: self.Format_uppercase, Frames: f}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *ImageData) Serialize() (SerializableImageMetadata, [][]byte) {
|
||||||
|
m := self.SerializeOnlyMetadata()
|
||||||
|
data := make([][]byte, len(self.Frames))
|
||||||
|
for i, f := range self.Frames {
|
||||||
|
data[i] = f.Data()
|
||||||
|
m.Frames[i].Size = len(data[i])
|
||||||
|
}
|
||||||
|
return m, data
|
||||||
|
}
|
||||||
|
|
||||||
|
func ImageFromSerialized(m SerializableImageMetadata, data [][]byte) (*ImageData, error) {
|
||||||
|
if m.Version > SERIALIZE_VERSION {
|
||||||
|
return nil, fmt.Errorf("serialized image data has unsupported version: %d", m.Version)
|
||||||
|
}
|
||||||
|
if len(m.Frames) != len(data) {
|
||||||
|
return nil, fmt.Errorf("serialized image data has %d frames in metadata but have data for: %d", len(m.Frames), len(data))
|
||||||
|
}
|
||||||
|
ans := ImageData{
|
||||||
|
Width: m.Width, Height: m.Height, Format_uppercase: m.Format_uppercase,
|
||||||
|
}
|
||||||
|
for i, f := range m.Frames {
|
||||||
|
if ff, err := ImageFrameFromSerialized(f, data[i]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
ans.Frames = append(ans.Frames, ff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &ans, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (self *ImageFrame) Resize(x_frac, y_frac float64) *ImageFrame {
|
func (self *ImageFrame) Resize(x_frac, y_frac float64) *ImageFrame {
|
||||||
b := self.Img.Bounds()
|
b := self.Img.Bounds()
|
||||||
left, top, width, height := b.Min.X, b.Min.Y, b.Dx(), b.Dy()
|
left, top, width, height := b.Min.X, b.Min.Y, b.Dx(), b.Dy()
|
||||||
@@ -194,29 +286,7 @@ func MakeTempDir(template string) (ans string, err error) {
|
|||||||
return os.MkdirTemp("", template)
|
return os.MkdirTemp("", template)
|
||||||
}
|
}
|
||||||
|
|
||||||
func check_resize(frame *ImageFrame, filename string) error {
|
// Native {{{
|
||||||
// ImageMagick sometimes generates RGBA images smaller than the specified
|
|
||||||
// size. See https://github.com/kovidgoyal/kitty/issues/276 for examples
|
|
||||||
s, err := os.Stat(filename)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
sz := int(s.Size())
|
|
||||||
bytes_per_pixel := 4
|
|
||||||
if frame.Is_opaque {
|
|
||||||
bytes_per_pixel = 3
|
|
||||||
}
|
|
||||||
expected_size := bytes_per_pixel * frame.Width * frame.Height
|
|
||||||
if sz < expected_size {
|
|
||||||
missing := expected_size - sz
|
|
||||||
if missing%(bytes_per_pixel*frame.Width) != 0 {
|
|
||||||
return fmt.Errorf("ImageMagick failed to resize correctly. It generated %d < %d of data (w=%d h=%d bpp=%d)", sz, expected_size, frame.Width, frame.Height, bytes_per_pixel)
|
|
||||||
}
|
|
||||||
frame.Height -= missing / (bytes_per_pixel * frame.Width)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (frame *ImageFrame) set_delay(min_gap, delay int) {
|
func (frame *ImageFrame) set_delay(min_gap, delay int) {
|
||||||
frame.Delay_ms = int32(max(min_gap, delay) * 10)
|
frame.Delay_ms = int32(max(min_gap, delay) * 10)
|
||||||
if frame.Delay_ms == 0 {
|
if frame.Delay_ms == 0 {
|
||||||
@@ -266,10 +336,36 @@ func OpenNativeImageFromReader(f io.ReadSeeker) (ans *ImageData, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// }}}
|
||||||
|
|
||||||
|
// ImageMagick {{{
|
||||||
var MagickExe = sync.OnceValue(func() string {
|
var MagickExe = sync.OnceValue(func() string {
|
||||||
return utils.FindExe("magick")
|
return utils.FindExe("magick")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
func check_resize(frame *ImageFrame, filename string) error {
|
||||||
|
// ImageMagick sometimes generates RGBA images smaller than the specified
|
||||||
|
// size. See https://github.com/kovidgoyal/kitty/issues/276 for examples
|
||||||
|
s, err := os.Stat(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sz := int(s.Size())
|
||||||
|
bytes_per_pixel := 4
|
||||||
|
if frame.Is_opaque {
|
||||||
|
bytes_per_pixel = 3
|
||||||
|
}
|
||||||
|
expected_size := bytes_per_pixel * frame.Width * frame.Height
|
||||||
|
if sz < expected_size {
|
||||||
|
missing := expected_size - sz
|
||||||
|
if missing%(bytes_per_pixel*frame.Width) != 0 {
|
||||||
|
return fmt.Errorf("ImageMagick failed to resize correctly. It generated %d < %d of data (w=%d h=%d bpp=%d)", sz, expected_size, frame.Width, frame.Height, bytes_per_pixel)
|
||||||
|
}
|
||||||
|
frame.Height -= missing / (bytes_per_pixel * frame.Width)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func RunMagick(path string, cmd []string) ([]byte, error) {
|
func RunMagick(path string, cmd []string) ([]byte, error) {
|
||||||
if MagickExe() != "magick" {
|
if MagickExe() != "magick" {
|
||||||
cmd = append([]string{MagickExe()}, cmd...)
|
cmd = append([]string{MagickExe()}, cmd...)
|
||||||
@@ -610,6 +706,8 @@ func OpenImageFromPathWithMagick(path string) (ans *ImageData, err error) {
|
|||||||
return ans, nil
|
return ans, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// }}}
|
||||||
|
|
||||||
func OpenImageFromPath(path string) (ans *ImageData, err error) {
|
func OpenImageFromPath(path string) (ans *ImageData, err error) {
|
||||||
mt := utils.GuessMimeType(path)
|
mt := utils.GuessMimeType(path)
|
||||||
if DecodableImageTypes[mt] {
|
if DecodableImageTypes[mt] {
|
||||||
@@ -620,7 +718,7 @@ func OpenImageFromPath(path string) (ans *ImageData, err error) {
|
|||||||
defer f.Close()
|
defer f.Close()
|
||||||
ans, err = OpenNativeImageFromReader(f)
|
ans, err = OpenNativeImageFromReader(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to load image at %#v with error: %w", path, err)
|
return OpenImageFromPathWithMagick(path)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return OpenImageFromPathWithMagick(path)
|
return OpenImageFromPathWithMagick(path)
|
||||||
|
|||||||
@@ -9,6 +9,35 @@ import (
|
|||||||
|
|
||||||
var _ = fmt.Print
|
var _ = fmt.Print
|
||||||
|
|
||||||
|
func paletted_is_opaque(p *image.Paletted) bool {
|
||||||
|
if len(p.Palette) > 256 {
|
||||||
|
return p.Opaque()
|
||||||
|
}
|
||||||
|
var is_alpha [256]bool
|
||||||
|
has_alpha := false
|
||||||
|
for i, c := range p.Palette {
|
||||||
|
_, _, _, a := c.RGBA()
|
||||||
|
if a != 0xffff {
|
||||||
|
is_alpha[i] = true
|
||||||
|
has_alpha = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !has_alpha {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
i0, i1 := 0, p.Rect.Dx()
|
||||||
|
for y := p.Rect.Min.Y; y < p.Rect.Max.Y; y++ {
|
||||||
|
for _, c := range p.Pix[i0:i1] {
|
||||||
|
if is_alpha[c] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i0 += p.Stride
|
||||||
|
i1 += p.Stride
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func IsOpaque(img image.Image) bool {
|
func IsOpaque(img image.Image) bool {
|
||||||
switch i := img.(type) {
|
switch i := img.(type) {
|
||||||
case *image.RGBA:
|
case *image.RGBA:
|
||||||
@@ -30,7 +59,7 @@ func IsOpaque(img image.Image) bool {
|
|||||||
case *image.CMYK:
|
case *image.CMYK:
|
||||||
return i.Opaque()
|
return i.Opaque()
|
||||||
case *image.Paletted:
|
case *image.Paletted:
|
||||||
return i.Opaque()
|
return paletted_is_opaque(i)
|
||||||
case *image.Uniform:
|
case *image.Uniform:
|
||||||
return i.Opaque()
|
return i.Opaque()
|
||||||
case *image.YCbCr:
|
case *image.YCbCr:
|
||||||
|
|||||||
31
tools/utils/images/serialize_test.go
Normal file
31
tools/utils/images/serialize_test.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package images
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/kovidgoyal/kitty"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = fmt.Print
|
||||||
|
|
||||||
|
func TestImageSerialize(t *testing.T) {
|
||||||
|
img, err := OpenNativeImageFromReader(bytes.NewReader(kitty.KittyLogoAsPNGData))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
m, data := img.Serialize()
|
||||||
|
img2, err := ImageFromSerialized(m, data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
m2, data2 := img2.Serialize()
|
||||||
|
if diff := cmp.Diff(m, m2); diff != "" {
|
||||||
|
t.Fatalf("Image metadata failed to roundtrip:\n%s", diff)
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(data, data2); diff != "" {
|
||||||
|
t.Fatalf("Image data failed to roundtrip:\n%s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,14 +25,14 @@ func (c NRGBColor) RGBA() (r, g, b, a uint32) {
|
|||||||
g |= g << 8
|
g |= g << 8
|
||||||
b = uint32(c.B)
|
b = uint32(c.B)
|
||||||
b |= b << 8
|
b |= b << 8
|
||||||
a = 65280 // ( 255 << 8 )
|
a = 65535 // (255 << 8 | 255)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// NRGB is an in-memory image whose At method returns NRGBColor values.
|
// NRGB is an in-memory image whose At method returns NRGBColor values.
|
||||||
type NRGB struct {
|
type NRGB struct {
|
||||||
// Pix holds the image's pixels, in R, G, B, A order. The pixel at
|
// Pix holds the image's pixels, in R, G, B order. The pixel at
|
||||||
// (x, y) starts at Pix[(y-Rect.Min.Y)*Stride + (x-Rect.Min.X)*4].
|
// (x, y) starts at Pix[(y-Rect.Min.Y)*Stride + (x-Rect.Min.X)*3].
|
||||||
Pix []uint8
|
Pix []uint8
|
||||||
// Stride is the Pix stride (in bytes) between vertically adjacent pixels.
|
// Stride is the Pix stride (in bytes) between vertically adjacent pixels.
|
||||||
Stride int
|
Stride int
|
||||||
@@ -45,17 +45,18 @@ func nrgbModel(c color.Color) color.Color {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
r, g, b, a := c.RGBA()
|
r, g, b, a := c.RGBA()
|
||||||
if a == 0xffff {
|
switch a {
|
||||||
|
case 0xffff:
|
||||||
|
return NRGBColor{uint8(r >> 8), uint8(g >> 8), uint8(b >> 8)}
|
||||||
|
case 0:
|
||||||
|
return NRGBColor{0, 0, 0}
|
||||||
|
default:
|
||||||
|
// Since Color.RGBA returns an alpha-premultiplied color, we should have r <= a && g <= a && b <= a.
|
||||||
|
r = (r * 0xffff) / a
|
||||||
|
g = (g * 0xffff) / a
|
||||||
|
b = (b * 0xffff) / a
|
||||||
return NRGBColor{uint8(r >> 8), uint8(g >> 8), uint8(b >> 8)}
|
return NRGBColor{uint8(r >> 8), uint8(g >> 8), uint8(b >> 8)}
|
||||||
}
|
}
|
||||||
if a == 0 {
|
|
||||||
return NRGBColor{0, 0, 0}
|
|
||||||
}
|
|
||||||
// Since Color.RGBA returns an alpha-premultiplied color, we should have r <= a && g <= a && b <= a.
|
|
||||||
r = (r * 0xffff) / a
|
|
||||||
g = (g * 0xffff) / a
|
|
||||||
b = (b * 0xffff) / a
|
|
||||||
return NRGBColor{uint8(r >> 8), uint8(g >> 8), uint8(b >> 8)}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var NRGBModel color.Model = color.ModelFunc(nrgbModel)
|
var NRGBModel color.Model = color.ModelFunc(nrgbModel)
|
||||||
@@ -73,14 +74,14 @@ func (p *NRGB) NRGBAt(x, y int) NRGBColor {
|
|||||||
return NRGBColor{}
|
return NRGBColor{}
|
||||||
}
|
}
|
||||||
i := p.PixOffset(x, y)
|
i := p.PixOffset(x, y)
|
||||||
s := p.Pix[i : i+4 : i+4] // Small cap improves performance, see https://golang.org/issue/27857
|
s := p.Pix[i : i+3 : i+3] // Small cap improves performance, see https://golang.org/issue/27857
|
||||||
return NRGBColor{s[0], s[1], s[2]}
|
return NRGBColor{s[0], s[1], s[2]}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PixOffset returns the index of the first element of Pix that corresponds to
|
// PixOffset returns the index of the first element of Pix that corresponds to
|
||||||
// the pixel at (x, y).
|
// the pixel at (x, y).
|
||||||
func (p *NRGB) PixOffset(x, y int) int {
|
func (p *NRGB) PixOffset(x, y int) int {
|
||||||
return (y-p.Rect.Min.Y)*p.Stride + (x-p.Rect.Min.X)*4
|
return (y-p.Rect.Min.Y)*p.Stride + (x-p.Rect.Min.X)*3
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NRGB) Set(x, y int, c color.Color) {
|
func (p *NRGB) Set(x, y int, c color.Color) {
|
||||||
@@ -170,14 +171,17 @@ func newScannerRGB(img image.Image, opaque_base NRGBColor) *scanner_rgb {
|
|||||||
}
|
}
|
||||||
if img, ok := img.(*image.Paletted); ok {
|
if img, ok := img.(*image.Paletted); ok {
|
||||||
s.palette = make([]NRGBColor, max(256, len(img.Palette)))
|
s.palette = make([]NRGBColor, max(256, len(img.Palette)))
|
||||||
d := make([]uint8, 3)
|
d := [3]uint8{0, 0, 0}
|
||||||
|
ds := d[:]
|
||||||
for i := 0; i < len(img.Palette); i++ {
|
for i := 0; i < len(img.Palette); i++ {
|
||||||
r, g, b, a := img.Palette[i].RGBA()
|
r, g, b, a := img.Palette[i].RGBA()
|
||||||
switch a {
|
switch a {
|
||||||
case 0:
|
case 0:
|
||||||
s.palette[i] = opaque_base
|
s.palette[i] = opaque_base
|
||||||
|
case 0xffff:
|
||||||
|
s.palette[i] = NRGBColor{uint8(r >> 8), uint8(g >> 8), uint8(b >> 8)}
|
||||||
default:
|
default:
|
||||||
blend(d, s.opaque_base, uint8((r*0xffff/a)>>8), uint8((g*0xffff/a)>>8), uint8((b*0xffff/a)>>8), uint8(a>>8))
|
blend(ds, s.opaque_base, uint8((r*0xffff/a)>>8), uint8((g*0xffff/a)>>8), uint8((b*0xffff/a)>>8), uint8(a>>8))
|
||||||
s.palette[i] = NRGBColor{d[0], d[1], d[2]}
|
s.palette[i] = NRGBColor{d[0], d[1], d[2]}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,3 +429,15 @@ func NewNRGB(r image.Rectangle) *NRGB {
|
|||||||
Rect: r,
|
Rect: r,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewNRGBWithContiguousRGBPixels(p []byte, left, top, width, height int) (*NRGB, error) {
|
||||||
|
const bpp = 3
|
||||||
|
if expected := bpp * width * height; expected != len(p) {
|
||||||
|
return nil, fmt.Errorf("the image width and height dont match the size of the specified pixel data: width=%d height=%d sz=%d != %d", width, height, len(p), expected)
|
||||||
|
}
|
||||||
|
return &NRGB{
|
||||||
|
Pix: p,
|
||||||
|
Stride: bpp * width,
|
||||||
|
Rect: image.Rectangle{image.Point{left, top}, image.Point{left + width, top + height}},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ func (self *Context) run_paste(src Scanner, background image.Image, pos image.Po
|
|||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("Unsupported image type: %v", v))
|
panic(fmt.Sprintf("Unsupported image type: %v", v))
|
||||||
}
|
}
|
||||||
self.Parallel(interRect.Min.Y, interRect.Max.Y, func(ys <-chan int) {
|
if err := self.SafeParallel(interRect.Min.Y, interRect.Max.Y, func(ys <-chan int) {
|
||||||
for y := range ys {
|
for y := range ys {
|
||||||
x1 := interRect.Min.X - pasteRect.Min.X
|
x1 := interRect.Min.X - pasteRect.Min.X
|
||||||
x2 := interRect.Max.X - pasteRect.Min.X
|
x2 := interRect.Max.X - pasteRect.Min.X
|
||||||
@@ -333,7 +333,9 @@ func (self *Context) run_paste(src Scanner, background image.Image, pos image.Po
|
|||||||
src.scan(x1, y1, x2, y2, dst)
|
src.scan(x1, y1, x2, y2, dst)
|
||||||
postprocess(dst)
|
postprocess(dst)
|
||||||
}
|
}
|
||||||
})
|
}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,3 +405,15 @@ func FitImage(width, height, pwidth, pheight int) (final_width int, final_height
|
|||||||
|
|
||||||
return width, height
|
return width, height
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewNRGBAWithContiguousRGBAPixels(p []byte, left, top, width, height int) (*image.NRGBA, error) {
|
||||||
|
const bpp = 4
|
||||||
|
if expected := bpp * width * height; expected != len(p) {
|
||||||
|
return nil, fmt.Errorf("the image width and height dont match the size of the specified pixel data: width=%d height=%d sz=%d != %d", width, height, len(p), expected)
|
||||||
|
}
|
||||||
|
return &image.NRGBA{
|
||||||
|
Pix: p,
|
||||||
|
Stride: bpp * width,
|
||||||
|
Rect: image.Rectangle{image.Point{left, top}, image.Point{left + width, left + height}},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,18 +27,20 @@ func reverse_row(bytes_per_pixel int, pix []uint8) {
|
|||||||
|
|
||||||
func (self *Context) FlipPixelsH(bytes_per_pixel, width, height int, pix []uint8) {
|
func (self *Context) FlipPixelsH(bytes_per_pixel, width, height int, pix []uint8) {
|
||||||
stride := bytes_per_pixel * width
|
stride := bytes_per_pixel * width
|
||||||
self.Parallel(0, height, func(ys <-chan int) {
|
if err := self.SafeParallel(0, height, func(ys <-chan int) {
|
||||||
for y := range ys {
|
for y := range ys {
|
||||||
i := y * stride
|
i := y * stride
|
||||||
reverse_row(bytes_per_pixel, pix[i:i+stride])
|
reverse_row(bytes_per_pixel, pix[i:i+stride])
|
||||||
}
|
}
|
||||||
})
|
}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Context) FlipPixelsV(bytes_per_pixel, width, height int, pix []uint8) {
|
func (self *Context) FlipPixelsV(bytes_per_pixel, width, height int, pix []uint8) {
|
||||||
stride := bytes_per_pixel * width
|
stride := bytes_per_pixel * width
|
||||||
num := height / 2
|
num := height / 2
|
||||||
self.Parallel(0, num, func(ys <-chan int) {
|
if err := self.SafeParallel(0, num, func(ys <-chan int) {
|
||||||
for y := range ys {
|
for y := range ys {
|
||||||
upper := y
|
upper := y
|
||||||
lower := height - 1 - y
|
lower := height - 1 - y
|
||||||
@@ -50,6 +52,7 @@ func (self *Context) FlipPixelsV(bytes_per_pixel, width, height int, pix []uint8
|
|||||||
as[i], bs[i] = bs[i], as[i]
|
as[i], bs[i] = bs[i], as[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/kovidgoyal/kitty/tools/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ = fmt.Print
|
var _ = fmt.Print
|
||||||
@@ -31,8 +33,10 @@ func (self *Context) EffectiveNumberOfThreads() int {
|
|||||||
return ans
|
return ans
|
||||||
}
|
}
|
||||||
|
|
||||||
// parallel processes the data in separate goroutines.
|
// parallel processes the data in separate goroutines. If any of them panics,
|
||||||
func (self *Context) Parallel(start, stop int, fn func(<-chan int)) {
|
// returns an error. Note that if multiple goroutines panic, only one error is
|
||||||
|
// returned.
|
||||||
|
func (self *Context) SafeParallel(start, stop int, fn func(<-chan int)) (err error) {
|
||||||
count := stop - start
|
count := stop - start
|
||||||
if count < 1 {
|
if count < 1 {
|
||||||
return
|
return
|
||||||
@@ -49,9 +53,16 @@ func (self *Context) Parallel(start, stop int, fn func(<-chan int)) {
|
|||||||
for range procs {
|
for range procs {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
text, _ := utils.Format_stacktrace_on_panic(r)
|
||||||
|
err = fmt.Errorf("%s", text)
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
fn(c)
|
fn(c)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,8 +73,16 @@ func Format_stacktrace_on_panic(r any) (text string, err error) {
|
|||||||
n := runtime.Callers(3, pcs)
|
n := runtime.Callers(3, pcs)
|
||||||
lines := []string{}
|
lines := []string{}
|
||||||
frames := runtime.CallersFrames(pcs[:n])
|
frames := runtime.CallersFrames(pcs[:n])
|
||||||
err = fmt.Errorf("Panicked: %s", r)
|
rt := fmt.Sprint(r)
|
||||||
lines = append(lines, fmt.Sprintf("\r\nPanicked with error: %s\r\nStacktrace (most recent call first):\r\n", r))
|
if strings.HasPrefix(rt, "Panicked with error:") {
|
||||||
|
err = fmt.Errorf("%s", rt)
|
||||||
|
lines = append(lines, "Panic caused by previous panic (probably in a gouroutine). Previous panic:\r\n")
|
||||||
|
lines = append(lines, rt)
|
||||||
|
lines = append(lines, "\r\n\r\nStacktrace of current panic (most recent call first):\r\n")
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("Panicked: %s", r)
|
||||||
|
lines = append(lines, fmt.Sprintf("\r\nPanicked with error: %s\r\nStacktrace (most recent call first):\r\n", r))
|
||||||
|
}
|
||||||
found_first_frame := false
|
found_first_frame := false
|
||||||
for frame, more := frames.Next(); more; frame, more = frames.Next() {
|
for frame, more := frames.Next(); more; frame, more = frames.Next() {
|
||||||
if !found_first_frame {
|
if !found_first_frame {
|
||||||
|
|||||||
Reference in New Issue
Block a user