mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-16 21:47:47 +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
|
||||
|
||||
If you live in the terminal, *kitty* is made for **you**!
|
||||
|
||||
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"]
|
||||
|
||||
@@ -30,10 +30,10 @@
|
||||
},
|
||||
|
||||
{
|
||||
"name": "openssl 3.5.2",
|
||||
"name": "openssl 3.5.4",
|
||||
"unix": {
|
||||
"file_extension": "tar.gz",
|
||||
"hash": "sha256:c53a47e5e441c930c3928cf7bf6fb00e5d129b630e0aa873b08258656e7345ec",
|
||||
"hash": "sha256:967311f84955316969bdb1d8d4b983718ef42338639c621ec4c34fddef355e99",
|
||||
"urls": ["https://www.openssl.org/source/{filename}"]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -134,6 +134,29 @@ consumption to do the same tasks.
|
||||
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]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
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,
|
||||
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
|
||||
mapping to :file:`kitty.conf`::
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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::
|
||||
:hidden:
|
||||
@@ -20,7 +22,7 @@ kitty
|
||||
|
||||
.. 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>`
|
||||
* Performance tradeoffs can be :ref:`tuned <conf-kitty-performance>`
|
||||
|
||||
|
||||
@@ -122,9 +122,9 @@ the table below:
|
||||
.. table:: Types of input to kittens
|
||||
:align: left
|
||||
|
||||
=========================== =======================================================================================================
|
||||
=========================== ========================================================================================
|
||||
Keyword Type of :file:`STDIN` input
|
||||
=========================== =======================================================================================================
|
||||
=========================== ========================================================================================
|
||||
``text`` Plain text of active window
|
||||
``ansi`` Formatted text of active window
|
||||
``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
|
||||
|
||||
``selection`` The text currently selected with the mouse
|
||||
=========================== =======================================================================================================
|
||||
=========================== ========================================================================================
|
||||
|
||||
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
|
||||
@@ -298,107 +298,6 @@ So if you run kitty from another kitty instance, the output will be visible
|
||||
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
|
||||
----------------------------------------------------------
|
||||
|
||||
|
||||
@@ -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
|
||||
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
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -262,6 +262,12 @@ as shown below:
|
||||
return True
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
The payloads for the different remote control commands are documented in the
|
||||
:doc:`remote control protocol specification <rc_protocol>`.
|
||||
|
||||
|
||||
.. _rc_mapping:
|
||||
|
||||
Mapping key presses to remote control commands
|
||||
|
||||
@@ -8,6 +8,9 @@ import (
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
//go:embed logo/kitty.png
|
||||
var KittyLogoAsPNGData []byte
|
||||
|
||||
//go:embed kitty_tests/GraphemeBreakTest.json
|
||||
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 KittyConfigDefaults = struct {{
|
||||
Term, Shell_integration, Select_by_word_characters, Url_excluded_characters, Shell string
|
||||
Wheel_scroll_multiplier int
|
||||
Wheel_scroll_multiplier float64
|
||||
Url_prefixes []string
|
||||
}}{{
|
||||
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;
|
||||
float xscale, yscale;
|
||||
int blur_radius;
|
||||
bool live_resize_in_progress;
|
||||
|
||||
// The total sum of the distances the cursor has been warped
|
||||
// since the last cursor motion event was processed
|
||||
|
||||
@@ -537,6 +537,7 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
|
||||
if (self != nil) {
|
||||
window = initWindow;
|
||||
_lastScreenStates = [self captureScreenStates];
|
||||
window->ns.live_resize_in_progress = false;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -566,7 +567,12 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
|
||||
(void)notification;
|
||||
NSArray<NSDictionary *> *currentScreenStates = [self captureScreenStates];
|
||||
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) {
|
||||
// This resize likely happened because a screen was added, removed, or changed resolution.
|
||||
[_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
|
||||
// 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
|
||||
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
|
||||
@@ -824,12 +831,14 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
|
||||
- (void) viewWillStartLiveResize
|
||||
{
|
||||
if (!window) return;
|
||||
window->ns.live_resize_in_progress = true;
|
||||
_glfwInputLiveResize(window, true);
|
||||
}
|
||||
|
||||
- (void)viewDidEndLiveResize
|
||||
{
|
||||
if (!window) return;
|
||||
window->ns.live_resize_in_progress = false;
|
||||
_glfwInputLiveResize(window, false);
|
||||
}
|
||||
|
||||
|
||||
2
go.mod
2
go.mod
@@ -13,7 +13,7 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1
|
||||
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/shirou/gopsutil/v3 v3.24.5
|
||||
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/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/imaging v1.6.4 h1:K0idhRPXnRrJBKnBYcTfI1HTWSNDeAn7hYDvf9I0dCk=
|
||||
github.com/kovidgoyal/imaging v1.6.4/go.mod h1:bEIgsaZmXlvFfkv/CUxr9rJook6AQkJnpB5EPosRfRY=
|
||||
github.com/kovidgoyal/imaging v1.6.5 h1:Id9DKlz/ydl5Vxt9QG5IjGSiIcHcszSKXxDubdO49PQ=
|
||||
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/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
|
||||
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/tty"
|
||||
"github.com/kovidgoyal/kitty/tools/tui"
|
||||
"github.com/kovidgoyal/kitty/tools/tui/graphics"
|
||||
"github.com/kovidgoyal/kitty/tools/tui/loop"
|
||||
"github.com/kovidgoyal/kitty/tools/tui/readline"
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
@@ -123,6 +124,7 @@ type State struct {
|
||||
display_title bool
|
||||
pygments_style, dark_pygments_style string
|
||||
syntax_aliases map[string]string
|
||||
max_disk_cache_size int64
|
||||
|
||||
selections []string
|
||||
current_idx CollectionIndex
|
||||
@@ -131,6 +133,7 @@ type State struct {
|
||||
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) SyntaxAliases() map[string]string { return s.syntax_aliases }
|
||||
func (s State) DisplayTitle() bool { return s.display_title }
|
||||
@@ -204,16 +207,29 @@ type ScreenSize struct {
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
state State
|
||||
screen_size ScreenSize
|
||||
result_manager *ResultManager
|
||||
lp *loop.Loop
|
||||
rl *readline.Readline
|
||||
err_chan chan error
|
||||
shortcut_tracker config.ShortcutTracker
|
||||
msg_printer *message.Printer
|
||||
spinner *tui.Spinner
|
||||
preview_manager *PreviewManager
|
||||
state State
|
||||
screen_size ScreenSize
|
||||
result_manager *ResultManager
|
||||
lp *loop.Loop
|
||||
rl *readline.Readline
|
||||
err_chan chan error
|
||||
shortcut_tracker config.ShortcutTracker
|
||||
msg_printer *message.Printer
|
||||
spinner *tui.Spinner
|
||||
preview_manager *PreviewManager
|
||||
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) {
|
||||
@@ -222,15 +238,8 @@ func (h *Handler) draw_screen() (err error) {
|
||||
defer func() {
|
||||
h.state.mouse_state.UpdateHoveredIds()
|
||||
h.state.mouse_state.ApplyHoverStyles(h.lp)
|
||||
h.lp.EndAtomicUpdate()
|
||||
}()
|
||||
h.lp.ClearScreen()
|
||||
h.state.mouse_state.ClearCellRegions()
|
||||
switch h.state.screen {
|
||||
case NORMAL:
|
||||
matches, is_complete := h.get_results()
|
||||
h.lp.SetWindowTitle(h.state.WindowTitle())
|
||||
defer func() { // so that the cursor ends up in the right place
|
||||
h.graphics_handler.ApplyPlacements(h.lp)
|
||||
if h.state.screen == NORMAL { // so that the cursor ends up in the right place
|
||||
h.lp.MoveCursorTo(1, 1)
|
||||
if h.state.DisplayTitle() {
|
||||
h.lp.Println(h.state.WindowTitle())
|
||||
@@ -238,7 +247,16 @@ func (h *Handler) draw_screen() (err error) {
|
||||
} else {
|
||||
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)
|
||||
footer_height, err := h.draw_footer()
|
||||
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.draw_screen()
|
||||
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.dark_pygments_style = conf.Dark_pygments_style
|
||||
h.state.syntax_aliases = conf.Syntax_aliases
|
||||
h.state.max_disk_cache_size = int64(conf.Cache_size * (1024 * 1024 * 1024))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -775,6 +795,7 @@ func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) {
|
||||
lp.MouseTrackingMode(loop.FULL_MOUSE_TRACKING)
|
||||
lp.ColorSchemeChangeNotifications()
|
||||
handler := Handler{lp: lp, err_chan: make(chan error, 8), msg_printer: message.NewPrinter(utils.LanguageTag()), spinner: tui.NewSpinner("dots")}
|
||||
defer handler.graphics_handler.Cleanup()
|
||||
handler.rl = readline.New(lp, readline.RlInit{
|
||||
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()
|
||||
return handler.OnInitialize()
|
||||
}
|
||||
lp.OnFinalize = func() string {
|
||||
handler.graphics_handler.Finalize(lp)
|
||||
return ""
|
||||
}
|
||||
lp.OnResize = func(old, new_size loop.ScreenSize) (err error) {
|
||||
handler.init_sizes(new_size)
|
||||
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.OnText = handler.OnText
|
||||
lp.OnMouseEvent = handler.OnMouseEvent
|
||||
lp.OnEscapeCode = handler.on_escape_code
|
||||
lp.OnWakeup = func() (err error) {
|
||||
select {
|
||||
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
|
||||
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',
|
||||
long_text='''
|
||||
File extension aliases for syntax highlight. For example, to syntax highlight
|
||||
|
||||
@@ -25,6 +25,7 @@ var _ = fmt.Print
|
||||
type Preview interface {
|
||||
Render(h *Handler, x, y, width, height int)
|
||||
IsValidForColorScheme(light bool) bool
|
||||
Unload()
|
||||
}
|
||||
|
||||
type PreviewManager struct {
|
||||
@@ -80,6 +81,7 @@ type MessagePreview struct {
|
||||
|
||||
func (p MessagePreview) IsValidForColorScheme(bool) bool { return true }
|
||||
|
||||
func (p MessagePreview) Unload() {}
|
||||
func (p MessagePreview) Render(h *Handler, x, y, width, height int) {
|
||||
offset := 0
|
||||
if p.title != "" {
|
||||
@@ -189,6 +191,8 @@ type TextFilePreview struct {
|
||||
|
||||
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) {
|
||||
if p.highlighted_chan != nil {
|
||||
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.light_style, s.dark_style = pm.settings.HighlightStyles()
|
||||
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)
|
||||
if err != nil {
|
||||
debugprintln(fmt.Sprintf("Failed to highlight: %s with error: %s", path, err))
|
||||
}
|
||||
output <- highlighed_data{text: highlighted, err: err, light: s.light}
|
||||
close(output)
|
||||
pm.WakeupMainThread()
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -297,6 +307,14 @@ func (pm *PreviewManager) preview_for(abspath string, ftype fs.FileMode) (ans Pr
|
||||
pm.highlight_file_async(abspath, ch)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -308,9 +326,14 @@ func (h *Handler) draw_preview_content(x, y, width, height int) {
|
||||
return
|
||||
}
|
||||
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 {
|
||||
h.render_wrapped_text_in_region("No preview available", x, y, width, height, false)
|
||||
} else {
|
||||
h.last_rendered_preview = p
|
||||
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 {
|
||||
col_width = BASE_COL_WIDTH
|
||||
num_cols = available_width / col_width
|
||||
for num_cols > 0 && height*(num_cols-1) >= num_matches {
|
||||
num_cols = max(1, available_width/col_width)
|
||||
for num_cols > 1 && height*(num_cols-1) >= num_matches {
|
||||
num_cols--
|
||||
}
|
||||
col_width = available_width / num_cols
|
||||
|
||||
@@ -634,6 +634,7 @@ type Settings interface {
|
||||
GlobalIgnores() ignorefiles.IgnoreFile
|
||||
HighlightStyles() (string, string)
|
||||
SyntaxAliases() map[string]string
|
||||
DiskCacheSize() int64
|
||||
}
|
||||
|
||||
type ResultManager struct {
|
||||
|
||||
@@ -32,7 +32,7 @@ var highlighter = sync.OnceValue(func() highlight.Highlighter {
|
||||
func highlight_all(paths []string, light bool) {
|
||||
ctx := images.Context{}
|
||||
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 {
|
||||
path := paths[i]
|
||||
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))
|
||||
}
|
||||
}
|
||||
})
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ package diff
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -20,7 +21,7 @@ import (
|
||||
var _ = fmt.Print
|
||||
|
||||
type KittyOpts struct {
|
||||
Wheel_scroll_multiplier int
|
||||
Wheel_scroll_multiplier float64
|
||||
Copy_on_select bool
|
||||
}
|
||||
|
||||
@@ -29,7 +30,7 @@ func read_relevant_kitty_opts() KittyOpts {
|
||||
handle_line := func(key, val string) error {
|
||||
switch key {
|
||||
case "wheel_scroll_multiplier":
|
||||
v, err := strconv.Atoi(val)
|
||||
v, err := strconv.ParseFloat(val, 64)
|
||||
if err == nil {
|
||||
ans.Wheel_scroll_multiplier = v
|
||||
}
|
||||
@@ -47,7 +48,10 @@ var RelevantKittyOpts = sync.OnceValue(func() KittyOpts {
|
||||
})
|
||||
|
||||
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 {
|
||||
amt *= -1
|
||||
}
|
||||
|
||||
@@ -358,14 +358,16 @@ func diff(jobs []diff_job, context_count int) (ans map[string]*Patch, err error)
|
||||
patch *Patch
|
||||
}
|
||||
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 {
|
||||
job := jobs[i]
|
||||
r := result{file1: job.file1, file2: job.file2}
|
||||
r.patch, r.err = do_diff(job.file1, job.file2, context_count)
|
||||
results <- r
|
||||
}
|
||||
})
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
close(results)
|
||||
for r := range results {
|
||||
if r.err != nil {
|
||||
|
||||
@@ -106,7 +106,7 @@ func (self *Search) search(logical_lines *LogicalLines) {
|
||||
self.matches = make(map[ScrollPos][]Span)
|
||||
ctx := images.Context{}
|
||||
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 {
|
||||
line := logical_lines.At(i)
|
||||
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 {
|
||||
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)
|
||||
if err != nil {
|
||||
report_error(arg.value, "Could not render image to RGB", err)
|
||||
return
|
||||
merr := render_image_with_magick(&imgd, &f)
|
||||
if merr != nil {
|
||||
report_error(arg.value, "Could not render image to RGB", err)
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
}
|
||||
} else {
|
||||
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]);
|
||||
}
|
||||
|
||||
#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) {
|
||||
// Convert a cell color to an actual color based on the color table
|
||||
int t = int(c & BYTE_MASK);
|
||||
uint is_one = uint(one_if_equal_zero_otherwise(t, 1));
|
||||
uint is_two = uint(one_if_equal_zero_otherwise(t, 2));
|
||||
uint is_one = one_if_equal_zero_otherwise(t, 1);
|
||||
uint is_two = one_if_equal_zero_otherwise(t, 2);
|
||||
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;
|
||||
}
|
||||
@@ -94,7 +98,7 @@ vec3 to_color(uint c, uint defval) {
|
||||
|
||||
vec3 resolve_dynamic_color(uint c, vec3 special_val, vec3 defval) {
|
||||
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 (
|
||||
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)
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
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_y = clamp(y, cursor_y1, cursor_y2);
|
||||
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);
|
||||
#endif
|
||||
// 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_uses_main_cursor_shape = float((is_selected >> 4) & BIT_MASK);
|
||||
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':
|
||||
return 'uint64', 'strconv.ParseUint(val, 10, 64)'
|
||||
if p == 'positive_float':
|
||||
return 'float64', 'config.PositiveFloat(val, 10, 64)'
|
||||
return 'float64', 'config.PositiveFloat(val)'
|
||||
if p == 'unit_float':
|
||||
return 'float64', 'config.UnitFloat(val)'
|
||||
if p == 'python_string':
|
||||
|
||||
@@ -28,5 +28,9 @@ static inline void* disk_cache_malloc_allocator(void *x, size_t sz) {
|
||||
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) {
|
||||
*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) {
|
||||
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
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
|
||||
free_texture(GLuint *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);
|
||||
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 check_framebuffer_status_or_die(void);
|
||||
const char* check_framebuffer_status(void);
|
||||
void restore_viewport(void);
|
||||
void bind_framebuffer_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:
|
||||
groups = tuple(all_windows.iter_all_layoutable_groups())
|
||||
window_count = len(groups)
|
||||
root = self.pairs_root
|
||||
all_present_window_ids = frozenset(w.id for w in groups)
|
||||
already_placed_window_ids = frozenset(root.all_window_ids())
|
||||
windows_to_remove = already_placed_window_ids - all_present_window_ids
|
||||
if windows_to_remove:
|
||||
self.remove_windows(*windows_to_remove)
|
||||
id_window_map = {w.id: w for w in groups}
|
||||
id_idx_map = {w.id: i for i, w in enumerate(groups)}
|
||||
windows_to_add = all_present_window_ids - already_placed_window_ids
|
||||
if windows_to_add:
|
||||
for wid in sorted(windows_to_add, key=id_idx_map.__getitem__):
|
||||
root.balanced_add(wid)
|
||||
all_present_group_ids = {g.id for g in groups}
|
||||
already_placed_group_ids = frozenset(root.all_window_ids())
|
||||
if groups_to_remove := already_placed_group_ids - all_present_group_ids:
|
||||
self.remove_windows(*groups_to_remove)
|
||||
if groups_to_add := all_present_group_ids - already_placed_group_ids:
|
||||
id_idx_map = {g.id: i for i, g in enumerate(groups)}
|
||||
for gid in sorted(groups_to_add, key=id_idx_map.__getitem__):
|
||||
root.balanced_add(gid)
|
||||
|
||||
if window_count == 1:
|
||||
if len(groups) == 1:
|
||||
self.layout_single_window_group(groups[0])
|
||||
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(
|
||||
self,
|
||||
@@ -563,7 +560,9 @@ class Splits(Layout):
|
||||
parent_pair.bias = bias if parent_pair.one == target_group.id else (1 - bias)
|
||||
return
|
||||
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:
|
||||
p.bias = bias
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ from .utils import (
|
||||
get_custom_window_icon,
|
||||
log_error,
|
||||
parse_os_window_state,
|
||||
read_shell_environment,
|
||||
safe_mtime,
|
||||
startup_notification_handler,
|
||||
)
|
||||
@@ -478,6 +479,17 @@ def setup_environment(opts: Options, cli_opts: CLIOptions) -> None:
|
||||
from_config_file = True
|
||||
if cli_opts.listen_on:
|
||||
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()
|
||||
ensure_kitty_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',
|
||||
add_to_default=False,
|
||||
long_text='''
|
||||
@@ -3268,8 +3270,17 @@ recursively, for example::
|
||||
env VAR2=${HOME}/${VAR1}/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='''
|
||||
Specify rules to filter out notifications sent by applications running in kitty.
|
||||
|
||||
@@ -82,8 +82,7 @@ CmdGenerator = Iterator[CmdReturnType]
|
||||
PayloadType = Optional[Union[CmdReturnType, CmdGenerator]]
|
||||
PayloadGetType = PayloadGetter
|
||||
ArgsType = list[str]
|
||||
ImageCompletion = CompletionSpec.from_string('type:file group:"Images"')._replace(
|
||||
extensions=('png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'tiff'))
|
||||
ImageCompletion = CompletionSpec.from_string('type:file group:"Images" ext:png,jpg,jpeg,webp,gif,bmp,tiff')
|
||||
SUPPORTED_IMAGE_FORMATS = tuple(x.upper() for x in ImageCompletion.extensions if x != 'jpg')
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
from collections.abc import Callable, Generator, Iterator, Mapping
|
||||
@@ -64,7 +65,7 @@ class Tab:
|
||||
self.pending_resize_spec: ResizeSpec | None = None
|
||||
self.pending_focus_matching_window: str = ''
|
||||
self.name = name.strip()
|
||||
self.active_window_idx = 0
|
||||
self.active_window_idx = -1
|
||||
self.enabled_layouts = opts.enabled_layouts
|
||||
self.layout = (self.enabled_layouts or ['tall'])[0]
|
||||
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
|
||||
|
||||
|
||||
|
||||
def goto_session_options() -> str:
|
||||
return '''
|
||||
--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:
|
||||
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:
|
||||
opts, cmdline = parse_goto_session_cmdline(list(cmdline))
|
||||
except Exception as e:
|
||||
@@ -539,28 +555,13 @@ def goto_session(boss: BossType, cmdline: Sequence[str]) -> None:
|
||||
choose_session(boss, opts)
|
||||
return
|
||||
path = cmdline[0]
|
||||
if len(cmdline) == 1:
|
||||
if len(cmdline) == 1: # goto_session -- -1
|
||||
try:
|
||||
idx = int(path)
|
||||
except Exception:
|
||||
idx = 0
|
||||
if idx < 0:
|
||||
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()
|
||||
return
|
||||
else:
|
||||
for x in cmdline:
|
||||
if not x.startswith('-'):
|
||||
path = x
|
||||
break
|
||||
return goto_previous_session(boss, idx)
|
||||
path, session_name = resolve_session_path_and_name(path)
|
||||
if not session_name:
|
||||
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);
|
||||
// We use GL_RGBA16 to avoid incorrect colors due to quantization loss when
|
||||
// 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);
|
||||
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
|
||||
@@ -1241,14 +1261,13 @@ draw_cursor_trail(CursorTrail *trail, Window *active_window) {
|
||||
|
||||
// OSWindow {{{
|
||||
static void
|
||||
draw_bg_image(OSWindow *os_window) {
|
||||
draw_bg_image(OSWindow *os_window, Tab *tab) {
|
||||
if (!has_bgimage(os_window)) return;
|
||||
BackgroundImageRenderSettings s = {
|
||||
.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),
|
||||
.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 vwidth = s.os_window.width, vheight = s.os_window.height;
|
||||
if (CENTER_SCALED == OPT(background_image_layout)) {
|
||||
@@ -1278,6 +1297,9 @@ draw_bg_image(OSWindow *os_window) {
|
||||
bottom += hfrac;
|
||||
} 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);
|
||||
glUniform1f(bgimage_program_layout.uniforms.tiled, tiled);
|
||||
glUniform4f(bgimage_program_layout.uniforms.positions, left, top, right, bottom);
|
||||
@@ -1339,7 +1361,7 @@ blank_os_window(OSWindow *osw) {
|
||||
}
|
||||
|
||||
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) {
|
||||
blank_os_window(os_window);
|
||||
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);
|
||||
bind_framebuffer_for_output(0);
|
||||
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
|
||||
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);
|
||||
}
|
||||
// }}}
|
||||
|
||||
@@ -251,10 +251,14 @@ class Tab: # {{{
|
||||
def _startup(self, session_tab: SessionTab) -> None:
|
||||
target_tab = self
|
||||
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
|
||||
launched_window: Window | None = None
|
||||
if isinstance(spec, SpecialWindowInstance):
|
||||
self.new_special_window(spec)
|
||||
launched_window = self.new_special_window(spec)
|
||||
else:
|
||||
from .launch import launch
|
||||
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)
|
||||
if launched_window is not None:
|
||||
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:
|
||||
self.resize_window(*window.resize_spec)
|
||||
if window.focus_matching_window_spec:
|
||||
@@ -275,6 +285,8 @@ class Tab: # {{{
|
||||
):
|
||||
tab = w.tabref()
|
||||
if tab:
|
||||
did_focus_matching_spec = True
|
||||
active_window_id = 0
|
||||
target_tab = tab or self
|
||||
tm = tab.tab_manager_ref()
|
||||
if tm and boss.active_tab is not target_tab:
|
||||
@@ -283,8 +295,10 @@ class Tab: # {{{
|
||||
target_tab.set_active_window(w)
|
||||
boss.focus_os_window(w.os_window_id)
|
||||
|
||||
with suppress(IndexError):
|
||||
self.windows.set_active_window_group_for(self.windows.all_windows[session_tab.active_window_idx])
|
||||
if not did_focus_matching_spec and not active_window_id:
|
||||
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:
|
||||
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:
|
||||
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 len(self.tabs) == 0:
|
||||
self._active_tab_idx = 0
|
||||
elif len(self.tabs) == 1:
|
||||
remove_from_end_of_active_history(self.tabs[0])
|
||||
self._set_active_tab(0, store_in_history=False)
|
||||
if len(tabs) == 0 or (len(tabs) == 1 and removed_tab is tabs[0]):
|
||||
tab = previous_active_tab()
|
||||
if tab is None:
|
||||
self._active_tab_idx = 0
|
||||
else:
|
||||
self._set_active_tab(self.tabs.index(tab), store_in_history=False)
|
||||
else:
|
||||
next_active_tab: Tab | None = None
|
||||
match get_options().tab_switch_strategy:
|
||||
@@ -1387,6 +1410,8 @@ class TabManager: # {{{
|
||||
while self.active_tab_history and next_active_tab is None:
|
||||
tab_id = self.active_tab_history.pop()
|
||||
next_active_tab = self.tab_for_id(tab_id)
|
||||
if next_active_tab not in tabs:
|
||||
next_active_tab = None
|
||||
case 'left':
|
||||
next_active_tab = tabs[(tabs.index(active_tab_before_removal) - 1 + len(tabs)) % len(tabs)]
|
||||
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 functools import lru_cache
|
||||
from re import Match, Pattern
|
||||
from types import MappingProxyType
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
BinaryIO,
|
||||
NamedTuple,
|
||||
NoReturn,
|
||||
Optional,
|
||||
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]:
|
||||
editor = get_editor_from_env(os.environ)
|
||||
from .child import default_env
|
||||
editor = get_editor_from_env(default_env())
|
||||
if not editor:
|
||||
shell_env = read_shell_environment(opts)
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
if not path:
|
||||
return False
|
||||
@@ -743,62 +755,72 @@ def which(name: str, only_system: bool = False) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def read_shell_environment(opts: Options | None = None) -> dict[str, str]:
|
||||
ans: dict[str, str] | None = getattr(read_shell_environment, 'ans', None)
|
||||
if ans is None:
|
||||
from .child import openpty
|
||||
ans = {}
|
||||
setattr(read_shell_environment, 'ans', ans)
|
||||
import subprocess
|
||||
shell = resolved_shell(opts)
|
||||
master, slave = openpty()
|
||||
os.set_blocking(master, False)
|
||||
if '-l' not in shell and '--login' not in shell:
|
||||
shell += ['-l']
|
||||
if '-i' not in shell and '--interactive' not in shell:
|
||||
shell += ['-i']
|
||||
try:
|
||||
p = subprocess.Popen(
|
||||
shell + ['-c', 'env'], stdout=slave, stdin=slave, stderr=slave, start_new_session=True, close_fds=True,
|
||||
preexec_fn=clear_handled_signals)
|
||||
except FileNotFoundError:
|
||||
log_error('Could not find shell to read environment')
|
||||
return ans
|
||||
with os.fdopen(master, 'rb') as stdout, os.fdopen(slave, 'wb'):
|
||||
raw = b''
|
||||
from time import monotonic
|
||||
start_time = monotonic()
|
||||
while monotonic() - start_time < 1.5:
|
||||
@lru_cache(4)
|
||||
def read_resolved_shell_environment(shell: tuple[str, ...]) -> MappingProxyType[str, str]:
|
||||
import subprocess
|
||||
cmdline = list(shell)
|
||||
if '-l' not in cmdline and '--login' not in cmdline:
|
||||
cmdline += ['-l']
|
||||
if '-i' not in cmdline and '--interactive' not in cmdline:
|
||||
cmdline += ['-i']
|
||||
q = os.path.basename(cmdline[0]).lower()
|
||||
has_builtin = q in ('bash', 'zsh')
|
||||
cmd = 'builtin command env -0' if has_builtin else 'command env -0'
|
||||
ans: MappingProxyType[str, str] = MappingProxyType({})
|
||||
|
||||
from .child import openpty
|
||||
master, slave = openpty()
|
||||
os.set_blocking(master, False)
|
||||
try:
|
||||
p = subprocess.Popen(
|
||||
cmdline + ['-c', cmd], stdout=slave, stdin=slave, stderr=slave, start_new_session=True, close_fds=True,
|
||||
preexec_fn=clear_handled_signals)
|
||||
except FileNotFoundError:
|
||||
log_error(f'Could not find shell {cmdline[0]} to read environment')
|
||||
return ans
|
||||
with os.fdopen(master, 'rb') as stdout, os.fdopen(slave, 'wb'):
|
||||
raw = b''
|
||||
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:
|
||||
ret: int | None = p.wait(0.01)
|
||||
except subprocess.TimeoutExpired:
|
||||
ret = None
|
||||
with suppress(Exception):
|
||||
raw += stdout.read()
|
||||
if ret is not None:
|
||||
x = stdout.read()
|
||||
except Exception:
|
||||
break
|
||||
if cast(Optional[int], p.returncode) is None:
|
||||
log_error('Timed out waiting for shell to quit while reading shell environment')
|
||||
p.kill()
|
||||
elif p.returncode == 0:
|
||||
while True:
|
||||
try:
|
||||
x = stdout.read()
|
||||
except Exception:
|
||||
break
|
||||
if not x:
|
||||
break
|
||||
raw += x
|
||||
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')
|
||||
if not x:
|
||||
break
|
||||
raw += x
|
||||
draw = raw.decode('utf-8', 'replace')
|
||||
env = {}
|
||||
for line in draw.split('\0'):
|
||||
k, sep, v = line.partition('=')
|
||||
if k and v and sep:
|
||||
env[k] = v
|
||||
ans = MappingProxyType(env)
|
||||
else:
|
||||
log_error(f'Failed to run shell {cmdline} to read its environment')
|
||||
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]:
|
||||
' Get paths from file:// URLs '
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
@@ -718,12 +718,12 @@ class Window:
|
||||
self.title_stack: Deque[str] = deque(maxlen=10)
|
||||
self.user_vars: dict[str, str] = {}
|
||||
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.margin = EdgeWidths()
|
||||
self.padding = EdgeWidths()
|
||||
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.os_window_id = tab.os_window_id
|
||||
self.tabref: Callable[[], TabType | None] = weakref.ref(tab)
|
||||
|
||||
@@ -33,6 +33,9 @@ class WindowGroup:
|
||||
self.windows: list[WindowType] = []
|
||||
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:
|
||||
return len(self.windows)
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ type Entry struct {
|
||||
|
||||
type Metadata struct {
|
||||
TotalSize int64
|
||||
PathMap map[string]string
|
||||
SortedEntries []*Entry
|
||||
}
|
||||
|
||||
@@ -24,35 +25,55 @@ type DiskCache struct {
|
||||
Path string
|
||||
MaxSize int64
|
||||
|
||||
lock_file *os.File
|
||||
lock_mutex sync.Mutex
|
||||
entries Metadata
|
||||
entry_map map[string]*Entry
|
||||
entries_mod_time time.Time
|
||||
get_dir string
|
||||
lock_file *os.File
|
||||
lock_mutex sync.Mutex
|
||||
entries Metadata
|
||||
entry_map map[string]*Entry
|
||||
entries_last_read_state *file_state
|
||||
entries_dirty bool
|
||||
get_dir string
|
||||
read_count int
|
||||
}
|
||||
|
||||
func NewDiskCache(path string, max_size int64) (dc *DiskCache, err error) {
|
||||
return new_disk_cache(path, max_size)
|
||||
}
|
||||
|
||||
func KeyForPath(path string) (key string, err error) {
|
||||
return key_for_path(path)
|
||||
func (dc *DiskCache) ResultsDir() string {
|
||||
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()
|
||||
defer dc.unlock()
|
||||
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) {
|
||||
dc.lock()
|
||||
defer dc.unlock()
|
||||
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()
|
||||
defer dc.unlock()
|
||||
return dc.add(key, items)
|
||||
|
||||
@@ -5,9 +5,12 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
@@ -15,6 +18,48 @@ import (
|
||||
|
||||
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) {
|
||||
if path, err = filepath.Abs(path); err != nil {
|
||||
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 {
|
||||
return
|
||||
}
|
||||
if pruned, err := ans.prune(); err != nil {
|
||||
return nil, err
|
||||
} else if pruned {
|
||||
if err = ans.write_entries(); err != nil {
|
||||
return nil, err
|
||||
defer func() {
|
||||
if we := ans.write_entries_if_dirty(); we != nil && err == nil {
|
||||
err = we
|
||||
}
|
||||
}()
|
||||
if _, err := ans.prune(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ans.get_dir, err = os.MkdirTemp(ans.Path, "getdir-*"); err != nil {
|
||||
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) 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 {
|
||||
return err
|
||||
} 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 {
|
||||
return err
|
||||
return 0, nil, nil, err
|
||||
} else {
|
||||
ans := make(map[string]*Entry)
|
||||
ans = make(map[string]*Entry)
|
||||
var total int64
|
||||
for _, x := range entries {
|
||||
if x.IsDir() {
|
||||
@@ -105,7 +182,7 @@ func (dc *DiskCache) rebuild_entries() error {
|
||||
key := sub_entries[0].Name()
|
||||
path := dc.folder_for_key(key)
|
||||
if file_entries, err := os.ReadDir(path); err == nil {
|
||||
e := Entry{}
|
||||
e := Entry{Key: key}
|
||||
for _, f := range file_entries {
|
||||
if fi, err := f.Info(); err == nil {
|
||||
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 {
|
||||
return a.LastUsed.Compare(b.LastUsed)
|
||||
})
|
||||
dc.entries = Metadata{TotalSize: total, SortedEntries: sorted}
|
||||
dc.entry_map = ans
|
||||
return total, ans, sorted, nil
|
||||
}
|
||||
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 {
|
||||
needed := dc.entry_map == nil
|
||||
needed := dc.entry_map == nil || dc.entries_last_read_state == nil
|
||||
path := dc.entries_path()
|
||||
var fstate *file_state
|
||||
if !needed {
|
||||
if s, err := os.Stat(path); err == nil && s.ModTime().After(dc.entries_mod_time) {
|
||||
needed = true
|
||||
if s, err := get_file_state_from_path(path); err == nil {
|
||||
fstate = s
|
||||
if !s.equal(dc.entries_last_read_state) {
|
||||
needed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if needed {
|
||||
if data, err := os.ReadFile(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
} 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 {
|
||||
// corrupted data
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
||||
func (dc *DiskCache) update_last_used(key string) {
|
||||
if dc.ensure_entries() == nil {
|
||||
dc.update_timestamp(key)
|
||||
func (dc *DiskCache) export_to_get_dir(key, path string) (string, error) {
|
||||
dest := filepath.Join(dc.get_dir, key+"-"+filepath.Base(path))
|
||||
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 {
|
||||
ans := make(map[string]string, len(items))
|
||||
base := dc.folder_for_key(key)
|
||||
if s, err := os.Stat(base); err != nil || !s.IsDir() {
|
||||
return ans
|
||||
func (dc *DiskCache) get(key string, items []string) (map[string]string, error) {
|
||||
if err := dc.ensure_entries(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 {
|
||||
p := filepath.Join(base, x)
|
||||
if s, err := os.Stat(p); err != nil || s.IsDir() {
|
||||
continue
|
||||
}
|
||||
dest := filepath.Join(dc.get_dir, key+"-"+x)
|
||||
if err := os.Link(p, dest); err != nil {
|
||||
os.Remove(dest)
|
||||
if err := os.Link(p, dest); err != nil {
|
||||
dest = ""
|
||||
}
|
||||
}
|
||||
dest, _ := dc.export_to_get_dir(key, p)
|
||||
if dest != "" {
|
||||
ans[x] = dest
|
||||
}
|
||||
}
|
||||
dc.update_last_used(key)
|
||||
return ans
|
||||
if len(items) > 0 {
|
||||
dc.update_timestamp(key)
|
||||
}
|
||||
return ans, dc.write_entries_if_dirty()
|
||||
}
|
||||
|
||||
func (dc *DiskCache) remove(key 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
|
||||
}
|
||||
}()
|
||||
base := dc.folder_for_key(key)
|
||||
if err = os.RemoveAll(base); err == nil {
|
||||
t := dc.entry_map[key]
|
||||
@@ -214,7 +337,7 @@ func (dc *DiskCache) remove(key string) (err error) {
|
||||
delete(dc.entry_map, key)
|
||||
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 })
|
||||
return dc.write_entries()
|
||||
dc.entries_dirty = true
|
||||
}
|
||||
}
|
||||
return
|
||||
@@ -229,8 +352,10 @@ func (dc *DiskCache) prune() (bool, error) {
|
||||
if err := os.RemoveAll(base); err == nil {
|
||||
t := dc.entries.SortedEntries[0]
|
||||
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.SortedEntries = dc.entries.SortedEntries[1:]
|
||||
dc.entries_dirty = true
|
||||
} else {
|
||||
return false, err
|
||||
}
|
||||
@@ -244,6 +369,7 @@ func (dc *DiskCache) update_timestamp(key string) {
|
||||
idx := slices.Index(dc.entries.SortedEntries, t)
|
||||
copy(dc.entries.SortedEntries[idx:], dc.entries.SortedEntries[idx+1:])
|
||||
dc.entries.SortedEntries[len(dc.entries.SortedEntries)-1] = t
|
||||
dc.entries_dirty = true
|
||||
}
|
||||
|
||||
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 = max(0, t.Size)
|
||||
dc.entries.TotalSize += t.Size - old_size
|
||||
dc.entries_dirty = true
|
||||
dc.update_timestamp(key)
|
||||
dc.prune()
|
||||
return dc.write_entries()
|
||||
return dc.write_entries_if_dirty()
|
||||
}
|
||||
|
||||
func (dc *DiskCache) keys() (ans []string, err error) {
|
||||
if err = dc.ensure_entries(); err != nil {
|
||||
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 {
|
||||
return
|
||||
}
|
||||
base := dc.folder_for_key(key)
|
||||
if err = os.MkdirAll(base, 0o700); err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
var changed int64
|
||||
defer func() {
|
||||
@@ -284,22 +437,40 @@ func (dc *DiskCache) add(key string, items map[string][]byte) (err error) {
|
||||
err = e
|
||||
}
|
||||
}()
|
||||
ans = make(map[string]string, len(items))
|
||||
for x, data := range items {
|
||||
p := filepath.Join(base, x)
|
||||
var before int64
|
||||
exists := false
|
||||
if s, err := os.Stat(p); err == nil {
|
||||
before = s.Size()
|
||||
exists = true
|
||||
}
|
||||
if len(data) == 0 {
|
||||
if err = os.Remove(p); err != nil {
|
||||
return
|
||||
if exists {
|
||||
if err = os.Remove(p); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
}
|
||||
changed -= before
|
||||
}
|
||||
changed -= before
|
||||
} 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 {
|
||||
return
|
||||
}
|
||||
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
|
||||
|
||||
@@ -3,7 +3,8 @@ package disk_cache
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -23,16 +24,45 @@ func TestDiskCache(t *testing.T) {
|
||||
if err != nil {
|
||||
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) {
|
||||
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 {
|
||||
d, err := os.ReadFile(path)
|
||||
@@ -44,35 +74,78 @@ func TestDiskCache(t *testing.T) {
|
||||
if diff := cmp.Diff(expected, actual); diff != "" {
|
||||
t.Fatalf("Data for %s not equal: %s", key, diff)
|
||||
}
|
||||
ensure_entries()
|
||||
}
|
||||
}
|
||||
ak := func(keys ...string) {
|
||||
for _, x := range []*DiskCache{dc, dc2} {
|
||||
for i, x := range []*DiskCache{dc, dc2} {
|
||||
kk, err := x.keys()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
slices.Sort(kk)
|
||||
slices.Sort(keys)
|
||||
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"})
|
||||
dc.Add("k1", map[string][]byte{"3": []byte("ijk"), "4": []byte("lmo")})
|
||||
dc2.Add("k2", map[string][]byte{"1": []byte("123456789")})
|
||||
arc(1, 2) // the two gets cause two updates
|
||||
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"})
|
||||
if 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")
|
||||
dc.Add("k3", map[string][]byte{"1": []byte(strings.Repeat("a", int(dc.MaxSize)-10))})
|
||||
ak("k3", "k2")
|
||||
add(dc, "k3", map[string]string{"1": strings.Repeat("a", int(dc.MaxSize)-10)})
|
||||
arc(3, 6) // dc.Add() causes dc2 to read
|
||||
ak("k2", "k3")
|
||||
// check that creating a new disk cache prunes
|
||||
_, err = NewDiskCache(tdir, dc.MaxSize-8)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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() {
|
||||
lp.RecoverFromPanicInGoRoutine()
|
||||
dl_data.mutex.Lock()
|
||||
dl_data.download_started = true
|
||||
dl_data.mutex.Unlock()
|
||||
|
||||
@@ -136,14 +136,16 @@ func (self *ImageCollection) ResizeForPageSize(width, height int) {
|
||||
|
||||
ctx := images.Context{}
|
||||
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 {
|
||||
img := self.images[keys[i]]
|
||||
if img.src.loaded && img.err == nil {
|
||||
img.ResizeForPageSize(width, height)
|
||||
}
|
||||
}
|
||||
})
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *ImageCollection) DeleteAllVisiblePlacements(lp *loop.Loop) {
|
||||
@@ -294,7 +296,7 @@ func (self *ImageCollection) LoadAll() {
|
||||
defer self.mutex.Unlock()
|
||||
ctx := images.Context{}
|
||||
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 {
|
||||
img := all[i]
|
||||
if !img.src.loaded {
|
||||
@@ -305,7 +307,9 @@ func (self *ImageCollection) LoadAll() {
|
||||
img.src.loaded = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
text, _ := utils.Format_stacktrace_on_panic(r)
|
||||
err_channel <- fmt.Errorf("%s", text)
|
||||
}
|
||||
}()
|
||||
keep_going := true
|
||||
pipe_fd := int(pipe_r.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)
|
||||
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)
|
||||
n, err := read_ignoring_temporary_errors(term, buf)
|
||||
if n > 0 {
|
||||
|
||||
@@ -168,6 +168,12 @@ func write_to_tty(
|
||||
pipe_r *os.File, term *tty.Term,
|
||||
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
|
||||
defer func() {
|
||||
pipe_r.Close()
|
||||
|
||||
@@ -99,6 +99,9 @@ func find_matching_codepoints(prefix string) (ans mark_set) {
|
||||
ans.AddItems(marks...)
|
||||
}
|
||||
}
|
||||
if ans == nil {
|
||||
ans = utils.NewSet[uint16](0)
|
||||
}
|
||||
return ans
|
||||
}
|
||||
|
||||
@@ -107,11 +110,13 @@ func marks_for_query(query string) (ans mark_set) {
|
||||
prefixes := strings.Split(strings.ToLower(query), " ")
|
||||
results := make(chan mark_set, len(prefixes))
|
||||
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 {
|
||||
results <- find_matching_codepoints(prefixes[i])
|
||||
}
|
||||
})
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
close(results)
|
||||
for x := range results {
|
||||
if ans == nil {
|
||||
|
||||
@@ -5,8 +5,10 @@ import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/cli"
|
||||
@@ -17,9 +19,9 @@ var _ = fmt.Print
|
||||
|
||||
func encode_rgba(output io.Writer, img image.Image) (err error) {
|
||||
var final_img *image.NRGBA
|
||||
switch img.(type) {
|
||||
switch ti := img.(type) {
|
||||
case *image.NRGBA:
|
||||
final_img = img.(*image.NRGBA)
|
||||
final_img = ti
|
||||
default:
|
||||
b := img.Bounds()
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
root.AddSubCommand(&cli.Command{
|
||||
Name: "__convert_image__",
|
||||
@@ -82,6 +126,11 @@ func ConvertEntryPoint(root *cli.Command) {
|
||||
if _, err = io.Copy(buf, os.Stdin); err != nil {
|
||||
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 {
|
||||
rc = 1
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"image"
|
||||
"image/color"
|
||||
"image/gif"
|
||||
"image/png"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -53,6 +54,27 @@ type ImageFrame struct {
|
||||
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) {
|
||||
bytes_per_pixel := 4
|
||||
if self.Is_opaque {
|
||||
@@ -122,12 +144,82 @@ func (self *ImageFrame) Data() (ans []byte) {
|
||||
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 {
|
||||
Width, Height int
|
||||
Format_uppercase string
|
||||
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 {
|
||||
b := self.Img.Bounds()
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Native {{{
|
||||
func (frame *ImageFrame) set_delay(min_gap, delay int) {
|
||||
frame.Delay_ms = int32(max(min_gap, delay) * 10)
|
||||
if frame.Delay_ms == 0 {
|
||||
@@ -266,10 +336,36 @@ func OpenNativeImageFromReader(f io.ReadSeeker) (ans *ImageData, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// }}}
|
||||
|
||||
// ImageMagick {{{
|
||||
var MagickExe = sync.OnceValue(func() string {
|
||||
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) {
|
||||
if MagickExe() != "magick" {
|
||||
cmd = append([]string{MagickExe()}, cmd...)
|
||||
@@ -610,6 +706,8 @@ func OpenImageFromPathWithMagick(path string) (ans *ImageData, err error) {
|
||||
return ans, nil
|
||||
}
|
||||
|
||||
// }}}
|
||||
|
||||
func OpenImageFromPath(path string) (ans *ImageData, err error) {
|
||||
mt := utils.GuessMimeType(path)
|
||||
if DecodableImageTypes[mt] {
|
||||
@@ -620,7 +718,7 @@ func OpenImageFromPath(path string) (ans *ImageData, err error) {
|
||||
defer f.Close()
|
||||
ans, err = OpenNativeImageFromReader(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to load image at %#v with error: %w", path, err)
|
||||
return OpenImageFromPathWithMagick(path)
|
||||
}
|
||||
} else {
|
||||
return OpenImageFromPathWithMagick(path)
|
||||
|
||||
@@ -9,6 +9,35 @@ import (
|
||||
|
||||
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 {
|
||||
switch i := img.(type) {
|
||||
case *image.RGBA:
|
||||
@@ -30,7 +59,7 @@ func IsOpaque(img image.Image) bool {
|
||||
case *image.CMYK:
|
||||
return i.Opaque()
|
||||
case *image.Paletted:
|
||||
return i.Opaque()
|
||||
return paletted_is_opaque(i)
|
||||
case *image.Uniform:
|
||||
return i.Opaque()
|
||||
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
|
||||
b = uint32(c.B)
|
||||
b |= b << 8
|
||||
a = 65280 // ( 255 << 8 )
|
||||
a = 65535 // (255 << 8 | 255)
|
||||
return
|
||||
}
|
||||
|
||||
// NRGB is an in-memory image whose At method returns NRGBColor values.
|
||||
type NRGB struct {
|
||||
// Pix holds the image's pixels, in R, G, B, A order. The pixel at
|
||||
// (x, y) starts at Pix[(y-Rect.Min.Y)*Stride + (x-Rect.Min.X)*4].
|
||||
// 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)*3].
|
||||
Pix []uint8
|
||||
// Stride is the Pix stride (in bytes) between vertically adjacent pixels.
|
||||
Stride int
|
||||
@@ -45,17 +45,18 @@ func nrgbModel(c color.Color) color.Color {
|
||||
return c
|
||||
}
|
||||
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)}
|
||||
}
|
||||
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)
|
||||
@@ -73,14 +74,14 @@ func (p *NRGB) NRGBAt(x, y int) NRGBColor {
|
||||
return NRGBColor{}
|
||||
}
|
||||
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]}
|
||||
}
|
||||
|
||||
// PixOffset returns the index of the first element of Pix that corresponds to
|
||||
// the pixel at (x, y).
|
||||
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) {
|
||||
@@ -170,14 +171,17 @@ func newScannerRGB(img image.Image, opaque_base NRGBColor) *scanner_rgb {
|
||||
}
|
||||
if img, ok := img.(*image.Paletted); ok {
|
||||
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++ {
|
||||
r, g, b, a := img.Palette[i].RGBA()
|
||||
switch a {
|
||||
case 0:
|
||||
s.palette[i] = opaque_base
|
||||
case 0xffff:
|
||||
s.palette[i] = NRGBColor{uint8(r >> 8), uint8(g >> 8), uint8(b >> 8)}
|
||||
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]}
|
||||
}
|
||||
}
|
||||
@@ -425,3 +429,15 @@ func NewNRGB(r image.Rectangle) *NRGB {
|
||||
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:
|
||||
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 {
|
||||
x1 := interRect.Min.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)
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
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 {
|
||||
i := y * 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) {
|
||||
stride := bytes_per_pixel * width
|
||||
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 {
|
||||
upper := 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]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
@@ -31,8 +33,10 @@ func (self *Context) EffectiveNumberOfThreads() int {
|
||||
return ans
|
||||
}
|
||||
|
||||
// parallel processes the data in separate goroutines.
|
||||
func (self *Context) Parallel(start, stop int, fn func(<-chan int)) {
|
||||
// parallel processes the data in separate goroutines. If any of them panics,
|
||||
// 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
|
||||
if count < 1 {
|
||||
return
|
||||
@@ -49,9 +53,16 @@ func (self *Context) Parallel(start, stop int, fn func(<-chan int)) {
|
||||
for range procs {
|
||||
wg.Add(1)
|
||||
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)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -73,8 +73,16 @@ func Format_stacktrace_on_panic(r any) (text string, err error) {
|
||||
n := runtime.Callers(3, pcs)
|
||||
lines := []string{}
|
||||
frames := runtime.CallersFrames(pcs[:n])
|
||||
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))
|
||||
rt := fmt.Sprint(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
|
||||
for frame, more := frames.Next(); more; frame, more = frames.Next() {
|
||||
if !found_first_frame {
|
||||
|
||||
Reference in New Issue
Block a user