diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index d9a92048adc..5f74b1389cf 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -13,6 +13,8 @@ updates:
all-go-deps:
patterns:
- "*" # group all non-security update PRs
+ cooldown:
+ default-days: 7
- package-ecosystem: "github-actions"
directory: "/"
schedule:
@@ -21,3 +23,5 @@ updates:
actions:
patterns:
- "*"
+ cooldown:
+ default-days: 7
\ No newline at end of file
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 889636eea80..4eab20d795c 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -44,6 +44,7 @@ jobs:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
+ persist-credentials: false
- name: Install Go
if: matrix.language == 'c' || matrix.language == 'go'
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 295f4811afd..610fa93c66a 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -139,12 +139,35 @@ Detailed list of changes
- Add support for the `paste events protocol `__ (:iss:`9183`)
+- icat kitten: Add support for animated PNG and animated WebP, netPBM images, ICC color profiles and CCIP color space metadata to the builtin engine
+
+- icat kitten: Add a new flag :option:`kitty +kitten icat --fit` to control how images are scaled to fit the screen (:iss:`9201`)
+
+- icat kitten: The :option:`kitty +kitten icat --scale-up` flag now takes effect when not using :option:`kitty +kitten icat --place` as well
+
- Add a mappable action :ac:`copy_last_command_output` to copy the output of the last
command to the clipboard (:pull:`9185`)
- ssh kitten: Fix a bug where automatic login was not working (:iss:`9187`)
-- icat kitten: Add support for APNG, netPBM, ICC color profiles and CCIP metadata to the builtin engine
+- Graphics: Fix overwrite composition mode for animation frames not being honored
+
+- Automatic color scheme switching: Fix title bar and scroll bar colors not being updated (:iss:`9167`)
+
+- macOS: Fix cycle through OS windows only swapping between the two most recent
+ OS Windows. Also add a cycle through OS Windows backwards action.
+ (:iss:`9215`)
+
+- :ac:`goto_session`: allow specifying a directory to select a session file
+ from the directory (:pull:`9219`)
+
+- Have reloading config also reload the custom tab bar python modules (:disc:`9221`)
+
+- kitten @ ls: Also output the neighbors for every window (:disc:`9225`)
+
+- Have the :option:`kitty --start-as` flag be respected when used with
+ :option:`kitty --single-instance` (:iss:`9228`)
+
0.44.0 [2025-11-03]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -195,8 +218,6 @@ Detailed list of changes
- Wayland: Fix scrolling using some mouse wheels that produce "VALUE120" based
scroll events too fast on some compositors (:pull:`9128`)
-- Automatic color scheme switching: Fix title bar color not being updated (:iss:`9167`)
-
- Add support for Unicode 17
- Fix a regression in 0.43.0 that caused :opt:`tab_bar_margin_width` to be
diff --git a/docs/clipboard.rst b/docs/clipboard.rst
index 379f293d63c..0932e6c981b 100644
--- a/docs/clipboard.rst
+++ b/docs/clipboard.rst
@@ -180,6 +180,12 @@ free to request whatever MIME data it wants from the list of types.
The mode can be enabled using the standard DECSET or DECRST control sequences.
``CSI ? 5522 h`` to enable the mode. ``CSI ? 5522 l`` to disable the mode.
+The terminal *should* send a one time password with the list of mime
+types, as the ``pw`` key (base64 encoded). The application can then use this
+password to request data from the clipboard without needing a permission
+prompt. The human name *should* be set to ``Paste event`` (base64 encoded) when
+the application uses this one time password.
+
Detecting support for this protocol
-----------------------------------------
diff --git a/docs/integrations.rst b/docs/integrations.rst
index 957ffb51858..f1098d8d401 100644
--- a/docs/integrations.rst
+++ b/docs/integrations.rst
@@ -197,28 +197,8 @@ by kitty's graphics protocol for displaying plots
`gnuplot `_
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-A graphing and data visualization tool that can be made to display its output in
-kitty with the following bash snippet:
-
-.. code-block:: sh
-
- function iplot {
- cat <` mechanism described below. Some examples::
- shift+a -> CSI 97 ; 2 ; 65 u # The text 'A' is reported as 65
- option+a -> CSI 97 ; ; 229 u # The text 'å' is reported as 229
+ shift+a -> CSI 97 ; 2 ; 65 u # The text 'A' is reported as 65
+ alt+a -> CSI 0 ; ; 229 u # The text 'å' is reported as 229
If multiple code points are present, they must be separated by colons. If no
known key is associated with the text the key number ``0`` must be used. The
associated text must not contain control codes (control codes are code points
below U+0020 and codepoints in the C0 and C1 blocks). In the above example, the
-:kbd:`option` modifier is consumed by macOS itself to produce the text å
-and therefore not reported in the keyboard protocol. On some platforms
-composition keys might produce no key information at all, in which case the key
-number ``0`` must be used.
+:kbd:`alt` modifier is consumed by the OS itself to produce the text å and not
+sent to the terminal emulator, which gets only a "text input" event and no
+information about modifiers, thus the event gets encoded with no modifiers.
+The exact behavior in these situations depends on the OS, keyboard layout, IME
+system in use and so on. In general, if the terminal emulator receives no key
+information, the key number 0 must be used to indicate a pure "text event".
Non-Unicode keys
diff --git a/docs/sessions.rst b/docs/sessions.rst
index b173dee5a21..a1be3bc394f 100644
--- a/docs/sessions.rst
+++ b/docs/sessions.rst
@@ -57,12 +57,19 @@ easily swap between them, kitty has you covered. You can use the
map f7>/ goto_session
# Same as above, but the sessions are listed alphabetically instead of by most recent
map f7>/ goto_session --sort-by=alphabetical
+ # Browse session files inside a directory and pick one
+ map f7>p goto_session ~/.local/share/kitty/sessions
# Go to the previously active session (larger negative numbers jump further back in history)
map f7>- goto_session -1
In this manner you can define as many projects/sessions as you like and easily
switch between them with a keypress.
+When a directory path is supplied to :ac:`goto_session`, kitty scans it for
+files ending in ``.kitty-session``, ``.kitty_session`` or ``.session`` and
+presents an interactive list. The ``--sort-by`` option controls the ordering of that list just like it does
+for globally known sessions.
+
You can also close sessions using the :ac:`close_session` action, which closes
all windows in the session with a single keypress.
diff --git a/gen/config.py b/gen/config.py
index b7defd61309..d41e9c84d22 100755
--- a/gen/config.py
+++ b/gen/config.py
@@ -43,17 +43,27 @@ def main(args: list[str]=sys.argv) -> None:
from kitty.options.definition import definition
nullable_colors = []
all_colors = []
+ special_colors = []
for opt in definition.iter_all_options():
if callable(opt.parser_func):
- if opt.parser_func.__name__ in ('to_color_or_none', 'cursor_text_color'):
- nullable_colors.append(opt.name)
- all_colors.append(opt.name)
- elif opt.parser_func.__name__ in ('to_color', 'titlebar_color', 'macos_titlebar_color', 'scrollbar_color'):
- all_colors.append(opt.name)
+ match opt.parser_func.__name__:
+ case 'to_color':
+ all_colors.append(opt.name)
+ case 'macos_titlebar_color' | 'titlebar_color' | 'scrollbar_color':
+ all_colors.append(opt.name)
+ special_colors.append(opt.name)
+ case 'to_color_or_none' | 'cursor_text_color':
+ all_colors.append(opt.name)
+ nullable_colors.append(opt.name)
+
patch_color_list('tools/cmd/at/set_colors.go', nullable_colors, 'NULLABLE')
patch_color_list('tools/themes/collection.go', all_colors, 'ALL')
nc = ',\n '.join(f'{x!r}' for x in nullable_colors)
- write_output('kitty', definition, f'\nnullable_colors = frozenset({{\n {nc}\n}})\n')
+ sc = ',\n '.join(f'{x!r}' for x in special_colors)
+ write_output('kitty', definition,
+ f'\nnullable_colors = frozenset({{\n {nc}\n}})\n'
+ f'\nspecial_colors = frozenset({{\n {sc}\n}})\n'
+ )
if __name__ == '__main__':
diff --git a/glfw/cocoa_init.m b/glfw/cocoa_init.m
index 296949eed47..8a6ae6dfa7a 100644
--- a/glfw/cocoa_init.m
+++ b/glfw/cocoa_init.m
@@ -453,8 +453,9 @@ - (void)observeValueForKeyPath:(NSString *)keyPath
if ([keyPath isEqualToString:@"effectiveAppearance"]) {
// The initial call (from NSKeyValueObservingOptionInitial) might happen on a background thread.
// Dispatch to the main thread to be safe, especially if updating UI.
+ __block __typeof__(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
- [self handleAppearanceChange];
+ [weakSelf handleAppearanceChange];
});
}
} else {
diff --git a/glfw/cocoa_window.m b/glfw/cocoa_window.m
index b4c952f13c6..daba2e3cec9 100644
--- a/glfw/cocoa_window.m
+++ b/glfw/cocoa_window.m
@@ -558,6 +558,7 @@ - (void)cleanup {
- (BOOL)windowShouldClose:(id)sender
{
(void)sender;
+ if (window == nil) return YES;
_glfwInputWindowCloseRequest(window);
return NO;
}
@@ -1968,6 +1969,7 @@ void _glfwPlatformDestroyWindow(_GLFWwindow* window)
// control. Sigh.
NSRect frame = w.frame; frame.size.width = 0; frame.size.height = 0;
[w setFrame:frame display:NO];
+ [w setHasShadow:NO];
[w close]; // sends a release to NSWindow so we dont release it
window->ns.object = nil;
}
@@ -2330,8 +2332,9 @@ void _glfwPlatformShowWindow(_GLFWwindow* window, bool move_to_active_screen)
NSWindowCollectionBehavior old = nw.collectionBehavior;
nw.collectionBehavior = (old & !NSWindowCollectionBehaviorCanJoinAllSpaces) | NSWindowCollectionBehaviorMoveToActiveSpace;
[nw orderFront:nil];
+ __block __typeof__(nw) weakSelf = nw;
dispatch_async(dispatch_get_main_queue(), ^{
- nw.collectionBehavior = old;
+ weakSelf.collectionBehavior = old;
});
}
}
@@ -3479,6 +3482,36 @@ GLFWAPI void glfwCocoaSetWindowChrome(GLFWwindow *w, unsigned int color, bool us
GLFWAPI bool glfwIsLayerShellSupported(void) { return true; }
+GLFWAPI void
+glfwCocoaCycleThroughOSWindows(bool backwards) {
+ NSArray *allWindows = [NSApp windows];
+ if (allWindows.count < 2) return;
+ NSMutableArray *filteredWindows = [NSMutableArray array];
+ for (NSWindow *window in allWindows) {
+ NSRect windowFrame = [window frame];
+ // Exclude zero size windows which are likely zombie windows from the Tahoe bug
+ // if ([obj isMemberOfClass:[MyClass class]]) {
+ if (
+ windowFrame.size.width > 0 && windowFrame.size.height > 0 && \
+ !window.isMiniaturized && window.isVisible && \
+ [window isMemberOfClass:[GLFWWindow class]]
+ ) [filteredWindows addObject:window];
+ }
+ if (filteredWindows.count < 2) return;
+ NSWindow *keyWindow = [NSApp keyWindow];
+ NSUInteger index = [filteredWindows indexOfObject:keyWindow];
+ NSUInteger nextIndex = 0;
+ if (index != NSNotFound) {
+ if (backwards) {
+ nextIndex = (index == 0) ? [filteredWindows count] - 1 : index - 1;
+ } else nextIndex = (index + 1) % filteredWindows.count;
+ }
+ NSWindow *nextWindow = filteredWindows[nextIndex];
+ [nextWindow makeKeyAndOrderFront:nil];
+}
+
+
+
//////////////////////////////////////////////////////////////////////////
////// GLFW internal API //////
//////////////////////////////////////////////////////////////////////////
diff --git a/glfw/glfw.py b/glfw/glfw.py
index e5d7ab3d921..2d1dd3f0901 100755
--- a/glfw/glfw.py
+++ b/glfw/glfw.py
@@ -313,6 +313,7 @@ def generate_wrappers(glfw_header: str) -> None:
void* glfwGetX11Display(void)
unsigned long glfwGetX11Window(GLFWwindow* window)
void glfwSetPrimarySelectionString(GLFWwindow* window, const char* string)
+ void glfwCocoaCycleThroughOSWindows(bool backwards)
void glfwCocoaSetWindowChrome(GLFWwindow* window, unsigned int color, bool use_system_color, unsigned int system_color,\
int background_blur, unsigned int hide_window_decorations, bool show_text_in_titlebar, int color_space, float background_opacity, bool resizable)
const char* glfwGetPrimarySelectionString(GLFWwindow* window, void)
diff --git a/go.mod b/go.mod
index 7dd4009cb63..90c545d5dab 100644
--- a/go.mod
+++ b/go.mod
@@ -13,13 +13,14 @@ require (
github.com/google/uuid v1.6.0
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1
github.com/kovidgoyal/go-parallel v1.1.1
- github.com/kovidgoyal/imaging v1.8.2
+ github.com/kovidgoyal/go-shm v1.0.0
+ github.com/kovidgoyal/imaging v1.8.15
github.com/seancfoley/ipaddress-go v1.7.1
github.com/shirou/gopsutil/v4 v4.25.10
github.com/zeebo/xxh3 v1.0.2
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b
golang.org/x/image v0.32.0
- golang.org/x/sys v0.37.0
+ golang.org/x/sys v0.38.0
golang.org/x/text v0.30.0
howett.net/plist v1.0.1
)
@@ -33,7 +34,6 @@ require (
require (
github.com/ebitengine/purego v0.9.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
- github.com/kettek/apng v0.0.0-20250827064933-2bb5f5fcf253 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
diff --git a/go.sum b/go.sum
index d73f4743a60..2b22db75911 100644
--- a/go.sum
+++ b/go.sum
@@ -24,16 +24,16 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
-github.com/kettek/apng v0.0.0-20250827064933-2bb5f5fcf253 h1:ar6YqPcuumkcWgAJHkmda6Q35V3OnpxeTej4iU/QFLA=
-github.com/kettek/apng v0.0.0-20250827064933-2bb5f5fcf253/go.mod h1:x78/VRQYKuCftMWS0uK5e+F5RJ7S4gSlESRWI0Prl6Q=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1 h1:rMY/hWfcVzBm6BLX6YLA+gLJEpuXBed/VP6YEkXt8R4=
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1/go.mod h1:RbNG3Q1g6GUy1/WzWVx+S24m7VKyvl57vV+cr2hpt50=
github.com/kovidgoyal/go-parallel v1.1.1 h1:1OzpNjtrUkBPq3UaqrnvOoB2F9RttSt811uiUXyI7ok=
github.com/kovidgoyal/go-parallel v1.1.1/go.mod h1:BJNIbe6+hxyFWv7n6oEDPj3PA5qSw5OCtf0hcVxWJiw=
-github.com/kovidgoyal/imaging v1.8.2 h1:AYBaYSAI7W6p2CfQnAqVEB6AeiOVMJ0uUEuhuM+zuWo=
-github.com/kovidgoyal/imaging v1.8.2/go.mod h1:fHutWjAiIZ3t7+YRVzuPkICFFzHTTkxgx2jSeQXKjE0=
+github.com/kovidgoyal/go-shm v1.0.0 h1:HJEel9D1F9YhULvClEHJLawoRSj/1u/EDV7MJbBPgQo=
+github.com/kovidgoyal/go-shm v1.0.0/go.mod h1:Yzb80Xf9L3kaoB2RGok9hHwMIt7Oif61kT6t3+VnZds=
+github.com/kovidgoyal/imaging v1.8.15 h1:FwKRGvmbQ04OFYF5WIalcvUontcnj1chBeHmR5I7UYg=
+github.com/kovidgoyal/imaging v1.8.15/go.mod h1:aXZHGm/gKcaXpxn1NnQiVtuz3Z2BCnXWyjF0ZGrmJ1g=
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=
@@ -67,8 +67,8 @@ golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
-golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
+golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/kittens/choose_files/graphics.go b/kittens/choose_files/graphics.go
index 69c5bbd26f9..e6b675a5e50 100644
--- a/kittens/choose_files/graphics.go
+++ b/kittens/choose_files/graphics.go
@@ -219,7 +219,8 @@ func (self *GraphicsHandler) transmit(lp *loop.Loop, img *images.ImageData, m *i
gc.SetLeftEdge(uint64(frame.Left)).SetTopEdge(uint64(frame.Top))
}
if cached_data == nil {
- transmit_by_escape_code(lp, img.Frames[frame_num].Data(), gc)
+ _, _, _, data := img.Frames[frame_num].Data()
+ transmit_by_escape_code(lp, data, gc)
} else {
path := cached_data[IMAGE_DATA_PREFIX+strconv.Itoa(frame_num)]
transmit_by_file(lp, utils.UnsafeStringToBytes(path), gc)
diff --git a/kittens/choose_files/main.py b/kittens/choose_files/main.py
index 4169085033f..f0657499b37 100644
--- a/kittens/choose_files/main.py
+++ b/kittens/choose_files/main.py
@@ -21,18 +21,18 @@
opt('show_hidden', 'last', choices=('last', 'yes', 'y', 'true', 'no', 'n', 'false'), long_text='''
Whether to show hidden files. The default value of :code:`last` means remember the last
-used value. This setting can be toggled withing the program.''')
+used value. This setting can be toggled within the program.''')
opt('sort_by_last_modified', 'last', choices=('last', 'yes', 'y', 'true', 'no', 'n', 'false'), long_text='''
Whether to sort the list of entries by last modified, instead of name. Note that sorting only applies
before any query is entered. Once a query is entered entries are sorted by their matching score.
The default value of :code:`last` means remember the last
-used value. This setting can be toggled withing the program.''')
+used value. This setting can be toggled within the program.''')
opt('respect_ignores', 'last', choices=('last', 'yes', 'y', 'true', 'no', 'n', 'false'), long_text='''
Whether to respect .gitignore and .ignore files and the :opt:`ignore` setting.
The default value of :code:`last` means remember the last used value.
-This setting can be toggled withing the program.''')
+This setting can be toggled within the program.''')
opt('+ignore', '', add_to_default=False, long_text='''
An ignore pattern to ignore matched files. Uses the same sytax as :code:`.gitignore` files (see :code:`man gitignore`).
@@ -46,7 +46,7 @@
opt('show_preview', 'last', choices=('last', 'yes', 'y', 'true', 'no', 'n', 'false'), long_text='''
Whether to show a preview of the current file/directory. The default value of :code:`last` means remember the last
-used value. This setting can be toggled withing the program.''')
+used value. This setting can be toggled within the program.''')
opt('pygments_style', 'default', long_text='''
The pygments color scheme to use for syntax highlighting of file previews. See :link:`pygments
diff --git a/kittens/choose_files/scan_test.go b/kittens/choose_files/scan_test.go
index 146cc8ed0dd..0d230a7ace2 100644
--- a/kittens/choose_files/scan_test.go
+++ b/kittens/choose_files/scan_test.go
@@ -94,7 +94,7 @@ func (n node) ReadFile(name string) ([]byte, error) {
return nil, fs.ErrNotExist
}
p := &n
- for _, x := range strings.Split(strings.Trim(name, string(os.PathSeparator)), string(os.PathSeparator)) {
+ for x := range strings.SplitSeq(strings.Trim(name, string(os.PathSeparator)), string(os.PathSeparator)) {
c, found := p.children[x]
if !found {
return nil, fs.ErrNotExist
@@ -109,7 +109,7 @@ func (n node) ReadDir(name string) ([]fs.DirEntry, error) {
return n.dir_entries(), nil
}
p := &n
- for _, x := range strings.Split(strings.Trim(name, string(os.PathSeparator)), string(os.PathSeparator)) {
+ for x := range strings.SplitSeq(strings.Trim(name, string(os.PathSeparator)), string(os.PathSeparator)) {
c, found := p.children[x]
if !found {
return nil, fs.ErrNotExist
@@ -334,11 +334,11 @@ func TestSortedResults(t *testing.T) {
}
func run_scoring(b *testing.B, depth, breadth int, query string) {
- b.StopTimer()
+
root := node{name: string(os.PathSeparator)}
root.generate_random_tree(depth, breadth)
- b.StartTimer()
- for range b.N {
+
+ for b.Loop() {
b.StopTimer()
wg := sync.WaitGroup{}
wg.Add(1)
diff --git a/kittens/choose_fonts/face.go b/kittens/choose_fonts/face.go
index 477a10289a3..aaa6dd0d7a7 100644
--- a/kittens/choose_fonts/face.go
+++ b/kittens/choose_fonts/face.go
@@ -101,7 +101,7 @@ func (self *face_panel) draw_axis(sz loop.ScreenSize, y int, ax VariableAxis, ax
}
frac := (min(axis_value, ax.Maximum) - ax.Minimum) / (ax.Maximum - ax.Minimum)
current_cell := int(math.Floor(frac * float64(num_of_cells-1)))
- for i := 0; i < num_of_cells; i++ {
+ for i := range num_of_cells {
buf.WriteString(utils.IfElse(i == current_cell, lp.SprintStyled(current_val_style, `⬤`),
tui.InternalHyperlink("•", fmt.Sprintf("axis:%d/%d:%s", i, num_of_cells-1, ax.Tag))))
}
diff --git a/kittens/clipboard/read.go b/kittens/clipboard/read.go
index 6634336716c..e2e138caf77 100644
--- a/kittens/clipboard/read.go
+++ b/kittens/clipboard/read.go
@@ -258,7 +258,7 @@ func parse_escape_code(etype loop.EscapeCodeType, data []byte) (metadata map[str
}
}
if len(parts) > 1 {
- for _, record := range bytes.Split(parts[1], utils.UnsafeStringToBytes(":")) {
+ for record := range bytes.SplitSeq(parts[1], utils.UnsafeStringToBytes(":")) {
rp := bytes.SplitN(record, utils.UnsafeStringToBytes("="), 2)
v := ""
if len(rp) == 2 {
diff --git a/kittens/desktop_ui/portal.go b/kittens/desktop_ui/portal.go
index 9f010906ec6..c0595c0e4b9 100644
--- a/kittens/desktop_ui/portal.go
+++ b/kittens/desktop_ui/portal.go
@@ -452,7 +452,7 @@ Exec=%s desktop-ui run-server
patched_file := ""
desktops := utils.Filter(strings.Split(d, ":"), func(x string) bool { return x != "" })
desktops = append(desktops, "")
- for _, x := range strings.Split(d, ":") {
+ for x := range strings.SplitSeq(d, ":") {
q := filepath.Join(cf, utils.IfElse(x == "", "portals.conf", fmt.Sprintf("%s-portals.conf", strings.ToLower(x))))
if text, err := os.ReadFile(q); err == nil {
text := patch_portals_conf(text)
diff --git a/kittens/diff/diff.go b/kittens/diff/diff.go
index da903b3a1aa..ca9b6667c37 100644
--- a/kittens/diff/diff.go
+++ b/kittens/diff/diff.go
@@ -119,10 +119,7 @@ func Diff(oldName, old, newName, new string, num_of_context_lines int) []byte {
// End chunk with common lines for context.
if len(ctext) > 0 {
- n := end.x - start.x
- if n > C {
- n = C
- }
+ n := min(end.x-start.x, C)
for _, s := range x[start.x : start.x+n] {
ctext = append(ctext, " "+s)
count.x++
@@ -237,7 +234,7 @@ func tgs(x, y []string) []pair {
for i := range T {
T[i] = n + 1
}
- for i := 0; i < n; i++ {
+ for i := range n {
k := sort.Search(n, func(k int) bool {
return T[k] >= J[i]
})
diff --git a/kittens/hints/marks.go b/kittens/hints/marks.go
index 1dab50887f3..b24da2f9deb 100644
--- a/kittens/hints/marks.go
+++ b/kittens/hints/marks.go
@@ -79,7 +79,7 @@ func process_escape_codes(text string) (ans string, hyperlinks []Mark) {
active_hyperlink_url = url
active_hyperlink_start_offset = start
if metadata != "" {
- for _, entry := range strings.Split(metadata, ":") {
+ for entry := range strings.SplitSeq(metadata, ":") {
if strings.HasPrefix(entry, "id=") && len(entry) > 3 {
active_hyperlink_id = entry[3:]
}
diff --git a/kittens/hyperlinked_grep/main.go b/kittens/hyperlinked_grep/main.go
index c83f77da253..38bab8a4b38 100644
--- a/kittens/hyperlinked_grep/main.go
+++ b/kittens/hyperlinked_grep/main.go
@@ -47,7 +47,7 @@ func get_options_for_rg() (expecting_args map[string]bool, alias_map map[string]
expecting_arg := strings.Contains(s, "=")
single_letter_aliases := make([]string, 0, 1)
long_option_names := make([]string, 0, 1)
- for _, x := range strings.Split(s, ",") {
+ for x := range strings.SplitSeq(s, ",") {
x = strings.TrimSpace(x)
if strings.HasPrefix(x, "--") {
lon, _, _ := strings.Cut(x[2:], "=")
@@ -136,7 +136,7 @@ func parse_args(args ...string) (delegate_to_rg bool, sanitized_args []string, k
if !found || k != "hyperlink" {
return fmt.Errorf("Unknown --kitten option: %s", val)
}
- for _, x := range strings.Split(v, ",") {
+ for x := range strings.SplitSeq(v, ",") {
switch x {
case "none":
kitten_opts.context_lines = false
diff --git a/kittens/icat/detect.go b/kittens/icat/detect.go
index 226a600d54e..9d19d5cda8f 100644
--- a/kittens/icat/detect.go
+++ b/kittens/icat/detect.go
@@ -8,11 +8,11 @@ import (
"os"
"time"
+ "github.com/kovidgoyal/go-shm"
"github.com/kovidgoyal/kitty/tools/tui/graphics"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/images"
- "github.com/kovidgoyal/kitty/tools/utils/shm"
)
var _ = fmt.Print
diff --git a/kittens/icat/magick.go b/kittens/icat/magick.go
deleted file mode 100644
index 7b5047cb9ec..00000000000
--- a/kittens/icat/magick.go
+++ /dev/null
@@ -1,64 +0,0 @@
-// License: GPLv3 Copyright: 2023, Kovid Goyal,
-
-package icat
-
-import (
- "fmt"
-
- "github.com/kovidgoyal/go-parallel"
- "github.com/kovidgoyal/kitty/tools/tui/graphics"
- "github.com/kovidgoyal/kitty/tools/utils/images"
-)
-
-var _ = fmt.Print
-
-func Render(path string, ro *images.RenderOptions, frames []images.IdentifyRecord) (ans []*image_frame, err error) {
- ro.TempfilenameTemplate = shm_template
- image_frames, filenames, err := images.RenderWithMagick(path, ro, frames)
- if err == nil {
- ans = make([]*image_frame, len(image_frames))
- for i, x := range image_frames {
- ans[i] = &image_frame{
- filename: filenames[x.Number], filename_is_temporary: true,
- number: x.Number, width: x.Width, height: x.Height, left: x.Left, top: x.Top,
- transmission_format: graphics.GRT_format_rgba, delay_ms: int(x.Delay_ms), compose_onto: x.Compose_onto,
- }
- if x.Is_opaque {
- ans[i].transmission_format = graphics.GRT_format_rgb
- }
- }
- }
- return ans, err
-}
-
-func render_image_with_magick(imgd *image_data, src *opened_input) (err error) {
- defer func() {
- if r := recover(); r != nil {
- err = parallel.Format_stacktrace_on_panic(r, 1)
- }
- }()
- err = src.PutOnFilesystem()
- if err != nil {
- return err
- }
- frames, err := images.IdentifyWithMagick(src.FileSystemName())
- if err != nil {
- return err
- }
- imgd.format_uppercase = frames[0].Fmt_uppercase
- imgd.canvas_width, imgd.canvas_height = frames[0].Canvas.Width, frames[0].Canvas.Height
- set_basic_metadata(imgd)
- if !imgd.needs_conversion {
- make_output_from_input(imgd, src)
- return nil
- }
- ro := images.RenderOptions{RemoveAlpha: remove_alpha, Flip: flip, Flop: flop}
- if scale_image(imgd) {
- ro.ResizeTo.X, ro.ResizeTo.Y = imgd.canvas_width, imgd.canvas_height
- }
- imgd.frames, err = Render(src.FileSystemName(), &ro, frames)
- if err != nil {
- return err
- }
- return nil
-}
diff --git a/kittens/icat/main.go b/kittens/icat/main.go
index 64d8bef526b..6b84c070e98 100644
--- a/kittens/icat/main.go
+++ b/kittens/icat/main.go
@@ -42,6 +42,15 @@ const (
supported
)
+type fit_t int
+
+const (
+ fit_none fit_t = iota
+ fit_width
+ fit_height
+ fit_both
+)
+
var transfer_by_file, transfer_by_memory transfer_mode
var files_channel chan input_arg
@@ -49,6 +58,7 @@ var output_channel chan *image_data
var num_of_items int
var keep_going *atomic.Bool
var screen_size *unix.Winsize
+var fit_mode fit_t
func send_output(imgd *image_data) {
output_channel <- imgd
@@ -87,6 +97,22 @@ func parse_z_index() (err error) {
return
}
+func parse_fit() (err error) {
+ switch strings.ToLower(opts.Fit) {
+ case "width":
+ fit_mode = fit_width
+ case "height":
+ fit_mode = fit_height
+ case "none", "neither":
+ fit_mode = fit_none
+ case "both":
+ fit_mode = fit_both
+ default:
+ return fmt.Errorf("unknown fit specification: %#v", opts.Fit)
+ }
+ return nil
+}
+
func parse_place() (err error) {
if opts.Place == "" {
return nil
@@ -130,8 +156,10 @@ func print_error(format string, args ...any) {
func main(cmd *cli.Command, o *Options, args []string) (rc int, err error) {
opts = o
- err = parse_place()
- if err != nil {
+ if err = parse_place(); err != nil {
+ return 1, err
+ }
+ if err = parse_fit(); err != nil {
return 1, err
}
err = parse_z_index()
diff --git a/kittens/icat/main.py b/kittens/icat/main.py
index b3ad4e88518..c571a500af2 100644
--- a/kittens/icat/main.py
+++ b/kittens/icat/main.py
@@ -22,9 +22,16 @@
--scale-up
type=bool-set
-When used in combination with :option:`--place` it will cause images that are
-smaller than the specified area to be scaled up to use as much of the specified
-area as possible.
+Cause images that are smaller than the specified area to be scaled up to use as much
+of the specified area as possible. The specified area depends on either the :option:`--place`
+or the :option:`--fit` options.
+
+
+--fit
+choices=width,height,both,none
+default=width
+When not using :option:`--place`, control how the image is scaled relative to the screen.
+You can have it fit in the screen width or height or both or neither.
--background
diff --git a/kittens/icat/native.go b/kittens/icat/native.go
deleted file mode 100644
index 4ff59722782..00000000000
--- a/kittens/icat/native.go
+++ /dev/null
@@ -1,149 +0,0 @@
-// License: GPLv3 Copyright: 2023, Kovid Goyal,
-
-package icat
-
-import (
- "fmt"
- "image"
-
- "github.com/kovidgoyal/go-parallel"
- "github.com/kovidgoyal/imaging/nrgb"
- "github.com/kovidgoyal/kitty/tools/tty"
- "github.com/kovidgoyal/kitty/tools/tui/graphics"
- "github.com/kovidgoyal/kitty/tools/utils/images"
- "github.com/kovidgoyal/kitty/tools/utils/shm"
-
- "github.com/kovidgoyal/imaging"
-)
-
-var _ = fmt.Print
-
-func resize_frame(imgd *image_data, img image.Image) (image.Image, image.Rectangle) {
- b := img.Bounds()
- left, top, width, height := b.Min.X, b.Min.Y, b.Dx(), b.Dy()
- new_width := int(imgd.scaled_frac.x * float64(width))
- new_height := int(imgd.scaled_frac.y * float64(height))
- img = imaging.Resize(img, new_width, new_height, imaging.Lanczos)
- newleft := int(imgd.scaled_frac.x * float64(left))
- newtop := int(imgd.scaled_frac.y * float64(top))
- return img, image.Rect(newleft, newtop, newleft+new_width, newtop+new_height)
-}
-
-const shm_template = "kitty-icat-*"
-
-func add_frame(ctx *images.Context, imgd *image_data, img image.Image, left, top int) *image_frame {
- is_opaque := false
- if imgd.format_uppercase == "JPEG" {
- // special cased because EXIF orientation could have already changed this image to an NRGBA making IsOpaque() very slow
- is_opaque = true
- } else {
- is_opaque = imaging.IsOpaque(img)
- }
- b := img.Bounds()
- if imgd.scaled_frac.x != 0 {
- img, b = resize_frame(imgd, img)
- }
- f := image_frame{width: b.Dx(), height: b.Dy(), number: len(imgd.frames) + 1, left: left, top: top}
- dest_rect := image.Rect(0, 0, f.width, f.height)
- var final_img image.Image
- bytes_per_pixel := 4
-
- if is_opaque || remove_alpha != nil {
- var rgb *imaging.NRGB
- bytes_per_pixel = 3
- m, err := shm.CreateTemp(shm_template, uint64(f.width*f.height*bytes_per_pixel))
- if err != nil {
- rgb = nrgb.NewNRGB(dest_rect)
- } else {
- rgb = &imaging.NRGB{Pix: m.Slice(), Stride: bytes_per_pixel * f.width, Rect: dest_rect}
- f.shm = m
- }
- f.transmission_format = graphics.GRT_format_rgb
- f.in_memory_bytes = rgb.Pix
- final_img = rgb
- } else {
- var rgba *image.NRGBA
- m, err := shm.CreateTemp(shm_template, uint64(f.width*f.height*bytes_per_pixel))
- if err != nil {
- rgba = image.NewNRGBA(dest_rect)
- } else {
- rgba = &image.NRGBA{Pix: m.Slice(), Stride: bytes_per_pixel * f.width, Rect: dest_rect}
- f.shm = m
- }
- f.transmission_format = graphics.GRT_format_rgba
- f.in_memory_bytes = rgba.Pix
- final_img = rgba
- }
- ctx.PasteCenter(final_img, img, remove_alpha)
- imgd.frames = append(imgd.frames, &f)
- if flip {
- ctx.FlipPixelsV(bytes_per_pixel, f.width, f.height, f.in_memory_bytes)
- if f.height < imgd.canvas_height {
- f.top = (2*imgd.canvas_height - f.height - f.top) % imgd.canvas_height
- }
- }
- if flop {
- ctx.FlipPixelsH(bytes_per_pixel, f.width, f.height, f.in_memory_bytes)
- if f.width < imgd.canvas_width {
- f.left = (2*imgd.canvas_width - f.width - f.left) % imgd.canvas_width
- }
- }
- return &f
-}
-
-func scale_image(imgd *image_data) bool {
- if imgd.needs_scaling {
- width, height := imgd.canvas_width, imgd.canvas_height
- if imgd.canvas_width < imgd.available_width && opts.ScaleUp && place != nil {
- r := float64(imgd.available_width) / float64(imgd.canvas_width)
- imgd.canvas_width, imgd.canvas_height = imgd.available_width, int(r*float64(imgd.canvas_height))
- }
- neww, newh := images.FitImage(imgd.canvas_width, imgd.canvas_height, imgd.available_width, imgd.available_height)
- imgd.needs_scaling = false
- imgd.scaled_frac.x = float64(neww) / float64(width)
- imgd.scaled_frac.y = float64(newh) / float64(height)
- imgd.canvas_width = int(imgd.scaled_frac.x * float64(width))
- imgd.canvas_height = int(imgd.scaled_frac.y * float64(height))
- return true
- }
- return false
-}
-
-var debugprintln = tty.DebugPrintln
-var _ = debugprintln
-
-func add_frames(ctx *images.Context, imgd *image_data, gf *imaging.Image) {
- for _, f := range gf.Frames {
- frame := add_frame(ctx, imgd, f.Image, f.X, f.Y)
- frame.number, frame.compose_onto = int(f.Number), int(f.ComposeOnto)
- frame.replace = f.Replace
- frame.delay_ms = int(f.Delay.Milliseconds())
- if frame.delay_ms <= 0 {
- frame.delay_ms = -1 // -1 is gapless in graphics protocol
- }
- }
-}
-
-func render_image_with_go(imgd *image_data, src *opened_input) (err error) {
- defer func() {
- if r := recover(); r != nil {
- err = parallel.Format_stacktrace_on_panic(r, 1)
- }
- }()
- ctx := images.Context{}
- imgs, _, err := imaging.DecodeAll(src.file)
- if err != nil {
- return err
- }
- if imgs == nil {
- return fmt.Errorf("unknown image format")
- }
- // Loading could auto orient and therefore change width/height, so
- // re-calculate
- imgd.canvas_width = int(imgs.Metadata.PixelWidth)
- imgd.canvas_height = int(imgs.Metadata.PixelHeight)
- set_basic_metadata(imgd)
- scale_image(imgd)
- add_frames(&ctx, imgd, imgs)
- return nil
-}
diff --git a/kittens/icat/process_images.go b/kittens/icat/process_images.go
index c9957f279f9..3a3f0dc5b9e 100644
--- a/kittens/icat/process_images.go
+++ b/kittens/icat/process_images.go
@@ -8,58 +8,22 @@ import (
"image"
"io"
"io/fs"
+ "math"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
+ "github.com/kovidgoyal/imaging"
"github.com/kovidgoyal/kitty/tools/tty"
"github.com/kovidgoyal/kitty/tools/tui/graphics"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/images"
- "github.com/kovidgoyal/kitty/tools/utils/shm"
)
var _ = fmt.Print
-type BytesBuf struct {
- data []byte
- pos int64
-}
-
-func (self *BytesBuf) Seek(offset int64, whence int) (int64, error) {
- switch whence {
- case io.SeekStart:
- self.pos = offset
- case io.SeekCurrent:
- self.pos += offset
- case io.SeekEnd:
- self.pos = int64(len(self.data)) + offset
- default:
- return self.pos, fmt.Errorf("Unknown value for whence: %#v", whence)
- }
- self.pos = utils.Max(0, utils.Min(self.pos, int64(len(self.data))))
- return self.pos, nil
-}
-
-func (self *BytesBuf) Read(p []byte) (n int, err error) {
- nb := utils.Min(int64(len(p)), int64(len(self.data))-self.pos)
- if nb == 0 {
- err = io.EOF
- } else {
- n = copy(p, self.data[self.pos:self.pos+nb])
- self.pos += nb
- }
- return
-}
-
-func (self *BytesBuf) Close() error {
- self.data = nil
- self.pos = 0
- return nil
-}
-
type input_arg struct {
arg string
value string
@@ -119,54 +83,14 @@ func process_dirs(args ...string) (results []input_arg, err error) {
}
type opened_input struct {
- file io.ReadSeekCloser
- name_to_unlink string
-}
-
-func (self *opened_input) Rewind() {
- if self.file != nil {
- _, _ = self.file.Seek(0, io.SeekStart)
- }
-}
-
-func (self *opened_input) Release() {
- if self.file != nil {
- self.file.Close()
- self.file = nil
- }
- if self.name_to_unlink != "" {
- os.Remove(self.name_to_unlink)
- self.name_to_unlink = ""
- }
+ file io.Reader
+ bytes []byte
+ path string
}
-func (self *opened_input) PutOnFilesystem() (err error) {
- if self.name_to_unlink != "" {
- return
- }
- f, err := images.CreateTempInRAM()
- if err != nil {
- return fmt.Errorf("Failed to create a temporary file to store input data with error: %w", err)
- }
- self.Rewind()
- _, err = io.Copy(f, self.file)
- if err != nil {
- f.Close()
- return fmt.Errorf("Failed to copy input data to temporary file with error: %w", err)
- }
- self.Release()
- self.file = f
- self.name_to_unlink = f.Name()
- return
-}
-
-func (self *opened_input) FileSystemName() string { return self.name_to_unlink }
-
type image_frame struct {
filename string
- shm shm.MMap
in_memory_bytes []byte
- filename_is_temporary bool
width, height, left, top int
transmission_format graphics.GRT_f
compose_onto int
@@ -179,8 +103,7 @@ type image_data struct {
canvas_width, canvas_height int
format_uppercase string
available_width, available_height int
- needs_scaling, needs_conversion bool
- scaled_frac struct{ x, y float64 }
+ needs_scaling bool
frames []*image_frame
image_number uint32
image_id uint32
@@ -196,18 +119,31 @@ type image_data struct {
source_name string
}
+const inf = math.MaxInt
+
func set_basic_metadata(imgd *image_data) {
if imgd.frames == nil {
imgd.frames = make([]*image_frame, 0, 32)
}
- imgd.available_width = int(screen_size.Xpixel)
- imgd.available_height = 10 * imgd.canvas_height
if place != nil {
imgd.available_width = place.width * int(screen_size.Xpixel) / int(screen_size.Col)
imgd.available_height = place.height * int(screen_size.Ypixel) / int(screen_size.Row)
+ } else {
+ switch fit_mode {
+ case fit_none:
+ imgd.available_width, imgd.available_height = inf, inf
+ case fit_both:
+ imgd.available_width = int(screen_size.Xpixel)
+ imgd.available_height = int(screen_size.Ypixel)
+ case fit_width:
+ imgd.available_width = int(screen_size.Xpixel)
+ imgd.available_height = inf
+ case fit_height:
+ imgd.available_width = inf
+ imgd.available_height = int(screen_size.Ypixel)
+ }
}
imgd.needs_scaling = imgd.canvas_width > imgd.available_width || imgd.canvas_height > imgd.available_height || opts.ScaleUp
- imgd.needs_conversion = imgd.needs_scaling || remove_alpha != nil || flip || flop || imgd.format_uppercase != "PNG"
}
func report_error(source_name, msg string, err error) {
@@ -216,7 +152,6 @@ func report_error(source_name, msg string, err error) {
}
func make_output_from_input(imgd *image_data, f *opened_input) {
- bb, ok := f.file.(*BytesBuf)
frame := image_frame{}
imgd.frames = append(imgd.frames, &frame)
frame.width = imgd.canvas_width
@@ -225,15 +160,75 @@ func make_output_from_input(imgd *image_data, f *opened_input) {
panic(fmt.Sprintf("Unknown transmission format: %s", imgd.format_uppercase))
}
frame.transmission_format = graphics.GRT_format_png
- if ok {
- frame.in_memory_bytes = bb.data
+ if f.bytes != nil {
+ frame.in_memory_bytes = f.bytes
+ } else if f.path != "" {
+ frame.filename = f.path
+ } else {
+ var err error
+ if frame.in_memory_bytes, err = io.ReadAll(f.file); err != nil {
+ panic(err)
+ }
+ }
+}
+
+func scale_up(width, height, maxWidth, maxHeight int) (newWidth, newHeight int) {
+ if width == 0 || height == 0 {
+ return 0, 0
+ }
+ // Calculate the ratio to scale the width and the ratio to scale the height.
+ // We use floating-point division for precision.
+ widthRatio := float64(maxWidth) / float64(width)
+ heightRatio := float64(maxHeight) / float64(height)
+
+ // To preserve the aspect ratio and fit within the limits, we must use the
+ // smaller of the two scaling ratios.
+ var ratio float64
+ if widthRatio < heightRatio {
+ ratio = widthRatio
} else {
- frame.filename = f.file.(*os.File).Name()
- if f.name_to_unlink != "" {
- frame.filename_is_temporary = true
- f.name_to_unlink = ""
+ ratio = heightRatio
+ }
+
+ // Calculate the new dimensions and convert them back to uints.
+ newWidth = int(float64(width) * ratio)
+ newHeight = int(float64(height) * ratio)
+
+ return newWidth, newHeight
+}
+
+func scale_image(imgd *image_data) bool {
+ if imgd.needs_scaling {
+ width, height := imgd.canvas_width, imgd.canvas_height
+ if opts.ScaleUp && (imgd.canvas_width < imgd.available_width || imgd.canvas_height < imgd.available_height) && (imgd.available_height != inf || imgd.available_width != inf) {
+ imgd.canvas_width, imgd.canvas_height = scale_up(imgd.canvas_width, imgd.canvas_height, imgd.available_width, imgd.available_height)
}
+ neww, newh := images.FitImage(imgd.canvas_width, imgd.canvas_height, imgd.available_width, imgd.available_height)
+ imgd.needs_scaling = false
+ x := float64(neww) / float64(width)
+ y := float64(newh) / float64(height)
+ imgd.canvas_width = int(x * float64(width))
+ imgd.canvas_height = int(y * float64(height))
+ return true
+ }
+ return false
+}
+
+func add_frame(imgd *image_data, img image.Image, left, top int) *image_frame {
+ const shm_template = "kitty-icat-*"
+ num_channels := 4
+ var pix []byte
+ if imaging.IsOpaque(img) {
+ num_channels, pix = 3, imaging.AsRGBData8(img)
+ } else {
+ pix = imaging.AsRGBAData8(img)
}
+ b := img.Bounds()
+ f := image_frame{width: b.Dx(), height: b.Dy(), number: len(imgd.frames) + 1, left: left, top: top}
+ f.transmission_format = utils.IfElse(num_channels == 3, graphics.GRT_format_rgb, graphics.GRT_format_rgba)
+ f.in_memory_bytes = pix
+ imgd.frames = append(imgd.frames, &f)
+ return &f
}
func process_arg(arg input_arg) {
@@ -256,14 +251,16 @@ func process_arg(arg input_arg) {
report_error(arg.value, "Could not download", err)
return
}
- f.file = &BytesBuf{data: dest.Bytes()}
+ f.bytes = dest.Bytes()
+ f.file = bytes.NewReader(f.bytes)
} else if arg.value == "" {
stdin, err := io.ReadAll(os.Stdin)
if err != nil {
report_error("", "Could not read from", err)
return
}
- f.file = &BytesBuf{data: stdin}
+ f.bytes = stdin
+ f.file = bytes.NewReader(f.bytes)
} else {
q, err := os.Open(arg.value)
if err != nil {
@@ -271,56 +268,70 @@ func process_arg(arg input_arg) {
return
}
f.file = q
+ f.path = q.Name()
+ defer q.Close()
+ }
+
+ var img *images.ImageData
+ var dopts []imaging.DecodeOption
+ needs_conversion := false
+ if flip {
+ dopts = append(dopts, imaging.Transform(imaging.FlipVTransform))
+ needs_conversion = true
+ }
+ if flop {
+ dopts = append(dopts, imaging.Transform(imaging.FlipHTransform))
+ needs_conversion = true
+ }
+ if remove_alpha != nil {
+ dopts = append(dopts, imaging.Background(*remove_alpha))
+ needs_conversion = true
+ }
+ switch opts.Engine {
+ case "native", "builtin":
+ dopts = append(dopts, imaging.Backends(imaging.GO_IMAGE))
+ case "magick":
+ dopts = append(dopts, imaging.Backends(imaging.MAGICK_IMAGE))
}
- defer f.Release()
- can_use_go := false
- var c image.Config
- var format string
- var err error
imgd := image_data{source_name: arg.value}
- if opts.Engine == "auto" || opts.Engine == "builtin" {
- c, format, err = image.DecodeConfig(f.file)
- f.Rewind()
- can_use_go = err == nil
+ dopts = append(dopts, imaging.ResizeCallback(func(w, h int) (int, int) {
+ imgd.canvas_width, imgd.canvas_height = w, h
+ set_basic_metadata(&imgd)
+ if scale_image(&imgd) {
+ needs_conversion = true
+ w, h = imgd.canvas_width, imgd.canvas_height
+ }
+ return w, h
+ }))
+ var err error
+ if f.path != "" {
+ img, err = images.OpenImageFromPath(f.path, dopts...)
+ } else {
+ img, f.file, err = images.OpenImageFromReader(f.file, dopts...)
+ }
+ if err != nil {
+ report_error(arg.value, "Could not render image to RGB", err)
+ return
}
if !keep_going.Load() {
return
}
- if can_use_go {
- imgd.canvas_width = c.Width
- imgd.canvas_height = c.Height
- imgd.format_uppercase = strings.ToUpper(format)
- set_basic_metadata(&imgd)
- if !imgd.needs_conversion {
- make_output_from_input(&imgd, &f)
- send_output(&imgd)
- return
- }
- err = render_image_with_go(&imgd, &f)
- if err != nil {
- if opts.Engine != "builtin" {
- merr := render_image_with_magick(&imgd, &f)
- if merr != nil {
- report_error(arg.value, "Could not render image to RGB", err)
- return
- }
- err = nil
- }
- report_error(arg.value, "could not render", err)
- return
- }
+ imgd.format_uppercase = img.Format_uppercase
+ imgd.canvas_width, imgd.canvas_height = img.Width, img.Height
+ if !needs_conversion && imgd.format_uppercase == "PNG" && len(img.Frames) == 1 {
+ make_output_from_input(&imgd, &f)
} else {
- err = render_image_with_magick(&imgd, &f)
- if err != nil {
- report_error(arg.value, "ImageMagick failed", err)
- return
+ for _, f := range img.Frames {
+ frame := add_frame(&imgd, f.Img, f.Left, f.Top)
+ frame.number, frame.compose_onto = int(f.Number), int(f.Compose_onto)
+ frame.replace = f.Replace
+ frame.delay_ms = int(f.Delay_ms)
}
}
if !keep_going.Load() {
return
}
send_output(&imgd)
-
}
func run_worker() {
diff --git a/kittens/icat/scaling_test.go b/kittens/icat/scaling_test.go
new file mode 100644
index 00000000000..82b974b7169
--- /dev/null
+++ b/kittens/icat/scaling_test.go
@@ -0,0 +1,29 @@
+package icat
+
+import (
+ "fmt"
+ "image"
+ "testing"
+)
+
+var _ = fmt.Print
+
+func TestScaling(t *testing.T) {
+ for _, tc := range []struct {
+ w, h, pw, ph, ew, eh int
+ }{
+ {1000, 50, 800, 600, 800, 40},
+ {1000, 50, 800000, 600, 12000, 600},
+ {100, 50, 800, 600, 800, 400},
+ {1920, 1080, 800, 600, 800, 450},
+ {300, 900, 800, 600, 200, 600},
+ {400, 300, 800, 600, 800, 600},
+ } {
+ aw, ah := scale_up(tc.w, tc.h, tc.pw, tc.ph)
+ actual := image.Pt(aw, ah)
+ expected := image.Pt(tc.ew, tc.eh)
+ if actual != expected {
+ t.Fatalf("want: %v got: %v", expected, actual)
+ }
+ }
+}
diff --git a/kittens/icat/transmit.go b/kittens/icat/transmit.go
index a5c4a30038c..4da2746f775 100644
--- a/kittens/icat/transmit.go
+++ b/kittens/icat/transmit.go
@@ -6,7 +6,6 @@ import (
"bytes"
"crypto/rand"
"encoding/binary"
- "errors"
"fmt"
"github.com/kovidgoyal/kitty"
"io"
@@ -16,12 +15,12 @@ import (
"path/filepath"
"strings"
+ "github.com/kovidgoyal/go-shm"
"github.com/kovidgoyal/kitty/tools/tui"
"github.com/kovidgoyal/kitty/tools/tui/graphics"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/images"
- "github.com/kovidgoyal/kitty/tools/utils/shm"
)
var _ = fmt.Print
@@ -92,43 +91,32 @@ func transmit_shm(imgd *image_data, frame_num int, frame *image_frame) (err erro
return fmt.Errorf("Failed to open image data output file: %s with error: %w", frame.filename, err)
}
defer f.Close()
- data_size, _ = f.Seek(0, io.SeekEnd)
- _, _ = f.Seek(0, io.SeekStart)
- mmap, err = shm.CreateTemp("icat-*", uint64(data_size))
- if err != nil {
+ if data_size, err = f.Seek(0, io.SeekEnd); err != nil {
+ return fmt.Errorf("Failed to seek in image data output file: %s with error: %w", frame.filename, err)
+ }
+ if _, err = f.Seek(0, io.SeekStart); err != nil {
+ return fmt.Errorf("Failed to seek in image data output file: %s with error: %w", frame.filename, err)
+ }
+ if mmap, err = shm.CreateTemp("icat-*", uint64(data_size)); err != nil {
return fmt.Errorf("Failed to create a SHM file for transmission: %w", err)
}
- dest := mmap.Slice()
- for len(dest) > 0 {
- n, err := f.Read(dest)
- dest = dest[n:]
- if err != nil {
- if errors.Is(err, io.EOF) {
- break
- }
- _ = mmap.Unlink()
- return fmt.Errorf("Failed to read data from image output data file: %w", err)
- }
+ if _, err = io.ReadFull(f, mmap.Slice()); err != nil {
+ mmap.Close()
+ mmap.Unlink()
+ return fmt.Errorf("Failed to read data from image output data file: %w", err)
}
} else {
- if frame.shm == nil {
- data_size = int64(len(frame.in_memory_bytes))
- mmap, err = shm.CreateTemp("icat-*", uint64(data_size))
- if err != nil {
- return fmt.Errorf("Failed to create a SHM file for transmission: %w", err)
- }
- copy(mmap.Slice(), frame.in_memory_bytes)
- } else {
- mmap = frame.shm
- frame.shm = nil
+ data_size = int64(len(frame.in_memory_bytes))
+ if mmap, err = shm.CreateTemp("icat-*", uint64(data_size)); err != nil {
+ return fmt.Errorf("Failed to create a SHM file for transmission: %w", err)
}
+ copy(mmap.Slice(), frame.in_memory_bytes)
}
+ defer mmap.Close() // terminal is responsible for unlink
gc := gc_for_image(imgd, frame_num, frame)
gc.SetTransmission(graphics.GRT_transmission_sharedmem)
gc.SetDataSize(uint64(data_size))
err = gc.WriteWithPayloadTo(os.Stdout, utils.UnsafeStringToBytes(mmap.Name()))
- mmap.Close()
-
return
}
@@ -137,7 +125,6 @@ func transmit_file(imgd *image_data, frame_num int, frame *image_frame) (err err
fname := ""
var data_size int
if frame.in_memory_bytes == nil {
- is_temp = frame.filename_is_temporary
fname, err = filepath.Abs(frame.filename)
if err != nil {
return fmt.Errorf("Failed to convert image data output file: %s to absolute path with error: %w", frame.filename, err)
@@ -145,30 +132,21 @@ func transmit_file(imgd *image_data, frame_num int, frame *image_frame) (err err
frame.filename = "" // so it isn't deleted in cleanup
} else {
is_temp = true
- if frame.shm != nil && frame.shm.FileSystemName() != "" {
- fname = frame.shm.FileSystemName()
- frame.shm.Close()
- frame.shm = nil
- } else {
- f, err := images.CreateTempInRAM()
- if err != nil {
- return fmt.Errorf("Failed to create a temp file for image data transmission: %w", err)
- }
- data_size = len(frame.in_memory_bytes)
- _, err = bytes.NewBuffer(frame.in_memory_bytes).WriteTo(f)
- f.Close()
- if err != nil {
- return fmt.Errorf("Failed to write image data to temp file for transmission: %w", err)
- }
- fname = f.Name()
+ f, err := images.CreateTempInRAM()
+ if err != nil {
+ return fmt.Errorf("Failed to create a temp file for image data transmission: %w", err)
}
+ data_size = len(frame.in_memory_bytes)
+ _, err = bytes.NewBuffer(frame.in_memory_bytes).WriteTo(f)
+ f.Close()
+ if err != nil {
+ os.Remove(f.Name())
+ return fmt.Errorf("Failed to write image data to temp file for transmission: %w", err)
+ }
+ fname = f.Name()
}
gc := gc_for_image(imgd, frame_num, frame)
- if is_temp {
- gc.SetTransmission(graphics.GRT_transmission_tempfile)
- } else {
- gc.SetTransmission(graphics.GRT_transmission_file)
- }
+ gc.SetTransmission(utils.IfElse(is_temp, graphics.GRT_transmission_tempfile, graphics.GRT_transmission_file))
if data_size > 0 {
gc.SetDataSize(uint64(data_size))
}
@@ -178,14 +156,9 @@ func transmit_file(imgd *image_data, frame_num int, frame *image_frame) (err err
func transmit_stream(imgd *image_data, frame_num int, frame *image_frame) (err error) {
data := frame.in_memory_bytes
if data == nil {
- f, err := os.Open(frame.filename)
- if err != nil {
- return fmt.Errorf("Failed to open image data output file: %s with error: %w", frame.filename, err)
- }
- data, err = io.ReadAll(f)
- f.Close()
+ data, err = os.ReadFile(frame.filename)
if err != nil {
- return fmt.Errorf("Failed to read data from image output data file: %w", err)
+ return fmt.Errorf("Failed to read image data output file: %s with error: %w", frame.filename, err)
}
}
gc := gc_for_image(imgd, frame_num, frame)
@@ -282,20 +255,6 @@ func transmit_image(imgd *image_data, no_trailing_newline bool) {
if seen_image_ids == nil {
seen_image_ids = utils.NewSet[uint32](32)
}
- defer func() {
- for _, frame := range imgd.frames {
- if frame.filename_is_temporary && frame.filename != "" {
- os.Remove(frame.filename)
- frame.filename = ""
- }
- if frame.shm != nil {
- _ = frame.shm.Unlink()
- frame.shm.Close()
- frame.shm = nil
- }
- frame.in_memory_bytes = nil
- }
- }()
var f func(*image_data, int, *image_frame) error
if opts.TransferMode != "detect" {
switch opts.TransferMode {
diff --git a/kittens/notify/main.go b/kittens/notify/main.go
index ff99c891846..594f09ea8d2 100644
--- a/kittens/notify/main.go
+++ b/kittens/notify/main.go
@@ -137,7 +137,7 @@ func (p *parsed_data) run_loop() (err error) {
raw := utils.UnsafeBytesToString(data[len(ESC_CODE_PREFIX[2:]):])
metadata, payload, _ := strings.Cut(raw, ";")
sent_identifier, payload_type := "", ""
- for _, x := range strings.Split(metadata, ":") {
+ for x := range strings.SplitSeq(metadata, ":") {
key, val, _ := strings.Cut(x, "=")
switch key {
case "i":
diff --git a/kittens/ssh/askpass.go b/kittens/ssh/askpass.go
index ac28035ec43..742dd9c787e 100644
--- a/kittens/ssh/askpass.go
+++ b/kittens/ssh/askpass.go
@@ -13,9 +13,9 @@ import (
"strings"
"time"
+ "github.com/kovidgoyal/go-shm"
"github.com/kovidgoyal/kitty/tools/cli"
"github.com/kovidgoyal/kitty/tools/tty"
- "github.com/kovidgoyal/kitty/tools/utils/shm"
)
var _ = fmt.Print
diff --git a/kittens/ssh/main.go b/kittens/ssh/main.go
index 438c4791046..0eb90f4c0a5 100644
--- a/kittens/ssh/main.go
+++ b/kittens/ssh/main.go
@@ -28,6 +28,7 @@ import (
"syscall"
"time"
+ "github.com/kovidgoyal/go-shm"
"github.com/kovidgoyal/kitty/tools/cli"
"github.com/kovidgoyal/kitty/tools/themes"
"github.com/kovidgoyal/kitty/tools/tty"
@@ -37,7 +38,6 @@ import (
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/secrets"
"github.com/kovidgoyal/kitty/tools/utils/shlex"
- "github.com/kovidgoyal/kitty/tools/utils/shm"
"golang.org/x/sys/unix"
)
diff --git a/kittens/ssh/main_test.go b/kittens/ssh/main_test.go
index 0e1a83a294f..21fcee8d29f 100644
--- a/kittens/ssh/main_test.go
+++ b/kittens/ssh/main_test.go
@@ -6,8 +6,8 @@ import (
"encoding/binary"
"encoding/json"
"fmt"
+ "github.com/kovidgoyal/go-shm"
"github.com/kovidgoyal/kitty"
- "github.com/kovidgoyal/kitty/tools/utils/shm"
"io/fs"
"os"
"os/exec"
diff --git a/kittens/transfer/ftc_test.go b/kittens/transfer/ftc_test.go
index 9880b43960f..67f62bde1f5 100644
--- a/kittens/transfer/ftc_test.go
+++ b/kittens/transfer/ftc_test.go
@@ -18,11 +18,11 @@ func TestFTCSerialization(t *testing.T) {
q := func(expected string) {
actual := ftc.Serialize()
ad := make(map[string]bool)
- for _, x := range strings.Split(actual, ";") {
+ for x := range strings.SplitSeq(actual, ";") {
ad[x] = true
}
ed := make(map[string]bool)
- for _, x := range strings.Split(expected, ";") {
+ for x := range strings.SplitSeq(expected, ";") {
ed[x] = true
}
if diff := cmp.Diff(ed, ad); diff != "" {
diff --git a/kittens/transfer/send_test.go b/kittens/transfer/send_test.go
index 89969121bc2..0290a1c05f4 100644
--- a/kittens/transfer/send_test.go
+++ b/kittens/transfer/send_test.go
@@ -45,7 +45,7 @@ func TestPathMappingSend(t *testing.T) {
actual[f.expanded_local_path] = f.remote_path
}
e := make(map[string]string, len(actual))
- for _, rec := range strings.Split(expected, " ") {
+ for rec := range strings.SplitSeq(expected, " ") {
k, v, _ := strings.Cut(rec, ":")
e[mp(k, false)] = mp(v, true)
}
diff --git a/kitty/boss.py b/kitty/boss.py
index 3611fe9eb4a..d1d3efb87d5 100644
--- a/kitty/boss.py
+++ b/kitty/boss.py
@@ -941,9 +941,10 @@ def peer_message_received(self, msg_bytes: bytes, peer_id: int, is_remote_contro
assert isinstance(window.launch_spec, LaunchSpec)
launch(get_boss(), window.launch_spec.opts, window.launch_spec.args)
continue
+ wstate = args.start_as if args.start_as and args.start_as != 'normal' else None
os_window_id = self.add_os_window(
session, wclass=args.cls, wname=args.name, opts_for_size=opts, startup_id=startup_id,
- override_title=args.title or None)
+ override_title=args.title or None, window_state=wstate)
if session.focus_os_window:
focused_os_window = os_window_id
if opts.background_opacity != get_options().background_opacity:
@@ -1318,7 +1319,11 @@ def toggle_macos_secure_keyboard_entry(self) -> None:
@ac('misc', 'Cycle through OS windows on macOS')
def macos_cycle_through_os_windows(self) -> None:
- macos_cycle_through_os_windows()
+ macos_cycle_through_os_windows(False)
+
+ @ac('misc', 'Cycle through OS windows backwards on macOS')
+ def macos_cycle_through_os_windows_backwards(self) -> None:
+ macos_cycle_through_os_windows(True)
@ac('misc', 'Hide macOS kitty application')
def hide_macos_app(self) -> None:
@@ -2934,6 +2939,8 @@ def load_config_file(self, *paths: str, apply_overrides: bool = True, overrides:
from .guess_mime_type import clear_mime_cache
clear_mime_cache()
store_effective_config()
+ from .tab_bar import clear_caches
+ clear_caches()
def safe_delete_temp_file(self, path: str) -> None:
if is_path_in_temp_dir(path):
diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c
index a1351191481..4ed473a19c3 100644
--- a/kitty/child-monitor.c
+++ b/kitty/child-monitor.c
@@ -1241,6 +1241,7 @@ process_cocoa_pending_actions(void) {
if (cocoa_pending_actions[RELOAD_CONFIG]) { call_boss(load_config_file, NULL); }
if (cocoa_pending_actions[TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY]) { call_boss(toggle_macos_secure_keyboard_entry, NULL); }
if (cocoa_pending_actions[MACOS_CYCLE_THROUGH_OS_WINDOWS]) { call_boss(macos_cycle_through_os_windows, NULL); }
+ if (cocoa_pending_actions[MACOS_CYCLE_THROUGH_OS_WINDOWS_BACKWARDS]) { call_boss(macos_cycle_through_os_windows_backwards, NULL); }
if (cocoa_pending_actions[TOGGLE_FULLSCREEN]) { call_boss(toggle_fullscreen, NULL); }
if (cocoa_pending_actions[OPEN_KITTY_WEBSITE]) { call_boss(open_kitty_website, NULL); }
if (cocoa_pending_actions[HIDE]) { call_boss(hide_macos_app, NULL); }
diff --git a/kitty/clipboard.py b/kitty/clipboard.py
index b145f85900d..e0f73c3d10c 100644
--- a/kitty/clipboard.py
+++ b/kitty/clipboard.py
@@ -214,8 +214,12 @@ class ReadRequest(NamedTuple):
protocol_type: ProtocolType = ProtocolType.osc_52
human_name: str = ''
password: str = ''
+ otp_for_response: str = ''
def encode_response(self, status: str = 'DATA', mime: str = '', payload: bytes | memoryview = b'') -> bytes:
+ from base64 import standard_b64encode
+ def encode_b64(s: str) -> str:
+ return standard_b64encode(s.encode()).decode()
ans = f'{self.protocol_type.value};type=read:status={status}'
if status == 'OK' and self.is_primary_selection:
ans += ':loc=primary'
@@ -223,10 +227,11 @@ def encode_response(self, status: str = 'DATA', mime: str = '', payload: bytes |
ans += f':id={self.id}'
if mime:
ans += f':mime={encode_mime(mime)}'
+ if self.otp_for_response:
+ ans += f':pw={encode_b64(self.otp_for_response)}'
a = ans.encode('ascii')
if payload:
- import base64
- a += b';' + base64.standard_b64encode(payload)
+ a += b';' + standard_b64encode(payload)
return a
@@ -329,9 +334,13 @@ def data_for(self, mime: str = 'text/plain', offset: int = 0, size: int = -1) ->
class GrantedPermission:
- def __init__(self, read: bool = False, write: bool = False):
+ one_time: bool = False
+ write_ban: bool = False
+ read_ban: bool = False
+
+ def __init__(self, read: bool = False, write: bool = False, one_time: bool = False):
self.read, self.write = read, write
- self.write_ban = self.read_ban = False
+ self.one_time = one_time
class ClipboardRequestManager:
@@ -521,7 +530,12 @@ def cb(q: str) -> None:
default='d', window=window, title=_('A program wants to access the clipboard'))
def password_is_allowed_already(self, password: str, for_write: bool = False) -> bool:
- return (q := self.granted_passwords.get(password)) is not None and (q.write if for_write else q.read)
+ q = self.granted_passwords.get(password)
+ if q is not None:
+ if q.one_time:
+ self.granted_passwords.pop(password, None)
+ return q.write if for_write else q.read
+ return False
def fulfill_legacy_write_request(self, wr: WriteRequest, allowed: bool = True) -> None:
cp = get_boss().primary_selection if wr.is_primary_selection else get_boss().clipboard
@@ -543,7 +557,11 @@ def handle_read_request(self, rr: ReadRequest) -> None:
self.fulfill_read_request(rr, allowed=allowed)
def send_paste_event(self, is_primary_selection: bool) -> None:
+ from kitty.short_uuid import uuid4
+ pw = uuid4()
+ self.granted_passwords[pw] = GrantedPermission(read=True, one_time=True)
rr = ReadRequest(is_primary_selection=is_primary_selection, mime_types=(TARGETS_MIME,), protocol_type=ProtocolType.osc_5522)
+ rr = rr._replace(otp_for_response=pw)
self.fulfill_read_request(rr)
def fulfill_read_request(self, rr: ReadRequest, allowed: bool = True) -> None:
diff --git a/kitty/cocoa_window.h b/kitty/cocoa_window.h
index a723c5a537e..b7587f98911 100644
--- a/kitty/cocoa_window.h
+++ b/kitty/cocoa_window.h
@@ -31,6 +31,7 @@ typedef enum {
RELOAD_CONFIG,
TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY,
MACOS_CYCLE_THROUGH_OS_WINDOWS,
+ MACOS_CYCLE_THROUGH_OS_WINDOWS_BACKWARDS,
TOGGLE_FULLSCREEN,
OPEN_KITTY_WEBSITE,
HIDE,
@@ -51,7 +52,6 @@ void cocoa_system_beep(const char*);
void cocoa_set_activation_policy(bool);
bool cocoa_alt_option_key_pressed(unsigned long);
void cocoa_toggle_secure_keyboard_entry(void);
-void cocoa_cycle_through_os_windows(void);
void cocoa_hide(void);
void cocoa_clear_global_shortcuts(void);
void cocoa_hide_others(void);
diff --git a/kitty/cocoa_window.m b/kitty/cocoa_window.m
index 9ec07e30bf5..a2d533098b8 100644
--- a/kitty/cocoa_window.m
+++ b/kitty/cocoa_window.m
@@ -259,6 +259,7 @@ - (void)user_menu_action:(id)sender {
PENDING(reload_config, RELOAD_CONFIG)
PENDING(toggle_macos_secure_keyboard_entry, TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY)
PENDING(macos_cycle_through_os_windows, MACOS_CYCLE_THROUGH_OS_WINDOWS)
+PENDING(macos_cycle_through_os_windows_backwards, MACOS_CYCLE_THROUGH_OS_WINDOWS_BACKWARDS)
PENDING(toggle_fullscreen, TOGGLE_FULLSCREEN)
PENDING(open_kitty_website, OPEN_KITTY_WEBSITE)
PENDING(hide_macos_app, HIDE)
@@ -320,7 +321,7 @@ + (GlobalMenuTarget *) shared_instance
GlobalShortcut clear_terminal_and_scrollback, clear_screen, clear_scrollback, clear_last_command;
GlobalShortcut toggle_macos_secure_keyboard_entry, toggle_fullscreen, open_kitty_website;
GlobalShortcut hide_macos_app, hide_macos_other_apps, minimize_macos_window, quit;
- GlobalShortcut macos_cycle_through_os_windows;
+ GlobalShortcut macos_cycle_through_os_windows, macos_cycle_through_os_windows_backwards;
} GlobalShortcuts;
static GlobalShortcuts global_shortcuts;
@@ -338,7 +339,8 @@ + (GlobalMenuTarget *) shared_instance
else Q(clear_terminal_and_scrollback); else Q(clear_scrollback); else Q(clear_screen); else Q(clear_last_command);
else Q(reload_config); else Q(toggle_macos_secure_keyboard_entry); else Q(toggle_fullscreen);
else Q(open_kitty_website); else Q(hide_macos_app); else Q(hide_macos_other_apps);
- else Q(minimize_macos_window); else Q(quit); else Q(macos_cycle_through_os_windows);
+ else Q(minimize_macos_window); else Q(quit);
+ else Q(macos_cycle_through_os_windows); else Q(macos_cycle_through_os_windows_backwards);
#undef Q
if (gs == NULL) { PyErr_SetString(PyExc_KeyError, "Unknown shortcut name"); return NULL; }
int cocoa_mods;
@@ -808,6 +810,7 @@ - (void)quickAccessTerminal:(NSPasteboard *)pboard userData:(NSString *)userData
keyEquivalent:@""];
[windowMenu addItem:[NSMenuItem separatorItem]];
MENU_ITEM(windowMenu, @"Cycle Through OS Windows", macos_cycle_through_os_windows);
+ MENU_ITEM(windowMenu, @"Cycle Through OS Windows backwards", macos_cycle_through_os_windows_backwards);
[windowMenu addItem:[NSMenuItem separatorItem]];
[windowMenu addItemWithTitle:@"Bring All to Front"
action:@selector(arrangeInFront:)
@@ -924,20 +927,6 @@ - (void)quickAccessTerminal:(NSPasteboard *)pboard userData:(NSString *)userData
[[NSUserDefaults standardUserDefaults] setBool:k.isDesired forKey:@"SecureKeyboardEntry"];
}
-void
-cocoa_cycle_through_os_windows(void) {
- NSArray *windows = [NSApp orderedWindows];
- if (windows.count < 2) return;
-
- NSWindow *keyWindow = [NSApp keyWindow];
- NSUInteger index = [windows indexOfObject:keyWindow];
- NSUInteger nextIndex = (index + 1) % windows.count;
-
- NSWindow *nextWindow = windows[nextIndex];
- [nextWindow makeKeyAndOrderFront:nil];
-}
-
-
void
cocoa_hide(void) {
[[NSApplication sharedApplication] performSelectorOnMainThread:@selector(hide:) withObject:nil waitUntilDone:NO];
diff --git a/kitty/colors.py b/kitty/colors.py
index eec04cce6e8..e7e955dd029 100644
--- a/kitty/colors.py
+++ b/kitty/colors.py
@@ -10,7 +10,7 @@
from .config import parse_config
from .constants import config_dir
from .fast_data_types import Color, get_boss, get_options, glfw_get_system_color_theme, patch_color_profiles, patch_global_colors, set_os_window_chrome
-from .options.types import Options, nullable_colors
+from .options.types import Options, nullable_colors, special_colors
from .rgb import color_from_int
from .typing_compat import WindowType
@@ -52,6 +52,8 @@ def get_default_colors(self) -> ColorsSpec:
defval = getattr(defaults, name)
if isinstance(defval, Color):
ans[name] = int(defval)
+ for name in special_colors:
+ ans[name] = getattr(defaults, name)
self.default_colors = ans
self.default_background_image_options: BackgroundImageOptions = {
k: getattr(defaults, k) for k in BackgroundImageOptions.__optional_keys__} # type: ignore
@@ -190,8 +192,9 @@ def apply_theme(self, new_value: ColorSchemes, notify_on_bg_change: bool = True)
def parse_colors(args: Iterable[str | Iterable[str]], background_image_options: BackgroundImageOptions | None = None) -> Colors:
- colors: dict[str, Color | None] = {}
+ colors: dict[str, Color | None | int] = {}
nullable_color_map: dict[str, int | None] = {}
+ special_color_map: dict[str, int] = {}
transparent_background_colors = ()
for spec in args:
if isinstance(spec, str):
@@ -213,8 +216,13 @@ def parse_colors(args: Iterable[str | Iterable[str]], background_image_options:
if q is not False:
val = int(q) if isinstance(q, Color) else None
nullable_color_map[k] = val
+ for k in special_colors:
+ sq = colors.pop(k, None)
+ if isinstance(sq, int):
+ special_color_map[k] = sq
ans: dict[str, int | None] = {k: int(v) for k, v in colors.items() if isinstance(v, Color)}
ans.update(nullable_color_map)
+ ans.update(special_color_map)
return ans, transparent_background_colors
@@ -228,7 +236,10 @@ def patch_options_with_color_spec(
if k in nullable_colors:
setattr(opts, k, None)
else:
- setattr(opts, k, color_from_int(v))
+ if k in special_colors:
+ setattr(opts, k, v)
+ else:
+ setattr(opts, k, color_from_int(v))
opts.transparent_background_colors = transparent_background_colors
if background_image_options is not None:
for k, bv in background_image_options.items():
diff --git a/kitty/disk-cache.c b/kitty/disk-cache.c
index bcab2fb5b0c..83e85bb6554 100644
--- a/kitty/disk-cache.c
+++ b/kitty/disk-cache.c
@@ -28,7 +28,7 @@ typedef struct CacheKey {
typedef struct {
uint8_t *data;
size_t data_sz;
- bool written_to_disk;
+ bool written_to_disk, uses_encryption;
off_t pos_in_cache_file;
uint8_t encryption_key[64];
} CacheValue;
@@ -87,6 +87,8 @@ typedef struct {
cache_map map;
Holes holes;
unsigned long long total_size;
+ off_t end_of_data_offset;
+ bool needs_encryption;
} DiskCache;
#define mutex(op) pthread_mutex_##op(&self->lock)
@@ -99,6 +101,7 @@ new_diskcache_object(PyTypeObject *type, PyObject UNUSED *args, PyObject UNUSED
self->cache_file_fd = -1;
self->small_hole_threshold = 512;
self->defrag_factor = 2;
+ self->needs_encryption = true;
}
return (PyObject*) self;
}
@@ -120,14 +123,16 @@ open_cache_file_without_tmpfile(const char *cache_path) {
}
static int
-open_cache_file(const char *cache_path) {
+open_cache_file(const char *cache_path, bool *opened_securely) {
int fd = -1;
+ *opened_securely = false;
#ifdef O_TMPFILE
while (fd < 0) {
fd = safe_open(cache_path, O_TMPFILE | O_CLOEXEC | O_EXCL | O_RDWR, S_IRUSR | S_IWUSR);
if (fd > -1 || errno != EINTR) break;
}
if (fd == -1) fd = open_cache_file_without_tmpfile(cache_path);
+ else *opened_securely = true;
#else
fd = open_cache_file_without_tmpfile(cache_path);
#endif
@@ -136,23 +141,6 @@ open_cache_file(const char *cache_path) {
// Write loop {{{
-static off_t
-size_of_cache_file(DiskCache *self) {
- off_t pos = lseek(self->cache_file_fd, 0, SEEK_CUR);
- off_t ans = lseek(self->cache_file_fd, 0, SEEK_END);
- lseek(self->cache_file_fd, pos, SEEK_SET);
- return ans;
-}
-
-size_t
-disk_cache_size_on_disk(PyObject *self) {
- if (((DiskCache*)self)->cache_file_fd > -1) {
- off_t ans = size_of_cache_file((DiskCache*)self);
- return MAX(0, ans);
- }
- return 0;
-}
-
typedef struct {
CacheKey key;
off_t old_offset, new_offset;
@@ -181,11 +169,12 @@ defrag(DiskCache *self) {
RAII_FreeFastFileCopyBuffer(fcb);
bool lock_released = false, ok = false;
- off_t size_on_disk = size_of_cache_file(self);
+ off_t size_on_disk = self->end_of_data_offset;
if (size_on_disk <= 0) goto cleanup;
size_t num_entries = vt_size(&self->map);
if (!num_entries) goto cleanup;
- new_cache_file = open_cache_file(self->cache_dir);
+ bool opened_securely;
+ new_cache_file = open_cache_file(self->cache_dir, &opened_securely);
if (new_cache_file < 0) {
perror("Failed to open second file for defrag of disk cache");
goto cleanup;
@@ -236,6 +225,8 @@ defrag(DiskCache *self) {
if (!vt_is_end(i)) i.data->val->pos_in_cache_file = e->new_offset;
free(e->key.hash_key);
}
+ self->end_of_data_offset = lseek(self->cache_file_fd, 0, SEEK_CUR);
+ self->needs_encryption = !opened_securely;
}
if (new_cache_file > -1) safe_close(new_cache_file, __FILE__, __LINE__);
}
@@ -306,8 +297,7 @@ find_hole_to_use(DiskCache *self, const off_t required_sz) {
static inline bool
needs_defrag(DiskCache *self) {
- off_t size_on_disk = size_of_cache_file(self);
- return self->total_size && size_on_disk > 0 && (size_t)size_on_disk > self->total_size * self->defrag_factor;
+ return self->total_size && self->end_of_data_offset > 0 && (size_t)self->end_of_data_offset > self->total_size * self->defrag_factor;
}
static void
@@ -376,7 +366,11 @@ find_cache_entry_to_write(DiskCache *self) {
s->data = NULL;
self->currently_writing.val.data_sz = s->data_sz;
self->currently_writing.val.pos_in_cache_file = -1;
- xor_data64(s->encryption_key, self->currently_writing.val.data, s->data_sz);
+ s->uses_encryption = false;
+ if (self->needs_encryption && secure_random_bytes(s->encryption_key, sizeof(s->encryption_key))) {
+ xor_data64(s->encryption_key, self->currently_writing.val.data, s->data_sz);
+ s->uses_encryption = true;
+ }
self->currently_writing.key.hash_keylen = MIN(i.data->key.hash_keylen, MAX_KEY_SIZE);
memcpy(self->currently_writing.key.hash_key, i.data->key.hash_key, self->currently_writing.key.hash_keylen);
find_hole_to_use(self, self->currently_writing.val.data_sz);
@@ -395,7 +389,7 @@ write_dirty_entry(DiskCache *self) {
size_t left = self->currently_writing.val.data_sz;
uint8_t *p = self->currently_writing.val.data;
if (self->currently_writing.val.pos_in_cache_file < 0) {
- self->currently_writing.val.pos_in_cache_file = size_of_cache_file(self);
+ self->currently_writing.val.pos_in_cache_file = self->end_of_data_offset;
if (self->currently_writing.val.pos_in_cache_file < 0) {
perror("Failed to seek in disk cache file");
return false;
@@ -418,6 +412,7 @@ write_dirty_entry(DiskCache *self) {
left -= n;
p += n;
offset += n;
+ self->end_of_data_offset = MAX(self->end_of_data_offset, offset);
}
return true;
}
@@ -434,6 +429,18 @@ retire_currently_writing(DiskCache *self) {
self->currently_writing.val.data_sz = 0;
}
+static int
+clear_disk_cache_with_lock_held(DiskCache *self) {
+ vt_cleanup(&self->map);
+ cleanup_holes(&self->holes);
+ self->total_size = 0;
+ self->end_of_data_offset = 0;
+ if (self->cache_file_fd > -1) {
+ if (ftruncate(self->cache_file_fd, 0) == -1) return errno;
+ }
+ return 0;
+}
+
static void*
write_loop(void *data) {
DiskCache *self = (DiskCache*)data;
@@ -457,9 +464,7 @@ write_loop(void *data) {
} else if (!count) {
mutex(lock);
count = vt_size(&self->map);
- if (!count && self->cache_file_fd > -1) {
- if (ftruncate(self->cache_file_fd, 0) == 0) lseek(self->cache_file_fd, 0, SEEK_END);
- }
+ if (!count) clear_disk_cache_with_lock_held(self); // failure to truncate is not fatal
mutex(unlock);
}
@@ -517,11 +522,13 @@ ensure_state(DiskCache *self) {
}
if (self->cache_file_fd < 0) {
- self->cache_file_fd = open_cache_file(self->cache_dir);
+ bool opened_securely;
+ self->cache_file_fd = open_cache_file(self->cache_dir, &opened_securely);
if (self->cache_file_fd < 0) {
PyErr_SetFromErrnoWithFilename(PyExc_OSError, self->cache_dir);
return false;
}
+ self->needs_encryption = !opened_securely;
}
vt_init(&self->map); vt_init(&self->holes.pos_map); vt_init(&self->holes.size_map); vt_init(&self->holes.end_pos_map);
self->fully_initialized = true;
@@ -566,7 +573,6 @@ static CacheValue*
create_cache_entry(void) {
CacheValue *s = calloc(1, sizeof(CacheValue));
if (!s) return (CacheValue*)PyErr_NoMemory();
- if (!secure_random_bytes(s->encryption_key, sizeof(s->encryption_key))) { free(s); PyErr_SetFromErrno(PyExc_OSError); return NULL; }
s->pos_in_cache_file = -2;
return s;
}
@@ -628,17 +634,18 @@ remove_from_disk_cache(PyObject *self_, const void *key, size_t key_sz) {
return removed;
}
-void
+static int
clear_disk_cache(PyObject *self_) {
+ // This is currently only used in testing
DiskCache *self = (DiskCache*)self_;
- if (!ensure_state(self)) return;
+ if (!ensure_state(self)) return 0;
+ int saved_errno = 0;
+ disk_cache_wait_for_write(self_, 0);
mutex(lock);
- vt_cleanup(&self->map);
- cleanup_holes(&self->holes);
- self->total_size = 0;
- if (self->cache_file_fd > -1) add_hole(self, 0, size_of_cache_file(self));
+ saved_errno = clear_disk_cache_with_lock_held(self);
mutex(unlock);
wakeup_write_loop(self);
+ return saved_errno;
}
static void
@@ -693,11 +700,11 @@ read_from_disk_cache(PyObject *self_, const void *key, size_t key_sz, void*(allo
if (s->data) { memcpy(data, s->data, s->data_sz); }
else if (self->currently_writing.val.data && self->currently_writing.key.hash_key && keys_are_equal(self->currently_writing.key, k)) {
memcpy(data, self->currently_writing.val.data, s->data_sz);
- xor_data64(s->encryption_key, data, s->data_sz);
+ if (s->uses_encryption) xor_data64(s->encryption_key, data, s->data_sz);
}
else {
read_from_cache_entry(self, s, data);
- xor_data64(s->encryption_key, data, s->data_sz);
+ if (s->uses_encryption) xor_data64(s->encryption_key, data, s->data_sz);
}
if (store_in_ram && !s->data && s->data_sz) {
void *copy = malloc(s->data_sz);
@@ -768,6 +775,7 @@ disk_cache_num_cached_in_ram(PyObject *self_) {
}
+// The Python interface used only for testing {{{
#define PYWRAP(name) static PyObject* py##name(DiskCache *self, PyObject *args)
#define PA(fmt, ...) if (!PyArg_ParseTuple(args, fmt, __VA_ARGS__)) return NULL;
PYWRAP(ensure_state) {
@@ -776,19 +784,6 @@ PYWRAP(ensure_state) {
Py_RETURN_NONE;
}
-PYWRAP(read_from_cache_file) {
- Py_ssize_t pos = 0, sz = -1;
- PA("|nn", &pos, &sz);
- mutex(lock);
- if (sz < 0) sz = size_of_cache_file(self);
- mutex(unlock);
- PyObject *ans = PyBytes_FromStringAndSize(NULL, sz);
- if (ans) {
- read_from_cache_file(self, pos, sz, PyBytes_AS_STRING(ans));
- }
- return ans;
-}
-
static PyObject*
wait_for_write(PyObject *self, PyObject *args) {
double timeout = 0;
@@ -798,17 +793,20 @@ wait_for_write(PyObject *self, PyObject *args) {
}
static PyObject*
-size_on_disk(PyObject *self_, PyObject *args UNUSED) {
+end_of_data_offset(PyObject *self_, PyObject *args UNUSED) {
+ // Only used for testing
DiskCache *self = (DiskCache*)self_;
+ unsigned long long ans = 0;
mutex(lock);
- unsigned long long ans = disk_cache_size_on_disk(self_);
+ if (self->cache_file_fd > -1) ans = MAX(0, self->end_of_data_offset);
mutex(unlock);
return PyLong_FromUnsignedLongLong(ans);
}
static PyObject*
clear(PyObject *self, PyObject *args UNUSED) {
- clear_disk_cache(self);
+ int saved_errno = clear_disk_cache(self);
+ if (saved_errno) return PyErr_SetFromErrno(PyExc_OSError);
Py_RETURN_NONE;
}
@@ -903,14 +901,13 @@ num_cached_in_ram(PyObject *self, PyObject *args UNUSED) {
#define MW(name, arg_type) {#name, (PyCFunction)py##name, arg_type, NULL}
static PyMethodDef methods[] = {
MW(ensure_state, METH_NOARGS),
- MW(read_from_cache_file, METH_VARARGS),
{"add", add, METH_VARARGS, NULL},
{"remove", pyremove, METH_VARARGS, NULL},
{"remove_from_ram", remove_from_ram, METH_O, NULL},
{"num_cached_in_ram", num_cached_in_ram, METH_NOARGS, NULL},
{"get", get, METH_VARARGS, NULL},
{"wait_for_write", wait_for_write, METH_VARARGS, NULL},
- {"size_on_disk", size_on_disk, METH_NOARGS, NULL},
+ {"end_of_data_offset", end_of_data_offset, METH_NOARGS, NULL},
{"clear", clear, METH_NOARGS, NULL},
{"holes", holes, METH_NOARGS, NULL},
@@ -939,3 +936,4 @@ PyTypeObject DiskCache_Type = {
INIT_TYPE(DiskCache)
PyObject* create_disk_cache(void) { return new_diskcache_object(&DiskCache_Type, NULL, NULL); }
+// }}}
diff --git a/kitty/disk-cache.h b/kitty/disk-cache.h
index de48556aec3..d6c1ea26fd7 100644
--- a/kitty/disk-cache.h
+++ b/kitty/disk-cache.h
@@ -16,7 +16,6 @@ PyObject* read_from_disk_cache_python(PyObject *self_, const void *key, size_t k
bool disk_cache_wait_for_write(PyObject *self, monotonic_t timeout);
size_t disk_cache_total_size(PyObject *self);
size_t disk_cache_size_on_disk(PyObject *self);
-void clear_disk_cache(PyObject *self);
size_t disk_cache_clear_from_ram(PyObject *self_, bool(matches)(void* data, void *key, unsigned keysz), void*);
size_t disk_cache_num_cached_in_ram(PyObject *self_);
diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi
index f2f5fa7c4cc..b3681c2ba31 100644
--- a/kitty/fast_data_types.pyi
+++ b/kitty/fast_data_types.pyi
@@ -948,7 +948,7 @@ def toggle_secure_input() -> None:
pass
-def macos_cycle_through_os_windows() -> None:
+def macos_cycle_through_os_windows(backwards: bool) -> None:
pass
@@ -1798,19 +1798,3 @@ class StreamingBase64Encodeer:
def reset(self) -> bytes: ...
# encode the specified data, return number of bytes written dest should be at least 4/3 *src + 2 bytes in size
def encode_into(self, dest: WriteableBuffer, src: ReadableBuffer) -> int: ...
-
-
-
-class DiskCache:
- small_hole_threshold: int
- defrag_factor: int
- @property
- def total_size(self) -> int: ...
-
- def add(self, key: bytes, data: bytes) -> None: ...
- def remove(self, key: bytes) -> bool: ...
- def remove_from_ram(self, predicate: Callable[[bytes], bool]) -> int: ...
- def num_cached_in_ram(self) -> int: ...
- def get(self, key: bytes, store_in_ram: bool = False) -> bytes: ... # raises KeyError if not found
- def size_on_disk(self) -> int: ...
- def clear(self) -> None: ...
diff --git a/kitty/gl.c b/kitty/gl.c
index 61c26c1dacd..7ced321f915 100644
--- a/kitty/gl.c
+++ b/kitty/gl.c
@@ -215,7 +215,7 @@ save_texture_as_png(uint32_t texture_id, const char *filename) {
data[i] = (r << 0) | (g << 8) | (b << 16) | (a << 24);
}
- const char *png = png_from_32bit_rgba(data, width, height, &sz, true);
+ const char *png = png_from_32bit_rgba((char*)data, width, height, &sz, true);
if (!sz) fatal("Failed to save PNG to %s with error: %s", filename, png);
free(data);
FILE* file = fopen(filename, "wb");
diff --git a/kitty/glfw-wrapper.c b/kitty/glfw-wrapper.c
index fdc2f427d3b..8e257fcc093 100644
--- a/kitty/glfw-wrapper.c
+++ b/kitty/glfw-wrapper.c
@@ -473,6 +473,9 @@ load_glfw(const char* path) {
*(void **) (&glfwSetPrimarySelectionString_impl) = dlsym(handle, "glfwSetPrimarySelectionString");
if (glfwSetPrimarySelectionString_impl == NULL) dlerror(); // clear error indicator
+ *(void **) (&glfwCocoaCycleThroughOSWindows_impl) = dlsym(handle, "glfwCocoaCycleThroughOSWindows");
+ if (glfwCocoaCycleThroughOSWindows_impl == NULL) dlerror(); // clear error indicator
+
*(void **) (&glfwCocoaSetWindowChrome_impl) = dlsym(handle, "glfwCocoaSetWindowChrome");
if (glfwCocoaSetWindowChrome_impl == NULL) dlerror(); // clear error indicator
diff --git a/kitty/glfw-wrapper.h b/kitty/glfw-wrapper.h
index f1553d42e52..74850dbf29d 100644
--- a/kitty/glfw-wrapper.h
+++ b/kitty/glfw-wrapper.h
@@ -2330,6 +2330,10 @@ typedef void (*glfwSetPrimarySelectionString_func)(GLFWwindow*, const char*);
GFW_EXTERN glfwSetPrimarySelectionString_func glfwSetPrimarySelectionString_impl;
#define glfwSetPrimarySelectionString glfwSetPrimarySelectionString_impl
+typedef void (*glfwCocoaCycleThroughOSWindows_func)(bool);
+GFW_EXTERN glfwCocoaCycleThroughOSWindows_func glfwCocoaCycleThroughOSWindows_impl;
+#define glfwCocoaCycleThroughOSWindows glfwCocoaCycleThroughOSWindows_impl
+
typedef void (*glfwCocoaSetWindowChrome_func)(GLFWwindow*, unsigned int, bool, unsigned int, int, unsigned int, bool, int, float, bool);
GFW_EXTERN glfwCocoaSetWindowChrome_func glfwCocoaSetWindowChrome_impl;
#define glfwCocoaSetWindowChrome glfwCocoaSetWindowChrome_impl
diff --git a/kitty/glfw.c b/kitty/glfw.c
index b62d4ae477e..54556a95bff 100644
--- a/kitty/glfw.c
+++ b/kitty/glfw.c
@@ -1922,9 +1922,11 @@ toggle_secure_input(PYNOARG) {
}
static PyObject*
-macos_cycle_through_os_windows(PYNOARG) {
+macos_cycle_through_os_windows(PyObject *self UNUSED, PyObject *backwards) {
#ifdef __APPLE__
- cocoa_cycle_through_os_windows();
+ glfwCocoaCycleThroughOSWindows(PyObject_IsTrue(backwards));
+#else
+ (void)backwards;
#endif
Py_RETURN_NONE;
}
@@ -2616,7 +2618,7 @@ static PyMethodDef module_methods[] = {
METHODB(set_clipboard_data_types, METH_VARARGS),
METHODB(get_clipboard_mime, METH_VARARGS),
METHODB(toggle_secure_input, METH_NOARGS),
- METHODB(macos_cycle_through_os_windows, METH_NOARGS),
+ METHODB(macos_cycle_through_os_windows, METH_O),
METHODB(get_content_scale_for_window, METH_NOARGS),
METHODB(ring_bell, METH_VARARGS),
METHODB(toggle_fullscreen, METH_VARARGS),
diff --git a/kitty/graphics.c b/kitty/graphics.c
index c1b8c5c7085..f985a757564 100644
--- a/kitty/graphics.c
+++ b/kitty/graphics.c
@@ -23,6 +23,7 @@
#include "png-reader.h"
PyTypeObject GraphicsManager_Type;
+#define MAX_IMAGE_DIMENSION 10000u
#define DEFAULT_STORAGE_LIMIT 320u * (1024u * 1024u)
#define REPORT_ERROR(...) { log_error(__VA_ARGS__); }
#define RAII_CoalescedFrameData(name, initializer) __attribute__((cleanup(cfd_free))) CoalescedFrameData name = initializer
@@ -380,7 +381,7 @@ png_error_handler(png_read_data *d UNUSED, const char *code, const char *msg) {
static bool
inflate_png(LoadData *load_data, uint8_t *buf, size_t bufsz) {
png_read_data d = {.err_handler=png_error_handler};
- inflate_png_inner(&d, buf, bufsz);
+ inflate_png_inner(&d, buf, bufsz, MAX_IMAGE_DIMENSION);
if (d.ok) {
free_load_data(load_data);
load_data->buf = d.decompressed;
@@ -415,7 +416,7 @@ print_png_read_error(png_read_data *d, const char *code, const char* msg) {
bool
png_from_data(void *png_data, size_t png_data_sz, const char *path_for_error_messages, uint8_t** data, unsigned int* width, unsigned int* height, size_t* sz) {
png_read_data d = {.err_handler=print_png_read_error};
- inflate_png_inner(&d, png_data, png_data_sz);
+ inflate_png_inner(&d, png_data, png_data_sz, MAX_IMAGE_DIMENSION);
if (!d.ok) {
log_error("Failed to decode PNG image at: %s with error: %s", path_for_error_messages, d.error.used > 0 ? d.error.buf : "");
free(d.decompressed); free(d.row_pointers); free(d.error.buf);
@@ -693,7 +694,6 @@ initialize_load_data(GraphicsManager *self, const GraphicsCommand *g, Image *img
tt = g->transmission_type ? g->transmission_type : 'd'; \
fmt = g->format ? g->format : RGBA; \
}
-#define MAX_IMAGE_DIMENSION 10000u
static void
upload_to_gpu(GraphicsManager *self, Image *img, const bool is_opaque, const bool is_4byte_aligned, const uint8_t *data) {
@@ -1574,7 +1574,7 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I
const unsigned long bytes_per_pixel = load_data->is_opaque ? 3 : 4;
if (load_data->data_sz < bytes_per_pixel * load_data->width * load_data->height)
- ABRT("ENODATA", "Insufficient image data %zu < %zu", load_data->data_sz, bytes_per_pixel * g->data_width, g->data_height);
+ ABRT("ENODATA", "Insufficient image data %zu < %zu", load_data->data_sz, bytes_per_pixel * g->data_width * g->data_height);
if (load_data->width > img->width)
ABRT("EINVAL", "Frame width %u larger than image width: %u", load_data->width, img->width);
if (load_data->height > img->height)
@@ -1590,7 +1590,7 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I
.x = g->x_offset, .y = g->y_offset,
.is_4byte_aligned = load_data->is_4byte_aligned,
.is_opaque = load_data->is_opaque,
- .alpha_blend = g->blend_mode != 1 && !load_data->is_opaque,
+ .alpha_blend = g->compose_mode != 1 && !load_data->is_opaque,
.gap = g->gap > 0 ? g->gap : (g->gap < 0) ? 0 : DEFAULT_GAP,
.bgcolor = g->bgcolor,
};
diff --git a/kitty/graphics.h b/kitty/graphics.h
index 50de464b51f..84e5d6d5713 100644
--- a/kitty/graphics.h
+++ b/kitty/graphics.h
@@ -13,7 +13,7 @@ typedef struct {
uint32_t format, more, id, image_number, data_sz, data_offset, placement_id, quiet, parent_id, parent_placement_id;
uint32_t width, height, x_offset, y_offset;
union { uint32_t cursor_movement, compose_mode; };
- union { uint32_t cell_x_offset, blend_mode; };
+ union { uint32_t cell_x_offset; };
union { uint32_t cell_y_offset, bgcolor; };
union { uint32_t data_width, animation_state; };
union { uint32_t data_height, loop_count; };
diff --git a/kitty/launch.py b/kitty/launch.py
index 948fcb05eed..233fedf5491 100644
--- a/kitty/launch.py
+++ b/kitty/launch.py
@@ -134,7 +134,7 @@ def options_spec() -> str:
--cwd
completion=type:directory kwds:current,oldest,last_reported,root
The working directory for the newly launched child. Use the special value
-:code:`current` to use the working directory of the :option:`source window `
+:code:`current` to use the working directory of the :option:`source window `.
The special value :code:`last_reported` uses the last working directory reported
by the shell (needs :ref:`shell_integration` to work). The special value
:code:`oldest` works like :code:`current` but uses the working directory of the
diff --git a/kitty/layout/base.py b/kitty/layout/base.py
index 8e55e893c8a..273e1b9bc68 100644
--- a/kitty/layout/base.py
+++ b/kitty/layout/base.py
@@ -9,8 +9,8 @@
from kitty.borders import BorderColor
from kitty.fast_data_types import Region, get_options, set_active_window, viewport_for_window
from kitty.options.types import Options
-from kitty.types import Edges, WindowGeometry, WindowMapper
-from kitty.typing_compat import TypedDict, WindowType
+from kitty.types import Edges, NeighborsMap, WindowGeometry, WindowMapper
+from kitty.typing_compat import WindowType
from kitty.window_list import WindowGroup, WindowList
@@ -41,13 +41,6 @@ class LayoutData(NamedTuple):
ListOfWindows = list[WindowType]
-class NeighborsMap(TypedDict):
- left: list[int]
- top: list[int]
- right: list[int]
- bottom: list[int]
-
-
class LayoutGlobalData:
draw_minimal_borders: bool = True
draw_active_borders: bool = True
@@ -437,7 +430,7 @@ def do_layout(self, windows: WindowList) -> None:
raise NotImplementedError()
def neighbors_for_window(self, window: WindowType, windows: WindowList) -> NeighborsMap:
- return {'left': [], 'right': [], 'top': [], 'bottom': []}
+ return {}
def compute_needs_borders_map(self, all_windows: WindowList) -> dict[int, bool]:
return all_windows.compute_needs_borders_map(lgd.draw_active_borders)
diff --git a/kitty/layout/grid.py b/kitty/layout/grid.py
index f62ea5206e7..75abd6e36dd 100644
--- a/kitty/layout/grid.py
+++ b/kitty/layout/grid.py
@@ -8,11 +8,11 @@
from typing import Any
from kitty.borders import BorderColor
-from kitty.types import Edges, WindowMapper
+from kitty.types import Edges, NeighborsMap, WindowMapper
from kitty.typing_compat import WindowType
from kitty.window_list import WindowGroup, WindowList
-from .base import BorderLine, Layout, LayoutData, LayoutDimension, ListOfWindows, NeighborsMap, layout_dimension, lgd
+from .base import BorderLine, Layout, LayoutData, LayoutDimension, ListOfWindows, layout_dimension, lgd
from .tall import neighbors_for_tall_window
@@ -302,12 +302,16 @@ def side(row: int, col: int, delta: int) -> list[int]:
xs.extend(neighbors(neighbor_row, neighbor_col))
return xs
- return {
- 'top': neighbors(row-1, col) if row else [],
- 'bottom': neighbors(row + 1, col),
- 'left': side(row, col, -1) if col else [],
- 'right': side(row, col, 1) if col < ncols - 1 else [],
- }
+ ans: NeighborsMap = {}
+ if row:
+ ans['top'] = neighbors(row-1, col)
+ if bottom := neighbors(row + 1, col):
+ ans['bottom'] = bottom
+ if col and (left := side(row, col, -1)):
+ ans['left'] = left
+ if col < ncols - 1:
+ ans['right'] = side(row, col, 1)
+ return ans
def layout_state(self) -> dict[str, Any]:
return {
diff --git a/kitty/layout/splits.py b/kitty/layout/splits.py
index bb83e3deda9..c13f069a7c4 100644
--- a/kitty/layout/splits.py
+++ b/kitty/layout/splits.py
@@ -5,11 +5,11 @@
from typing import Any, NamedTuple, Optional, TypedDict, Union
from kitty.borders import BorderColor
-from kitty.types import Edges, WindowGeometry, WindowMapper
+from kitty.types import Edges, NeighborsMap, WindowGeometry, WindowMapper
from kitty.typing_compat import EdgeLiteral, WindowType
from kitty.window_list import WindowGroup, WindowList
-from .base import BorderLine, Layout, LayoutOpts, NeighborsMap, blank_rects_for_window, lgd, window_geometry_from_layouts
+from .base import BorderLine, Layout, LayoutOpts, blank_rects_for_window, lgd, window_geometry_from_layouts
class Extent(NamedTuple):
@@ -405,14 +405,14 @@ def quadrant(is_horizontal: bool, is_first: bool) -> tuple[EdgeLiteral, EdgeLite
geometries = {group.id: group.geometry for group in all_windows.groups if group.geometry}
def extend(other: Union[int, 'Pair', None], edge: EdgeLiteral, which: EdgeLiteral) -> None:
- if not ans[which] and other:
+ if not ans.get(which) and other:
if isinstance(other, Pair):
neighbors = (
w for w in other.edge_windows(edge)
if is_neighbouring_geometry(geometries[w], geometries[window_id], which))
- ans[which].extend(neighbors)
+ ans.setdefault(which, []).extend(neighbors)
else:
- ans[which].append(other)
+ ans.setdefault(which, []).append(other)
def is_neighbouring_geometry(a: WindowGeometry, b: WindowGeometry, direction: str) -> bool:
def edges(g: WindowGeometry) -> tuple[int, int]:
@@ -610,7 +610,7 @@ def neighbors_for_window(self, window: WindowType, all_windows: WindowList) -> N
wg = all_windows.group_for_window(window)
assert wg is not None
pair = self.pairs_root.pair_for_window(wg.id)
- ans: NeighborsMap = {'left': [], 'right': [], 'top': [], 'bottom': []}
+ ans: NeighborsMap = {}
if pair is not None:
pair.neighbors_for_window(wg.id, ans, self, all_windows)
return ans
diff --git a/kitty/layout/stack.py b/kitty/layout/stack.py
index 6c65a0dd130..c855b2c64a8 100644
--- a/kitty/layout/stack.py
+++ b/kitty/layout/stack.py
@@ -1,10 +1,11 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2020, Kovid Goyal
+from kitty.types import NeighborsMap
from kitty.typing_compat import WindowType
from kitty.window_list import WindowList
-from .base import Layout, NeighborsMap
+from .base import Layout
class Stack(Layout):
diff --git a/kitty/layout/tall.py b/kitty/layout/tall.py
index 96292523911..0d2737971a9 100644
--- a/kitty/layout/tall.py
+++ b/kitty/layout/tall.py
@@ -7,7 +7,7 @@
from kitty.borders import BorderColor
from kitty.conf.utils import to_bool
-from kitty.types import Edges, WindowMapper
+from kitty.types import Edges, NeighborsMap, WindowMapper
from kitty.typing_compat import EdgeLiteral, WindowType
from kitty.window_list import WindowGroup, WindowList
@@ -17,7 +17,6 @@
LayoutData,
LayoutDimension,
LayoutOpts,
- NeighborsMap,
lgd,
normalize_biases,
safe_increment_bias,
@@ -38,7 +37,7 @@ def neighbors_for_tall_window(
idx = groups.index(wg)
prev = None if idx == 0 else groups[idx-1]
nxt = None if idx == len(groups) - 1 else groups[idx+1]
- ans: NeighborsMap = {'left': [], 'right': [], 'top': [], 'bottom': []}
+ ans: NeighborsMap = {}
main_before: EdgeLiteral = 'left' if main_is_horizontal else 'top'
main_after: EdgeLiteral = 'right' if main_is_horizontal else 'bottom'
cross_before: EdgeLiteral = 'top' if main_is_horizontal else 'left'
diff --git a/kitty/layout/vertical.py b/kitty/layout/vertical.py
index 9e9e4b23ac7..000963acc4d 100644
--- a/kitty/layout/vertical.py
+++ b/kitty/layout/vertical.py
@@ -5,11 +5,11 @@
from typing import Any
from kitty.borders import BorderColor
-from kitty.types import Edges, WindowMapper
-from kitty.typing_compat import WindowType
+from kitty.types import Edges, NeighborsMap, WindowMapper
+from kitty.typing_compat import EdgeLiteral, WindowType
from kitty.window_list import WindowGroup, WindowList
-from .base import BorderLine, Layout, LayoutData, LayoutDimension, NeighborsMap, lgd
+from .base import BorderLine, Layout, LayoutData, LayoutDimension, lgd
def borders(
@@ -133,9 +133,16 @@ def neighbors_for_window(self, window: WindowType, all_windows: WindowList) -> N
after = [groups[(idx + 1) % lg].id]
else:
before, after = [], []
+ ans: NeighborsMap = {}
+ akey: EdgeLiteral = 'top'
+ bkey: EdgeLiteral = 'bottom'
if self.main_is_horizontal:
- return {'left': before, 'right': after, 'top': [], 'bottom': []}
- return {'top': before, 'bottom': after, 'left': [], 'right': []}
+ akey, bkey = 'left', 'right'
+ if before:
+ ans[bkey] = before
+ if after:
+ ans[akey] = after
+ return ans
def layout_state(self) -> dict[str, Any]:
return {'biased_map': self.biased_map}
diff --git a/kitty/main.py b/kitty/main.py
index f862659d2cd..15564a09903 100644
--- a/kitty/main.py
+++ b/kitty/main.py
@@ -193,7 +193,7 @@ def set_cocoa_global_shortcuts(opts: Options) -> dict[str, SingleKey]:
for ac in ('new_os_window', 'close_os_window', 'close_tab', 'edit_config_file', 'previous_tab',
'next_tab', 'new_tab', 'new_window', 'close_window', 'toggle_macos_secure_keyboard_entry',
- 'toggle_fullscreen', 'macos_cycle_through_os_windows',
+ 'toggle_fullscreen', 'macos_cycle_through_os_windows', 'macos_cycle_through_os_windows_backwards',
'hide_macos_app', 'hide_macos_other_apps', 'minimize_macos_window', 'quit'):
val = get_macos_shortcut_for(func_map, ac)
if val is not None:
diff --git a/kitty/options/definition.py b/kitty/options/definition.py
index 0544a413baa..ef7e17af22d 100644
--- a/kitty/options/definition.py
+++ b/kitty/options/definition.py
@@ -4429,6 +4429,7 @@
)
map('macOS Cycle through OS Windows', 'macos_cycle_through_os_windows cmd+` macos_cycle_through_os_windows', only='macos')
+map('macOS Cycle through OS Windows backwards', 'macos_cycle_through_os_windows_backwards cmd+shift+` macos_cycle_through_os_windows_backwards', only='macos')
map('Unicode input',
'input_unicode_character kitty_mod+u kitten unicode_input',
diff --git a/kitty/options/types.py b/kitty/options/types.py
index 61ca32c37eb..c15a34e08c7 100644
--- a/kitty/options/types.py
+++ b/kitty/options/types.py
@@ -1006,6 +1006,7 @@ def __setattr__(self, key: str, val: typing.Any) -> typing.Any:
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=12, key=102), definition='toggle_fullscreen'))
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=10, key=115), definition='toggle_macos_secure_keyboard_entry'))
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=96), definition='macos_cycle_through_os_windows'))
+ defaults.map.append(KeyDefinition(trigger=SingleKey(mods=9, key=96), definition='macos_cycle_through_os_windows_backwards'))
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=12, key=32), definition='kitten unicode_input'))
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=44), definition='edit_config_file'))
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=10, key=114), definition='clear_terminal reset active'))
@@ -1095,5 +1096,12 @@ def __setattr__(self, key: str, val: typing.Any) -> typing.Any:
'selection_background'
})
+special_colors = frozenset({
+ 'scrollbar_handle_color',
+ 'scrollbar_track_color',
+ 'wayland_titlebar_color',
+ 'macos_titlebar_color'
+})
+
secret_options = ('remote_control_password', 'file_transfer_confirmation_bypass')
\ No newline at end of file
diff --git a/kitty/png-reader.c b/kitty/png-reader.c
index 740d5eab0a7..c666d59be92 100644
--- a/kitty/png-reader.c
+++ b/kitty/png-reader.c
@@ -47,7 +47,7 @@ read_png_warn_handler(png_structp UNUSED png_ptr, png_const_charp msg) {
#define ABRT(code, msg) { if(d->err_handler) d->err_handler(d, #code, msg); goto err; }
void
-inflate_png_inner(png_read_data *d, const uint8_t *buf, size_t bufsz) {
+inflate_png_inner(png_read_data *d, const uint8_t *buf, size_t bufsz, int max_image_dimension) {
struct fake_file f = {.buf = buf, .sz = bufsz};
png_structp png = NULL;
png_infop info = NULL;
@@ -64,6 +64,10 @@ inflate_png_inner(png_read_data *d, const uint8_t *buf, size_t bufsz) {
png_byte color_type, bit_depth;
d->width = png_get_image_width(png, info);
d->height = png_get_image_height(png, info);
+ // libpng uses too much memory for overly large images
+ if (d->width > max_image_dimension || d->height > max_image_dimension) {
+ ABRT(ENOMEM, "PNG image is too large");
+ }
color_type = png_get_color_type(png, info);
bit_depth = png_get_bit_depth(png, info);
double image_gamma;
@@ -159,8 +163,8 @@ png_write_to_memory(png_structp png_ptr, png_bytep data, png_size_t length) {
}
static void png_flush_memory(png_structp png_ptr) { (void)png_ptr; }
-const char*
-png_from_32bit_rgba(uint32_t *data, size_t width, size_t height, size_t *out_size, bool flip_vertically) {
+static const char*
+create_png_from_data(char *data, size_t width, size_t height, size_t stride, size_t *out_size, bool flip_vertically, int color_type) {
*out_size = 0;
png_memory_write_state state = {.capacity=width*height * sizeof(uint32_t)};
state.buffer = malloc(state.capacity);
@@ -177,7 +181,7 @@ png_from_32bit_rgba(uint32_t *data, size_t width, size_t height, size_t *out_siz
return("Error during PNG creation\n");
}
png_set_write_fn(png_ptr, &state, png_write_to_memory, png_flush_memory);
- png_set_IHDR(png_ptr, info_ptr, width, height, 8, PNG_COLOR_TYPE_RGBA,
+ png_set_IHDR(png_ptr, info_ptr, width, height, 8, color_type,
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
// Allocate memory for row pointers
png_bytep *row_pointers = (png_bytep*)malloc(sizeof(png_bytep) * height);
@@ -186,8 +190,8 @@ png_from_32bit_rgba(uint32_t *data, size_t width, size_t height, size_t *out_siz
free(state.buffer);
return ("Failed to allocate memory for row pointers");
}
- if (flip_vertically) for (size_t y = 0; y < height; y++) row_pointers[height - 1 - y] = (png_byte*)&data[y * width];
- else for (size_t y = 0; y < height; y++) row_pointers[y] = (png_byte*)&data[y * width];
+ if (flip_vertically) for (size_t y = 0; y < height; y++) row_pointers[height - 1 - y] = (png_byte*)&data[y * stride];
+ else for (size_t y = 0; y < height; y++) row_pointers[y] = (png_byte*)&data[y * stride];
png_write_info(png_ptr, info_ptr);
png_write_image(png_ptr, row_pointers);
png_write_end(png_ptr, NULL);
@@ -197,6 +201,17 @@ png_from_32bit_rgba(uint32_t *data, size_t width, size_t height, size_t *out_siz
return (char*)state.buffer;
}
+const char*
+png_from_32bit_rgba(char *data, size_t width, size_t height, size_t *out_size, bool flip_vertically) {
+ return create_png_from_data(data, width, height, 4 * width, out_size, flip_vertically, PNG_COLOR_TYPE_RGBA);
+}
+
+const char*
+png_from_24bit_rgb(char *data, size_t width, size_t height, size_t *out_size, bool flip_vertically) {
+ return create_png_from_data(data, width, height, 3 * width, out_size, flip_vertically, PNG_COLOR_TYPE_RGB);
+}
+
+
static void
png_error_handler(png_read_data *d UNUSED, const char *code, const char *msg) {
if (!PyErr_Occurred()) PyErr_Format(PyExc_ValueError, "[%s] %s", code, msg);
@@ -208,7 +223,7 @@ load_png_data(PyObject *self UNUSED, PyObject *args) {
const char *data;
if (!PyArg_ParseTuple(args, "s#", &data, &sz)) return NULL;
png_read_data d = {.err_handler=png_error_handler};
- inflate_png_inner(&d, (const uint8_t*)data, sz);
+ inflate_png_inner(&d, (const uint8_t*)data, sz, 10000);
PyObject *ans = NULL;
if (d.ok && !PyErr_Occurred()) {
ans = Py_BuildValue("y#ii", d.decompressed, (int)d.sz, d.width, d.height);
diff --git a/kitty/png-reader.h b/kitty/png-reader.h
index 4d6175333e7..41fdbe05c9e 100644
--- a/kitty/png-reader.h
+++ b/kitty/png-reader.h
@@ -27,5 +27,6 @@ typedef struct png_read_data {
} error;
} png_read_data;
-void inflate_png_inner(png_read_data *d, const uint8_t *buf, size_t bufsz);
-const char* png_from_32bit_rgba(uint32_t *data, size_t width, size_t height, size_t *out_size, bool flip_vertically);
+void inflate_png_inner(png_read_data *d, const uint8_t *buf, size_t bufsz, int max_image_dimension);
+const char* png_from_32bit_rgba(char *data, size_t width, size_t height, size_t *out_size, bool flip_vertically);
+const char* png_from_24bit_rgb(char *data, size_t width, size_t height, size_t *out_size, bool flip_vertically);
diff --git a/kitty/screen.c b/kitty/screen.c
index c3db48f5a01..0c042e29bd0 100644
--- a/kitty/screen.c
+++ b/kitty/screen.c
@@ -3009,6 +3009,11 @@ set_title(Screen *self, PyObject *title) {
CALLBACK("title_changed", "O", title);
}
+void
+osc_context(Screen *self, PyObject *ctx) {
+ CALLBACK("osc_context", "O", ctx);
+}
+
void
desktop_notify(Screen *self, unsigned int osc_code, PyObject *data) {
CALLBACK("desktop_notify", "IO", osc_code, data);
diff --git a/kitty/screen.h b/kitty/screen.h
index 07dd46ec50f..a81c537fe36 100644
--- a/kitty/screen.h
+++ b/kitty/screen.h
@@ -248,6 +248,7 @@ void screen_pop_colors(Screen *, unsigned int);
void screen_report_color_stack(Screen *);
void screen_handle_kitty_dcs(Screen *, const char *callback_name, PyObject *cmd);
void set_title(Screen *self, PyObject*);
+void osc_context(Screen *self, PyObject *ctx);
void desktop_notify(Screen *self, unsigned int, PyObject*);
void set_icon(Screen *self, PyObject*);
void set_dynamic_color(Screen *self, unsigned int code, PyObject*);
diff --git a/kitty/session.py b/kitty/session.py
index 4922f352cd6..031502411aa 100644
--- a/kitty/session.py
+++ b/kitty/session.py
@@ -196,15 +196,30 @@ def set_cwd(self, val: str, session_base_dir: str) -> None:
self.tabs[-1].cwd = session_base_dir
+SESSION_FILE_EXTENSIONS = {'session', 'kitty-session', 'kitty_session'}
+
+
+def has_session_extension(path: str) -> bool:
+ name = os.path.basename(path)
+ return name.rpartition('.')[2] in SESSION_FILE_EXTENSIONS
+
+
def session_arg_to_name(session_arg: str) -> str:
if session_arg in ('-', '/dev/stdin', 'none'):
session_arg = ''
session_name = os.path.basename(session_arg)
- if session_name.rpartition('.')[2] in ('session', 'kitty-session', 'kitty_session'):
+ if has_session_extension(session_name):
session_name = session_name.rpartition('.')[0]
return session_name
+def resolve_session_arg_path(path: str) -> str:
+ path = os.path.expanduser(path)
+ if not os.path.isabs(path):
+ path = os.path.join(config_dir, path)
+ return os.path.abspath(path)
+
+
def parse_session(
raw: str, opts: Options, environ: Mapping[str, str] | None = None, session_arg: str = '', session_path: str = ''
) -> Generator[Session, None, None]:
@@ -424,10 +439,7 @@ def switch_to_session(boss: BossType, session_name: str) -> bool:
def resolve_session_path_and_name(path: str) -> tuple[str, str]:
- path = os.path.expanduser(path)
- if not os.path.isabs(path):
- path = os.path.join(config_dir, path)
- path = os.path.abspath(path)
+ path = resolve_session_arg_path(path)
return path, session_arg_to_name(path)
@@ -503,8 +515,11 @@ def do_close(confirmed: bool) -> None:
do_close(True)
-def choose_session(boss: BossType, opts: GotoSessionOptions) -> None:
- all_known_sessions = get_all_known_sessions()
+def choose_session_from_map(
+ boss: BossType, opts: GotoSessionOptions, session_map: Mapping[str, str], title: str
+) -> bool:
+ if not session_map:
+ return False
hmap = {n: len(goto_session_history)-i for i, n in enumerate(goto_session_history)}
if opts.sort_by == 'alphabetical':
def skey(name: str) -> tuple[int, str]:
@@ -512,13 +527,39 @@ def skey(name: str) -> tuple[int, str]:
else:
def skey(name: str) -> tuple[int, str]:
return hmap.get(name, len(goto_session_history)), name.lower()
- names = sorted(all_known_sessions, key=skey)
+ names = sorted(session_map, key=skey)
def chosen(name: str | None) -> None:
if name:
- goto_session(boss, (all_known_sessions[name],))
- boss.choose_entry(
- _('Select a session to activate'), ((name, name) for name in names), chosen)
+ goto_session(boss, (session_map[name],))
+ boss.choose_entry(title, ((name, name) for name in names), chosen)
+ return True
+
+
+def choose_session(boss: BossType, opts: GotoSessionOptions) -> None:
+ all_known_sessions = get_all_known_sessions()
+ choose_session_from_map(boss, opts, all_known_sessions, _('Select a session to activate'))
+
+
+def choose_session_in_directory(boss: BossType, opts: GotoSessionOptions, directory_path: str) -> None:
+ try:
+ with os.scandir(directory_path) as entries:
+ session_map = {
+ session_arg_to_name(entry.path): entry.path
+ for entry in entries
+ if entry.is_file() and has_session_extension(entry.name)
+ }
+ except OSError as e:
+ boss.show_error(
+ _('Failed to list sessions'),
+ _('Could not list session files in {0} with error: {1}').format(directory_path, e))
+ return
+ session_map = {name: path for name, path in session_map.items() if name}
+ if not choose_session_from_map(
+ boss, opts, session_map, _('Select a session to activate from {0}').format(directory_path)
+ ):
+ boss.show_error(
+ _('No session files found'), _('No session files were found inside {0}').format(directory_path))
def parse_goto_session_cmdline(args: list[str]) -> tuple[GotoSessionOptions, list[str]]:
@@ -571,7 +612,11 @@ def goto_session(boss: BossType, cmdline: Sequence[str]) -> None:
idx = 0
if idx < 0:
return goto_previous_session(boss, idx)
- path, session_name = resolve_session_path_and_name(path)
+ resolved_path = resolve_session_arg_path(path)
+ if os.path.isdir(resolved_path):
+ choose_session_in_directory(boss, opts, resolved_path)
+ return
+ path, session_name = resolve_session_path_and_name(resolved_path)
if not session_name:
boss.show_error(_('Invalid session'), _('{} is not a valid path for a session').format(path))
return
diff --git a/kitty/simple_cli_definitions.py b/kitty/simple_cli_definitions.py
index 1c73964ab93..af40b461449 100644
--- a/kitty/simple_cli_definitions.py
+++ b/kitty/simple_cli_definitions.py
@@ -387,7 +387,7 @@ def generate_c_parsers() -> Iterator[str]:
def kitty_options_spec() -> str:
if not hasattr(kitty_options_spec, 'ans'):
- OPTIONS = '''
+ OPTIONS = """
--class --app-id
dest=cls
default={appname}
@@ -484,7 +484,10 @@ def kitty_options_spec() -> str:
type=choices
default=normal
choices=normal,fullscreen,maximized,minimized,hidden
-Control how the initial kitty window is created.
+Control how the initial kitty OS window is created. Note that
+this is applies to all OS Windows if you use the :option:`{appname} --session`
+option to create multiple OS Windows. Any OS Windows state in
+specified in the session file gets overriden.
--position
@@ -552,7 +555,7 @@ def kitty_options_spec() -> str:
--execute -e
type=bool-set
!
-'''
+"""
setattr(kitty_options_spec, 'ans', OPTIONS.format(
appname=appname, conf_name=appname, listen_on_defn=listen_on_defn,
grab_keyboard_docs=grab_keyboard_docs, wait_for_single_instance_defn=wait_for_single_instance_defn,
diff --git a/kitty/state.c b/kitty/state.c
index a8d330be21a..cdace646f1b 100644
--- a/kitty/state.c
+++ b/kitty/state.c
@@ -1202,6 +1202,7 @@ PYWRAP1(patch_global_colors) {
}
P(active_border_color); P(inactive_border_color); P(bell_border_color); P(tab_bar_background);
P(tab_bar_margin_color); P(macos_titlebar_color); P(wayland_titlebar_color);
+ P(scrollbar_handle_color); P(scrollbar_track_color);
if (configured) {
P(background); P(url_color);
}
diff --git a/kitty/tab_bar.py b/kitty/tab_bar.py
index 07ced098b85..ea9531f9d31 100644
--- a/kitty/tab_bar.py
+++ b/kitty/tab_bar.py
@@ -523,6 +523,11 @@ def draw_tab(
return draw_tab
+def clear_caches() -> None:
+ load_custom_draw_tab.clear_cached()
+ load_custom_draw_tab_module.clear_cached()
+
+
class CustomDrawTitleFunc:
def __init__(self, data: dict[str, Any], implementation: Callable[[dict[str, Any]], str] | None = None):
diff --git a/kitty/tabs.py b/kitty/tabs.py
index 70b65671e37..4c22ee15aae 100644
--- a/kitty/tabs.py
+++ b/kitty/tabs.py
@@ -943,12 +943,15 @@ def move_window_backward(self) -> None:
def list_windows(self, self_window: Window | None = None, window_filter: Callable[[Window], bool] | None = None) -> Generator[WindowDict, None, None]:
active_window = self.active_window
+ cl = self.current_layout
for w in self:
if window_filter is None or window_filter(w):
yield w.as_dict(
is_active=w is active_window,
is_focused=w.os_window_id == current_focused_os_window_id() and w is active_window,
- is_self=w is self_window)
+ is_self=w is self_window,
+ neighbors_map=cl.neighbors_for_window(w, self.windows)
+ )
def list_groups(self) -> list[dict[str, Any]]:
return [g.as_simple_dict() for g in self.windows.groups]
diff --git a/kitty/types.py b/kitty/types.py
index a63db5e9a71..02906522a18 100644
--- a/kitty/types.py
+++ b/kitty/types.py
@@ -234,3 +234,10 @@ def w(f: _T) -> _T:
WindowMapper = Callable[[int], int | None]
DecoratedFunc = TypeVar('DecoratedFunc', bound=Callable[..., Any])
+
+
+class NeighborsMap(TypedDict, total=False):
+ left: list[int]
+ top: list[int]
+ right: list[int]
+ bottom: list[int]
diff --git a/kitty/vt-parser.c b/kitty/vt-parser.c
index 8c56eef40bf..a2d308b1c07 100644
--- a/kitty/vt-parser.c
+++ b/kitty/vt-parser.c
@@ -602,7 +602,10 @@ dispatch_osc(PS *self, uint8_t *buf, size_t limit, bool is_extended_osc) {
case 666: REPORT_ERROR("Ignoring OSC 666, typically used by VTE terminals for shell integration"); break;
case 697: REPORT_ERROR("Ignoring OSC 697, typically used by Fig for shell integration"); break;
case 701: REPORT_ERROR("Ignoring OSC 701, used by mintty for locale"); break;
- case 3008: REPORT_ERROR("Ignoring OSC 3008, used by systemd for OSC-context"); break;
+ case 3008:
+ START_DISPATCH
+ DISPATCH_OSC(osc_context);
+ END_DISPATCH
case 7704: REPORT_ERROR("Ignoring OSC 7704, used by mintty for ANSI colors"); break;
case 7750: REPORT_ERROR("Ignoring OSC 7750, used by mintty for Emoji style"); break;
case 7770: REPORT_ERROR("Ignoring OSC 7770, used by mintty for font size"); break;
diff --git a/kitty/window.py b/kitty/window.py
index 297ddd59c2d..f6d4243b8e4 100644
--- a/kitty/window.py
+++ b/kitty/window.py
@@ -96,7 +96,7 @@
from .progress import Progress
from .rgb import to_color
from .terminfo import get_capabilities
-from .types import MouseEvent, OverlayType, WindowGeometry, ac, run_once
+from .types import MouseEvent, NeighborsMap, OverlayType, WindowGeometry, ac, run_once
from .typing_compat import BossType, ChildType, EdgeLiteral, TabType, TypedDict
from .utils import (
color_as_int,
@@ -250,6 +250,7 @@ class WindowDict(TypedDict):
at_prompt: bool
created_at: int
in_alternate_screen: bool
+ neighbors: NeighborsMap
class PipeData(TypedDict):
@@ -1286,6 +1287,9 @@ def title_changed(self, new_title: memoryview | None, is_base64: bool = False) -
if self.override_title is None:
self.title_updated()
+ def osc_context(self, ctx_data: memoryview) -> None:
+ pass # this is systemd's useless OSC 3008 context protocol https://systemd.io/OSC_CONTEXT/
+
def icon_changed(self, new_icon: memoryview) -> None:
pass # TODO: Implement this
@@ -1935,7 +1939,12 @@ def current_mouse_position(self) -> Optional['MousePosition']:
return get_mouse_data_for_window(self.os_window_id, self.tab_id, self.id)
# Serialization {{{
- def as_dict(self, is_focused: bool = False, is_self: bool = False, is_active: bool = False) -> WindowDict:
+ def as_dict(
+ self, is_focused: bool = False, is_self: bool = False, is_active: bool = False,
+ neighbors_map: NeighborsMap | None = None,
+ ) -> WindowDict:
+ if neighbors_map is None:
+ neighbors_map = {}
return {
'id': self.id,
'is_focused': is_focused,
@@ -1955,6 +1964,7 @@ def as_dict(self, is_focused: bool = False, is_self: bool = False, is_active: bo
'user_vars': self.user_vars,
'created_at': self.created_at,
'in_alternate_screen': self.screen.is_using_alternate_linebuf(),
+ 'neighbors': neighbors_map,
}
def serialize_state(self) -> dict[str, Any]:
diff --git a/kitty_tests/__init__.py b/kitty_tests/__init__.py
index bd9ce655f0a..cc1db836b4f 100644
--- a/kitty_tests/__init__.py
+++ b/kitty_tests/__init__.py
@@ -81,6 +81,9 @@ def p(x):
def title_changed(self, data, is_base64=False) -> None:
self.titlebuf.append(process_title_from_child(data, is_base64, ''))
+ def osc_context(self, data):
+ pass
+
def icon_changed(self, data) -> None:
self.iconbuf += str(data, 'utf-8')
@@ -402,7 +405,7 @@ def write_to_child(self, data, flush=False):
self.process_input_from_child(0)
def send_da1_response(self, data: str) -> None:
- self.write_to_child('\x1b[' + data, flush=True)
+ self.write_to_child('\x1b[' + data, flush=False) # ]]]]]]
def send_cmd_to_child(self, cmd, flush=False):
self.callbacks.last_cmd_exit_status = sys.maxsize
diff --git a/kitty_tests/graphics.py b/kitty_tests/graphics.py
index 659d60197d9..b254232713d 100644
--- a/kitty_tests/graphics.py
+++ b/kitty_tests/graphics.py
@@ -253,7 +253,7 @@ def reset(small_hole_threshold=0, defrag_factor=2):
self.assertEqual(dc.total_size, sum(map(len, data.values())))
self.assertTrue(dc.wait_for_write())
check_data()
- sz = dc.size_on_disk()
+ sz = dc.end_of_data_offset()
self.assertEqual(sz, sum(map(len, data.values())))
self.assertFalse(dc.holes())
holes = set()
@@ -262,9 +262,9 @@ def reset(small_hole_threshold=0, defrag_factor=2):
holes.add(x)
check_data()
self.assertRaises(KeyError, dc.get, key_as_bytes(x))
- self.assertEqual(sz, dc.size_on_disk())
+ self.assertEqual(sz, dc.end_of_data_offset())
self.assertEqual(holes, {x[1] for x in dc.holes()})
- self.assertEqual(sz, dc.size_on_disk())
+ self.assertEqual(sz, dc.end_of_data_offset())
# fill holes largest first to ensure small one doesn't go into large accidentally causing fragmentation
for i, x in enumerate(sorted(holes, reverse=True)):
x = 'ABCDEFGH'[i] * x
@@ -273,13 +273,10 @@ def reset(small_hole_threshold=0, defrag_factor=2):
check_data()
holes.discard(len(x))
self.assertEqual(holes, {x[1] for x in dc.holes()})
- self.assertEqual(sz, dc.size_on_disk(), f'Disk cache has unexpectedly grown from {sz} to {dc.size_on_disk} with data: {x!r}')
+ self.assertEqual(sz, dc.end_of_data_offset(), f'Disk cache has unexpectedly grown from {sz} to {dc.end_of_data_offset()} with data: {x!r}')
check_data()
dc.clear()
- st = time.monotonic()
- while dc.size_on_disk() and time.monotonic() - st < 2:
- time.sleep(0.001)
- self.assertEqual(dc.size_on_disk(), 0)
+ self.assertEqual(dc.end_of_data_offset(), 0)
data.clear()
for i in range(25):
@@ -287,25 +284,25 @@ def reset(small_hole_threshold=0, defrag_factor=2):
dc.wait_for_write()
check_data()
- before = dc.size_on_disk()
+ before = dc.end_of_data_offset()
while dc.total_size > before // 3:
key = random.choice(tuple(data))
self.assertTrue(remove(key))
check_data()
add('trigger defrag', 'XXX')
dc.wait_for_write()
- self.assertLess(dc.size_on_disk(), before)
+ self.assertLess(dc.end_of_data_offset(), before)
check_data()
dc.clear()
st = time.monotonic()
- while dc.size_on_disk() and time.monotonic() - st < 20:
+ while dc.end_of_data_offset() and time.monotonic() - st < 20:
time.sleep(0.01)
- self.assertEqual(dc.size_on_disk(), 0)
+ self.assertEqual(dc.end_of_data_offset(), 0)
for frame in range(32):
add(f'1:{frame}', f'{frame:02d}' * 8)
dc.wait_for_write()
- self.assertEqual(dc.size_on_disk(), 32 * 16)
+ self.assertEqual(dc.end_of_data_offset(), 32 * 16)
self.assertEqual(dc.num_cached_in_ram(), 0)
num_in_ram = 0
for frame in range(32):
@@ -326,18 +323,18 @@ def clear_predicate(key):
self.assertIsNone(add(1, '1' * 1024))
self.assertIsNone(add(2, '2' * 1024))
dc.wait_for_write()
- sz = dc.size_on_disk()
+ sz = dc.end_of_data_offset()
remove(1)
- self.ae(sz, dc.size_on_disk())
+ self.ae(sz, dc.end_of_data_offset())
self.ae({x[1] for x in dc.holes()}, {1024})
self.assertIsNone(add(3, '3' * 800))
dc.wait_for_write()
self.assertFalse(dc.holes())
- self.ae(sz, dc.size_on_disk())
+ self.ae(sz, dc.end_of_data_offset())
self.assertIsNone(add(4, '4' * 100))
sz += 100
dc.wait_for_write()
- self.ae(sz, dc.size_on_disk())
+ self.ae(sz, dc.end_of_data_offset())
check_data()
self.assertFalse(dc.holes())
remove(4)
@@ -345,7 +342,7 @@ def clear_predicate(key):
self.assertIsNone(add(5, '5' * 10))
sz += 10
dc.wait_for_write()
- self.ae(sz, dc.size_on_disk())
+ self.ae(sz, dc.end_of_data_offset())
# test hole coalescing
reset(defrag_factor=20)
diff --git a/kitty_tests/layout.py b/kitty_tests/layout.py
index 10c203af95d..3f045c5c336 100644
--- a/kitty_tests/layout.py
+++ b/kitty_tests/layout.py
@@ -267,7 +267,7 @@ def test_splits(self):
windows[1].set_geometry(WindowGeometry(11, 0, 20, 10, 0, 0))
windows[2].set_geometry(WindowGeometry(11, 11, 15, 20, 0, 0))
windows[3].set_geometry(WindowGeometry(16, 11, 20, 20, 0, 0))
- self.ae(q.neighbors_for_window(windows[0], all_windows), {'left': [], 'right': [2, 3], 'top': [], 'bottom': []})
- self.ae(q.neighbors_for_window(windows[1], all_windows), {'left': [1], 'right': [], 'top': [], 'bottom': [3, 4]})
- self.ae(q.neighbors_for_window(windows[2], all_windows), {'left': [1], 'right': [4], 'top': [2], 'bottom': []})
- self.ae(q.neighbors_for_window(windows[3], all_windows), {'left': [3], 'right': [], 'top': [2], 'bottom': []})
+ self.ae(q.neighbors_for_window(windows[0], all_windows), {'right': [2, 3]})
+ self.ae(q.neighbors_for_window(windows[1], all_windows), {'left': [1], 'bottom': [3, 4]})
+ self.ae(q.neighbors_for_window(windows[2], all_windows), {'left': [1], 'right': [4], 'top': [2]})
+ self.ae(q.neighbors_for_window(windows[3], all_windows), {'left': [3], 'top': [2]})
diff --git a/tools/cli/completion.go b/tools/cli/completion.go
index f502c57be9f..f78b1aea662 100644
--- a/tools/cli/completion.go
+++ b/tools/cli/completion.go
@@ -105,7 +105,7 @@ type Delegate struct {
type Completions struct {
Groups []*MatchGroup `json:"groups,omitempty"`
- Delegate Delegate `json:"delegate,omitempty"`
+ Delegate Delegate `json:"delegate"`
CurrentCmd *Command `json:"-"`
AllWords []string `json:"-"` // all words passed to parse_args()
diff --git a/tools/cli/files.go b/tools/cli/files.go
index cdb135ef79b..72e256f2b7c 100644
--- a/tools/cli/files.go
+++ b/tools/cli/files.go
@@ -7,6 +7,7 @@ import (
"mime"
"os"
"path/filepath"
+ "slices"
"strings"
"golang.org/x/sys/unix"
@@ -185,13 +186,7 @@ func complete_by_fnmatch(prefix, cwd string, patterns []string) []string {
}
func complete_by_mimepat(prefix, cwd string, patterns []string) []string {
- all_allowed := false
- for _, p := range patterns {
- if p == "*" {
- all_allowed = true
- break
- }
- }
+ all_allowed := slices.Contains(patterns, "*")
return fname_based_completer(prefix, cwd, func(name string) bool {
if all_allowed {
return true
diff --git a/tools/cli/markup/prettify.go b/tools/cli/markup/prettify.go
index c6129f5c554..a2d5413968e 100644
--- a/tools/cli/markup/prettify.go
+++ b/tools/cli/markup/prettify.go
@@ -19,7 +19,7 @@ var _ = fmt.Print
type Context struct {
fmt_ctx style.Context
- Cyan, Green, Blue, Magenta, Red, BrightRed, Yellow, Italic, Bold, Dim, Title, Exe, Opt, Emph, Err, Code func(args ...interface{}) string
+ Cyan, Green, Blue, Magenta, Red, BrightRed, Yellow, Italic, Bold, Dim, Title, Exe, Opt, Emph, Err, Code func(args ...any) string
Url func(string, string) string
}
diff --git a/tools/cmd/at/main.go b/tools/cmd/at/main.go
index f58210c4921..7ee108b39ec 100644
--- a/tools/cmd/at/main.go
+++ b/tools/cmd/at/main.go
@@ -184,7 +184,7 @@ func (self *ResponseData) UnmarshalJSON(data []byte) error {
type Response struct {
Ok bool `json:"ok"`
- Data ResponseData `json:"data,omitempty"`
+ Data ResponseData `json:"data"`
Error string `json:"error,omitempty"`
Traceback string `json:"tb,omitempty"`
}
diff --git a/tools/cmd/at/send_text.go b/tools/cmd/at/send_text.go
index 12eed10e6c3..d86d775887e 100644
--- a/tools/cmd/at/send_text.go
+++ b/tools/cmd/at/send_text.go
@@ -39,10 +39,7 @@ func parse_send_text(io_data *rc_io_data, args []string) error {
}
text := strings.Join(args, " ")
text_gen := func(io_data *rc_io_data) (bool, error) {
- limit := len(text)
- if limit > 2048 {
- limit = 2048
- }
+ limit := min(len(text), 2048)
set_payload_data(io_data, "base64:"+base64.StdEncoding.EncodeToString(utils.UnsafeStringToBytes(text[:limit])))
text = text[limit:]
return len(text) == 0, nil
diff --git a/tools/cmd/atexit/main.go b/tools/cmd/atexit/main.go
index 5fd527655fc..eb108ff77cd 100644
--- a/tools/cmd/atexit/main.go
+++ b/tools/cmd/atexit/main.go
@@ -7,9 +7,9 @@ import (
"os/signal"
"strings"
+ "github.com/kovidgoyal/go-shm"
"github.com/kovidgoyal/kitty/tools/cli"
"github.com/kovidgoyal/kitty/tools/utils"
- "github.com/kovidgoyal/kitty/tools/utils/shm"
)
var _ = fmt.Print
diff --git a/tools/cmd/pytest/main.go b/tools/cmd/pytest/main.go
index 08b4c3e850d..fea1742c15a 100644
--- a/tools/cmd/pytest/main.go
+++ b/tools/cmd/pytest/main.go
@@ -4,19 +4,62 @@ package pytest
import (
"fmt"
+ "io"
+ "os"
+ "github.com/kovidgoyal/go-shm"
"github.com/kovidgoyal/kitty/kittens/ssh"
"github.com/kovidgoyal/kitty/tools/cli"
- "github.com/kovidgoyal/kitty/tools/utils/shm"
)
var _ = fmt.Print
+func test_integration_with_python(args []string) (rc int, err error) {
+ switch args[0] {
+ default:
+ return 1, fmt.Errorf("Unknown test type: %s", args[0])
+ case "read":
+ data, err := shm.ReadWithSizeAndUnlink(args[1])
+ if err != nil {
+ return 1, err
+ }
+ _, err = os.Stdout.Write(data)
+ if err != nil {
+ return 1, err
+ }
+ case "write":
+ data, err := io.ReadAll(os.Stdin)
+ if err != nil {
+ return 1, err
+ }
+ mmap, err := shm.CreateTemp("shmtest-", uint64(len(data)+shm.NUM_BYTES_FOR_SIZE))
+ if err != nil {
+ return 1, err
+ }
+ if err = shm.WriteWithSize(mmap, data, 0); err != nil {
+ return 1, err
+ }
+ mmap.Close()
+ fmt.Println(mmap.Name())
+ }
+ return 0, nil
+}
+
+func shm_entry_point(root *cli.Command) {
+ root.AddSubCommand(&cli.Command{
+ Name: "shm",
+ OnlyArgsAllowed: true,
+ Run: func(cmd *cli.Command, args []string) (rc int, err error) {
+ return test_integration_with_python(args)
+ },
+ })
+
+}
func EntryPoint(root *cli.Command) {
root = root.AddSubCommand(&cli.Command{
Name: "__pytest__",
Hidden: true,
})
- shm.TestEntryPoint(root)
+ shm_entry_point(root)
ssh.TestEntryPoint(root)
}
diff --git a/tools/config/utils.go b/tools/config/utils.go
index f83c4cbc474..48df04ef310 100644
--- a/tools/config/utils.go
+++ b/tools/config/utils.go
@@ -18,7 +18,7 @@ var _ = fmt.Print
func ParseStrDict(val, record_sep, field_sep string) (map[string]string, error) {
ans := make(map[string]string)
- for _, record := range strings.Split(val, record_sep) {
+ for record := range strings.SplitSeq(val, record_sep) {
key, val, found := strings.Cut(record, field_sep)
if found {
ans[key] = val
diff --git a/tools/ignorefiles/gitignore.go b/tools/ignorefiles/gitignore.go
index 5377429b983..6b88b0ecdb4 100644
--- a/tools/ignorefiles/gitignore.go
+++ b/tools/ignorefiles/gitignore.go
@@ -261,11 +261,15 @@ func CompileGitIgnoreLine(line string) (ans GitPattern, skipped_line bool) {
}
func get_global_gitconfig_excludesfile() (ans string) {
+ return _get_global_gitconfig_excludesfile(utils.Expanduser("~/.gitconfig"))
+}
+
+func _get_global_gitconfig_excludesfile(home_gitconfig string) (ans string) {
cfhome := os.Getenv("XDG_CONFIG_HOME")
if cfhome == "" {
cfhome = utils.Expanduser("~/.config")
}
- for _, candidate := range []string{"/etc/gitconfig", filepath.Join(cfhome, "git", "config"), utils.Expanduser("~/.gitconfig")} {
+ for _, candidate := range []string{"/etc/gitconfig", filepath.Join(cfhome, "git", "config"), home_gitconfig} {
if data, err := os.ReadFile(candidate); err == nil {
s := utils.NewLineScanner(utils.UnsafeBytesToString(data))
in_core := false
diff --git a/tools/ignorefiles/gitignore_test.go b/tools/ignorefiles/gitignore_test.go
index 0b4dc10650d..a8bf49e8d89 100644
--- a/tools/ignorefiles/gitignore_test.go
+++ b/tools/ignorefiles/gitignore_test.go
@@ -4,6 +4,7 @@ import (
"fmt"
"io/fs"
"os"
+ "path/filepath"
"strings"
"testing"
@@ -143,19 +144,21 @@ bar `: {
}
}
}
- os.WriteFile(utils.Expanduser("~/.gitconfig"), []byte(`
- [core]
- something
- [else]
- ...
- [core]
- ...
- [core]
- excludesfile = one
- [core]
- excludesfile = ~/global-gitignore
+ tdir := t.TempDir()
+ gc := filepath.Join(tdir, ".gitconfig")
+ os.WriteFile(gc, []byte(`
+[core]
+something
+[else]
+...
+[core]
+...
+[core]
+excludesfile = one
+[core]
+excludesfile = ~/global-gitignore
`), 0600)
- if ef := get_global_gitconfig_excludesfile(); ef != utils.Expanduser("~/global-gitignore") {
+ if ef := _get_global_gitconfig_excludesfile(gc); ef != utils.Expanduser("~/global-gitignore") {
t.Fatalf("global gitconfig excludes file incorrect: %s != %s", utils.Expanduser("~/global-gitignore"), ef)
}
}
diff --git a/tools/rsync/api_test.go b/tools/rsync/api_test.go
index 63bffb02832..6d2a3a261bb 100644
--- a/tools/rsync/api_test.go
+++ b/tools/rsync/api_test.go
@@ -135,7 +135,7 @@ func generate_data(block_size, num_of_blocks int, extra ...string) []byte {
e := strings.Join(extra, "")
ans := make([]byte, num_of_blocks*block_size+len(e))
utils.Memset(ans, '_')
- for i := 0; i < num_of_blocks; i++ {
+ for i := range num_of_blocks {
offset := i * block_size
copy(ans[offset:], strconv.Itoa(i))
}
diff --git a/tools/simdstring/benchmarks_test.go b/tools/simdstring/benchmarks_test.go
index d445d3bebc0..37587962bce 100644
--- a/tools/simdstring/benchmarks_test.go
+++ b/tools/simdstring/benchmarks_test.go
@@ -33,7 +33,7 @@ func BenchmarkIndexByte(b *testing.B) {
f = bytes.IndexByte
}
b.Run(fmt.Sprintf("%s_sz=%d", which, pos), func(b *testing.B) {
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
f(data, 'q')
}
})
@@ -54,7 +54,7 @@ func BenchmarkIndexByte2(b *testing.B) {
f = index_byte2_scalar
}
b.Run(fmt.Sprintf("%s_sz=%d", which, pos), func(b *testing.B) {
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
f(data, 'q', 'x')
}
})
diff --git a/tools/simdstring/intrinsics_test.go b/tools/simdstring/intrinsics_test.go
index b0eedef49a6..23f29aec38e 100644
--- a/tools/simdstring/intrinsics_test.go
+++ b/tools/simdstring/intrinsics_test.go
@@ -48,7 +48,7 @@ func test_cmpeq_epi8(a, b []byte) []byte {
func test_cmplt_epi8(t *testing.T, a, b []byte) []byte {
ans := make([]byte, len(a))
var prev []byte
- for which := 0; which < 3; which++ {
+ for which := range 3 {
if len(ans) == 16 {
test_cmplt_epi8_asm_128(a, b, which, ans)
} else {
@@ -161,7 +161,7 @@ func TestSIMDStringOps(t *testing.T) {
}
// test alignment issues
q := []byte("abc")
- for sz := 0; sz < 32; sz++ {
+ for sz := range 32 {
test(q, '<', '>', sz)
test(q, ' ', 'b', sz)
test(q, '<', 'a', sz)
@@ -172,7 +172,7 @@ func TestSIMDStringOps(t *testing.T) {
tests := func(h string, a, b byte) {
for _, sz := range []int{0, 16, 32, 64, 79} {
q := strings.Repeat(" ", sz) + h
- for sz := 0; sz < 32; sz++ {
+ for sz := range 32 {
test([]byte(q), a, b, sz)
}
}
@@ -326,7 +326,7 @@ func TestIntrinsics(t *testing.T) {
if e := test_jump_if_zero(a); e != 0 {
t.Fatalf("Did not detect zero register")
}
- for i := 0; i < sz; i++ {
+ for i := range sz {
a = make([]byte, sz)
a[i] = 1
if e := test_jump_if_zero(a); e != 1 {
@@ -340,7 +340,7 @@ func TestIntrinsics(t *testing.T) {
if e := test_count_to_match(a, 77); e != -1 {
t.Fatalf("Unexpectedly found byte at: %d", e)
}
- for i := 0; i < sz; i++ {
+ for i := range sz {
if e := test_count_to_match(a, byte(i)); e != i {
t.Fatalf("Failed to find the byte: %d (%d != %d)", i, i, e)
}
diff --git a/tools/simdstring/scalar.go b/tools/simdstring/scalar.go
index 5320aebe822..3044740ce05 100644
--- a/tools/simdstring/scalar.go
+++ b/tools/simdstring/scalar.go
@@ -41,7 +41,7 @@ func index_byte2_string_scalar(data string, a, b byte) int {
}
func index_c0_scalar(data []byte) int {
- for i := 0; i < len(data); i++ {
+ for i := range data {
if data[i] == 0x7f || data[i] < ' ' {
return i
}
diff --git a/tools/themes/collection.go b/tools/themes/collection.go
index 94022f2ed22..6191c92e6d6 100644
--- a/tools/themes/collection.go
+++ b/tools/themes/collection.go
@@ -688,7 +688,7 @@ func ColorSettingsAsEscapeCodes(settings map[string]string) string {
set_default_color("selection_foreground", style.DefaultColors.SelectionFg, loop.SELECTION_FG)
w.WriteString("\033]4")
- for i := 0; i < 256; i++ {
+ for i := range 256 {
key := "color" + strconv.Itoa(i)
val := settings[key]
if val != "" {
diff --git a/tools/tui/graphics/collection.go b/tools/tui/graphics/collection.go
index 0e77af35a49..216017d8c91 100644
--- a/tools/tui/graphics/collection.go
+++ b/tools/tui/graphics/collection.go
@@ -10,11 +10,11 @@ import (
"sync"
"sync/atomic"
+ "github.com/kovidgoyal/go-shm"
"github.com/kovidgoyal/kitty/tools/tui"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/images"
- "github.com/kovidgoyal/kitty/tools/utils/shm"
)
var _ = fmt.Print
@@ -336,7 +336,8 @@ func transmit_by_escape_code(lp *loop.Loop, image_id uint32, temp_file_map map[u
atomic := lp.IsAtomicUpdateActive()
lp.EndAtomicUpdate()
gc.SetTransmission(GRT_transmission_direct)
- _ = gc.WriteWithPayloadToLoop(lp, frame.Data())
+ _, _, _, data := frame.Data()
+ _ = gc.WriteWithPayloadToLoop(lp, data)
if atomic {
lp.StartAtomicUpdate()
}
@@ -362,7 +363,8 @@ func transmit_by_file(lp *loop.Loop, image_id uint32, temp_file_map map[uint32]*
}
defer f.Close()
temp_file_map[image_id] = &temp_resource{path: f.Name()}
- _, err = f.Write(frame.Data())
+ _, _, _, data := frame.Data()
+ _, err = f.Write(data)
if err != nil {
transmit_by_escape_code(lp, image_id, temp_file_map, frame, gc)
return
diff --git a/tools/tui/run.go b/tools/tui/run.go
index ff4abb4a8bc..223972772a9 100644
--- a/tools/tui/run.go
+++ b/tools/tui/run.go
@@ -137,7 +137,7 @@ func get_shell_name(argv0 string) (ans string) {
func rc_modification_allowed(ksi string) (allowed bool, set_ksi_env_var bool) {
allowed = ksi != ""
set_ksi_env_var = true
- for _, x := range strings.Split(ksi, " ") {
+ for x := range strings.SplitSeq(ksi, " ") {
switch x {
case "disabled":
allowed = false
diff --git a/tools/tui/shell_integration/api.go b/tools/tui/shell_integration/api.go
index 1927a357cac..28352330254 100644
--- a/tools/tui/shell_integration/api.go
+++ b/tools/tui/shell_integration/api.go
@@ -110,7 +110,7 @@ func PathToTerminfoDb(term string) (ans string) {
return ans
}
if td := os.Getenv("TERMINFO_DIRS"); td != "" {
- for _, q := range strings.Split(td, string(os.PathListSeparator)) {
+ for q := range strings.SplitSeq(td, string(os.PathListSeparator)) {
if q == "" {
q = "/usr/share/terminfo"
}
diff --git a/tools/utils/base85/base85.go b/tools/utils/base85/base85.go
index afbe98a73c1..d31199ce084 100644
--- a/tools/utils/base85/base85.go
+++ b/tools/utils/base85/base85.go
@@ -45,7 +45,7 @@ var decoder_array = sync.OnceValue(func() *[256]byte {
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
}
- for i := 0; i < len(encode); i++ {
+ for i := range len(encode) {
decode[encode[i]] = byte(i)
}
return &decode
@@ -101,7 +101,7 @@ func decodeChunk(decode *[256]byte, dst, src []byte) (int, int) {
var val uint32
m := DecodedLen(len(src))
buf := [5]byte{84, 84, 84, 84, 84}
- for i := 0; i < len(src); i++ {
+ for i := range src {
e := decode[src[i]]
if e == 0xFF {
return 0, i + 1
@@ -109,7 +109,7 @@ func decodeChunk(decode *[256]byte, dst, src []byte) (int, int) {
buf[i] = e
}
- for i := 0; i < 5; i++ {
+ for i := range 5 {
r := buf[i]
val += uint32(r) * decode_base[i]
}
diff --git a/tools/utils/humanize/times.go b/tools/utils/humanize/times.go
index 04eb6085b3a..72b5bce704f 100644
--- a/tools/utils/humanize/times.go
+++ b/tools/utils/humanize/times.go
@@ -100,7 +100,7 @@ func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnit
n = len(magnitudes) - 1
}
mag := magnitudes[n]
- args := []interface{}{}
+ args := []any{}
escaped := false
for _, ch := range mag.Format {
if escaped {
diff --git a/tools/utils/images/convert.go b/tools/utils/images/convert.go
index cf24692d1d6..ecfb68e2105 100644
--- a/tools/utils/images/convert.go
+++ b/tools/utils/images/convert.go
@@ -11,6 +11,7 @@ import (
"strconv"
"strings"
+ "github.com/kovidgoyal/imaging"
"github.com/kovidgoyal/kitty/tools/cli"
"github.com/kovidgoyal/kitty/tools/utils"
)
@@ -51,14 +52,10 @@ func encode_rgba(output io.Writer, img image.Image) (err error) {
}
func convert_image(input io.ReadSeeker, output io.Writer, format string) (err error) {
- image_data, err := OpenNativeImageFromReader(input)
+ img, err := imaging.Decode(input)
if err != nil {
return err
}
- if len(image_data.Frames) == 0 {
- return fmt.Errorf("Image has no frames")
- }
- img := image_data.Frames[0].Img
q := strings.ToLower(format)
if q == "rgba" {
return encode_rgba(output, img)
@@ -92,7 +89,7 @@ func images_equal(img, rimg *ImageData) (err error) {
}
func develop_serialize(input_data io.ReadSeeker) (err error) {
- img, err := OpenNativeImageFromReader(input_data)
+ img, _, err := OpenImageFromReader(input_data)
if err != nil {
return err
}
@@ -113,7 +110,7 @@ func develop_resize(spec string, input_data io.ReadSeeker) (err error) {
if h, err = strconv.Atoi(hs); err != nil {
return
}
- img, err := OpenNativeImageFromReader(input_data)
+ img, _, err := OpenImageFromReader(input_data)
if err != nil {
return err
}
diff --git a/tools/utils/images/loading.go b/tools/utils/images/loading.go
index b8fb9f0d9a0..622edbe1eba 100644
--- a/tools/utils/images/loading.go
+++ b/tools/utils/images/loading.go
@@ -3,26 +3,16 @@
package images
import (
- "bytes"
- "encoding/json"
- "errors"
"fmt"
"image"
- "image/gif"
"image/png"
"io"
"os"
- "os/exec"
- "path/filepath"
- "slices"
- "strconv"
"strings"
- "sync"
+ "github.com/kovidgoyal/go-parallel"
+ "github.com/kovidgoyal/go-shm"
"github.com/kovidgoyal/imaging/nrgb"
- "github.com/kovidgoyal/imaging/prism/meta/gifmeta"
- "github.com/kovidgoyal/kitty/tools/utils"
- "github.com/kovidgoyal/kitty/tools/utils/shm"
"github.com/kovidgoyal/imaging"
)
@@ -62,11 +52,11 @@ type SerializableImageFrame struct {
Delay_ms int // negative for gapless frame, zero ignored, positive is number of ms
Replace bool // do a replace rather than an alpha blend
Is_opaque bool
- Size int
-}
-func (s SerializableImageFrame) NeededSize() int {
- return utils.IfElse(s.Is_opaque, 3, 4) * s.Width * s.Height
+ Size int // size in bytes of the serialized data
+ Number_of_channels int
+ Bits_per_channel int
+ Has_alpha_channel bool
}
func (s *ImageFrame) Serialize() SerializableImageFrame {
@@ -78,88 +68,45 @@ func (s *ImageFrame) Serialize() SerializableImageFrame {
}
func (self *ImageFrame) DataAsSHM(pattern string) (ans shm.MMap, err error) {
- bytes_per_pixel := 4
- if self.Is_opaque {
- bytes_per_pixel = 3
- }
- ans, err = shm.CreateTemp(pattern, uint64(self.Width*self.Height*bytes_per_pixel))
- if err != nil {
+ _, _, _, d := self.Data()
+ if ans, err = shm.CreateTemp(pattern, uint64(len(d))); err != nil {
return nil, err
}
- switch img := self.Img.(type) {
- case *imaging.NRGB:
- if bytes_per_pixel == 3 {
- copy(ans.Slice(), img.Pix)
- return
- }
- case *image.NRGBA:
- if bytes_per_pixel == 4 {
- copy(ans.Slice(), img.Pix)
- return
- }
- }
- dest_rect := image.Rect(0, 0, self.Width, self.Height)
- var final_img image.Image
- switch bytes_per_pixel {
- case 3:
- rgb := &imaging.NRGB{Pix: ans.Slice(), Stride: bytes_per_pixel * self.Width, Rect: dest_rect}
- final_img = rgb
- case 4:
- rgba := &image.NRGBA{Pix: ans.Slice(), Stride: bytes_per_pixel * self.Width, Rect: dest_rect}
- final_img = rgba
- }
- ctx := Context{}
- ctx.PasteCenter(final_img, self.Img, nil)
+ copy(ans.Slice(), d)
return
-
}
-func (self *ImageFrame) Data() (ans []byte) {
- bytes_per_pixel := 4
+func (self *ImageFrame) Data() (num_channels, bits_per_channel int, has_alpha_channel bool, ans []byte) {
if self.Is_opaque {
- bytes_per_pixel = 3
+ return 3, 8, false, imaging.AsRGBData8(self.Img)
}
- switch img := self.Img.(type) {
- case *imaging.NRGB:
- if bytes_per_pixel == 3 {
- return img.Pix
- }
- case *image.NRGBA:
- if bytes_per_pixel == 4 {
- return img.Pix
- }
- }
- dest_rect := image.Rect(0, 0, self.Width, self.Height)
- var final_img image.Image
- switch bytes_per_pixel {
- case 3:
- rgb := nrgb.NewNRGB(dest_rect)
- final_img = rgb
- ans = rgb.Pix
- case 4:
- rgba := image.NewNRGBA(dest_rect)
- final_img = rgba
- ans = rgba.Pix
- }
- ctx := Context{}
- ctx.PasteCenter(final_img, self.Img, nil)
- return
+ return 4, 8, true, imaging.AsRGBAData8(self.Img)
}
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, Replace: s.Replace,
+ Is_opaque: s.Is_opaque || !s.Has_alpha_channel, Replace: s.Replace,
+ }
+ bpc := s.Bits_per_channel
+ if bpc == 0 {
+ bpc = 8
+ }
+ if bpc != 8 {
+ return nil, fmt.Errorf("serialized image data has unsupported number of bits per channel: %d", bpc)
}
- bytes_per_pixel := utils.IfElse(s.Is_opaque, 3, 4)
+ bytes_per_pixel := bpc * s.Number_of_channels / 8
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 {
+ switch s.Number_of_channels {
+ case 3, 0:
ans.Img, err = nrgb.NewNRGBWithContiguousRGBPixels(data, s.Left, s.Top, s.Width, s.Height)
- } else {
+ case 4:
ans.Img, err = NewNRGBAWithContiguousRGBAPixels(data, s.Left, s.Top, s.Width, s.Height)
+ default:
+ return nil, fmt.Errorf("serialized image data has unsupported number of channels: %d", s.Number_of_channels)
}
return &ans, err
}
@@ -196,8 +143,12 @@ 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])
+ df := &m.Frames[i]
+ df.Number_of_channels, df.Bits_per_channel, df.Has_alpha_channel, data[i] = f.Data()
+ df.Size = len(data[i])
+ if !df.Has_alpha_channel {
+ df.Is_opaque = true
+ }
}
return m, data
}
@@ -222,22 +173,25 @@ func ImageFromSerialized(m SerializableImageMetadata, data [][]byte) (*ImageData
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()
- ans := *self
- ans.Width = int(x_frac * float64(width))
- ans.Height = int(y_frac * float64(height))
- ans.Img = imaging.Resize(self.Img, ans.Width, ans.Height, imaging.Lanczos)
- ans.Left = int(x_frac * float64(left))
- ans.Top = int(y_frac * float64(top))
+func (ans ImageFrame) Resize(x_frac, y_frac float64) *ImageFrame {
+ ans.Width = int(x_frac * float64(ans.Width))
+ ans.Height = int(y_frac * float64(ans.Height))
+ ans.Img = imaging.ResizeWithOpacity(ans.Img, ans.Width, ans.Height, imaging.Lanczos, ans.Is_opaque)
+ ans.Left = int(x_frac * float64(ans.Left))
+ ans.Top = int(y_frac * float64(ans.Top))
return &ans
-
}
func (self *ImageData) Resize(x_frac, y_frac float64) *ImageData {
ans := *self
- ans.Frames = utils.Map(func(f *ImageFrame) *ImageFrame { return f.Resize(x_frac, y_frac) }, self.Frames)
+ ans.Frames = make([]*ImageFrame, len(self.Frames))
+ if err := parallel.Run_in_parallel_over_range(1, func(start, limit int) {
+ for i := start; i < limit; i++ {
+ ans.Frames[i] = self.Frames[i].Resize(x_frac, y_frac)
+ }
+ }, 0, len(ans.Frames)); err != nil {
+ panic(err)
+ }
if len(ans.Frames) > 0 {
ans.Width, ans.Height = ans.Frames[0].Width, ans.Frames[0].Height
}
@@ -257,19 +211,18 @@ func MakeTempDir(template string) (ans string, err error) {
return os.MkdirTemp("", template)
}
-// Native {{{
-func OpenNativeImageFromReader(f io.ReadSeeker) (ans *ImageData, err error) {
- ic, _, err := imaging.DecodeAll(f)
- if err != nil {
- return nil, err
+func NewImageData(ic *imaging.Image) (ans *ImageData) {
+ b := ic.Bounds()
+ ans = &ImageData{
+ Width: b.Dx(), Height: b.Dy(),
+ }
+ if ic.Metadata != nil {
+ ans.Format_uppercase = strings.ToUpper(ic.Metadata.Format.String())
}
- _, _ = f.Seek(0, io.SeekStart)
-
- ans = &ImageData{Width: int(ic.Metadata.PixelWidth), Height: int(ic.Metadata.PixelHeight), Format_uppercase: strings.ToUpper(ic.Metadata.Format.String())}
for _, f := range ic.Frames {
fr := ImageFrame{
- Img: f.Image, Left: f.X, Top: f.Y, Width: f.Image.Bounds().Dx(), Height: f.Image.Bounds().Dy(),
+ Img: f.Image, Left: f.TopLeft.X, Top: f.TopLeft.Y, Width: f.Image.Bounds().Dx(), Height: f.Image.Bounds().Dy(),
Compose_onto: int(f.ComposeOnto), Number: int(f.Number), Delay_ms: int32(f.Delay.Milliseconds()),
Replace: f.Replace, Is_opaque: imaging.IsOpaque(f.Image),
}
@@ -281,395 +234,18 @@ 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...)
- }
- c := exec.Command(cmd[0], cmd[1:]...)
- output, err := c.Output()
- if err != nil {
- var exit_err *exec.ExitError
- if errors.As(err, &exit_err) {
- return nil, fmt.Errorf("Running the command: %s\nFailed with error:\n%s", strings.Join(cmd, " "), string(exit_err.Stderr))
- }
- return nil, fmt.Errorf("Could not find the program: %#v. Is ImageMagick installed and in your PATH?", cmd[0])
- }
- return output, nil
-}
-
-type IdentifyOutput struct {
- Fmt, Canvas, Transparency, Gap, Index, Size, Dpi, Dispose, Orientation string
-}
-
-type IdentifyRecord struct {
- Fmt_uppercase string
- Gap int
- Canvas struct{ Width, Height, Left, Top int }
- Width, Height int
- Dpi struct{ X, Y float64 }
- Index int
- Is_opaque bool
- Needs_blend bool
- Disposal int
- Dimensions_swapped bool
-}
-
-func parse_identify_record(ans *IdentifyRecord, raw *IdentifyOutput) (err error) {
- ans.Fmt_uppercase = strings.ToUpper(raw.Fmt)
- if raw.Gap != "" {
- ans.Gap, err = strconv.Atoi(raw.Gap)
- if err != nil {
- return fmt.Errorf("Invalid gap value in identify output: %s", raw.Gap)
- }
- ans.Gap = max(0, ans.Gap)
- }
- area, pos, found := strings.Cut(raw.Canvas, "+")
- ok := false
- if found {
- w, h, found := strings.Cut(area, "x")
- if found {
- ans.Canvas.Width, err = strconv.Atoi(w)
- if err == nil {
- ans.Canvas.Height, err = strconv.Atoi(h)
- if err == nil {
- x, y, found := strings.Cut(pos, "+")
- if found {
- ans.Canvas.Left, err = strconv.Atoi(x)
- if err == nil {
- if ans.Canvas.Top, err = strconv.Atoi(y); err == nil {
- ok = true
- }
- }
- }
- }
- }
- }
- }
- if !ok {
- return fmt.Errorf("Invalid canvas value in identify output: %s", raw.Canvas)
- }
- w, h, found := strings.Cut(raw.Size, "x")
- ok = false
- if found {
- ans.Width, err = strconv.Atoi(w)
- if err == nil {
- if ans.Height, err = strconv.Atoi(h); err == nil {
- ok = true
- }
- }
- }
- if !ok {
- return fmt.Errorf("Invalid size value in identify output: %s", raw.Size)
- }
- x, y, found := strings.Cut(raw.Dpi, "x")
- ok = false
- if found {
- ans.Dpi.X, err = strconv.ParseFloat(x, 64)
- if err == nil {
- if ans.Dpi.Y, err = strconv.ParseFloat(y, 64); err == nil {
- ok = true
- }
- }
- }
- if !ok {
- return fmt.Errorf("Invalid dpi value in identify output: %s", raw.Dpi)
- }
- ans.Index, err = strconv.Atoi(raw.Index)
- if err != nil {
- return fmt.Errorf("Invalid index value in identify output: %s", raw.Index)
- }
- q := strings.ToLower(raw.Transparency)
- if q == "blend" || q == "true" {
- ans.Is_opaque = false
- } else {
- ans.Is_opaque = true
- }
- ans.Needs_blend = q == "blend"
- switch strings.ToLower(raw.Dispose) {
- case "undefined":
- ans.Disposal = 0
- case "none":
- ans.Disposal = gif.DisposalNone
- case "background":
- ans.Disposal = gif.DisposalBackground
- case "previous":
- ans.Disposal = gif.DisposalPrevious
- default:
- return fmt.Errorf("Invalid value for dispose: %s", raw.Dispose)
- }
- switch raw.Orientation {
- case "5", "6", "7", "8":
- ans.Dimensions_swapped = true
- }
- if ans.Dimensions_swapped {
- ans.Canvas.Width, ans.Canvas.Height = ans.Canvas.Height, ans.Canvas.Width
- ans.Width, ans.Height = ans.Height, ans.Width
- }
-
- return
-}
-
-func IdentifyWithMagick(path string) (ans []IdentifyRecord, err error) {
- cmd := []string{"identify"}
- q := `{"fmt":"%m","canvas":"%g","transparency":"%A","gap":"%T","index":"%p","size":"%wx%h",` +
- `"dpi":"%xx%y","dispose":"%D","orientation":"%[EXIF:Orientation]"},`
- cmd = append(cmd, "-format", q, "--", path)
- output, err := RunMagick(path, cmd)
- if err != nil {
- return nil, fmt.Errorf("Failed to identify image at path: %s with error: %w", path, err)
- }
- output = bytes.TrimRight(bytes.TrimSpace(output), ",")
- raw_json := make([]byte, 0, len(output)+2)
- raw_json = append(raw_json, '[')
- raw_json = append(raw_json, output...)
- raw_json = append(raw_json, ']')
- var records []IdentifyOutput
- err = json.Unmarshal(raw_json, &records)
- if err != nil {
- return nil, fmt.Errorf("The ImageMagick identify program returned malformed output for the image at path: %s, with error: %w", path, err)
- }
- ans = make([]IdentifyRecord, len(records))
- for i, rec := range records {
- err = parse_identify_record(&ans[i], &rec)
- if err != nil {
- return nil, err
- }
- }
- return ans, nil
-}
-
-type RenderOptions struct {
- RemoveAlpha *imaging.NRGBColor
- Flip, Flop bool
- ResizeTo image.Point
- OnlyFirstFrame bool
- TempfilenameTemplate string
-}
-
-func RenderWithMagick(path string, ro *RenderOptions, frames []IdentifyRecord) (ans []*ImageFrame, fmap map[int]string, err error) {
- cmd := []string{"convert"}
- ans = make([]*ImageFrame, 0, len(frames))
- fmap = make(map[int]string, len(frames))
-
- defer func() {
- if err != nil {
- for _, f := range fmap {
- os.Remove(f)
- }
- }
- }()
-
- if ro.RemoveAlpha != nil {
- cmd = append(cmd, "-background", ro.RemoveAlpha.AsSharp(), "-alpha", "remove")
- } else {
- cmd = append(cmd, "-background", "none")
- }
- if ro.Flip {
- cmd = append(cmd, "-flip")
- }
- if ro.Flop {
- cmd = append(cmd, "-flop")
- }
- cpath := path
- if ro.OnlyFirstFrame {
- cpath += "[0]"
- }
- has_multiple_frames := len(frames) > 1
- get_multiple_frames := has_multiple_frames && !ro.OnlyFirstFrame
- cmd = append(cmd, "--", cpath, "-auto-orient")
- if ro.ResizeTo.X > 0 {
- rcmd := []string{"-resize", fmt.Sprintf("%dx%d!", ro.ResizeTo.X, ro.ResizeTo.Y)}
- if get_multiple_frames {
- cmd = append(cmd, "-coalesce")
- cmd = append(cmd, rcmd...)
- cmd = append(cmd, "-deconstruct")
- } else {
- cmd = append(cmd, rcmd...)
- }
- }
- cmd = append(cmd, "-depth", "8", "-set", "filename:f", "%w-%h-%g-%p")
- if get_multiple_frames {
- cmd = append(cmd, "+adjoin")
- }
- tdir, err := MakeTempDir(ro.TempfilenameTemplate)
+func OpenImageFromPath(path string, opts ...imaging.DecodeOption) (ans *ImageData, err error) {
+ ic, err := imaging.OpenAll(path, opts...)
if err != nil {
- err = fmt.Errorf("Failed to create temporary directory to hold ImageMagick output with error: %w", err)
- return
- }
- defer os.RemoveAll(tdir)
- mode := "rgba"
- if frames[0].Is_opaque {
- mode = "rgb"
- }
- cmd = append(cmd, filepath.Join(tdir, "im-%[filename:f]."+mode))
- _, err = RunMagick(path, cmd)
- if err != nil {
- return
- }
- entries, err := os.ReadDir(tdir)
- if err != nil {
- err = fmt.Errorf("Failed to read temp dir used to store ImageMagick output with error: %w", err)
- return
- }
- base_dir := filepath.Dir(tdir)
- gaps := make([]int, len(frames))
- for i, frame := range frames {
- gaps[i] = frame.Gap
- }
- // although ImageMagick *might* be already taking care of this adjustment,
- // I dont know for sure, so do it anyway.
- min_gap := gifmeta.CalcMinimumGap(gaps)
- for _, entry := range entries {
- fname := entry.Name()
- p, _, _ := strings.Cut(fname, ".")
- parts := strings.Split(p, "-")
- if len(parts) < 5 {
- continue
- }
- index, cerr := strconv.Atoi(parts[len(parts)-1])
- if cerr != nil || index < 0 || index >= len(frames) {
- continue
- }
- width, cerr := strconv.Atoi(parts[1])
- if cerr != nil {
- continue
- }
- height, cerr := strconv.Atoi(parts[2])
- if cerr != nil {
- continue
- }
- _, pos, found := strings.Cut(parts[3], "+")
- if !found {
- continue
- }
- px, py, found := strings.Cut(pos, "+")
- if !found {
- continue
- }
- x, cerr := strconv.Atoi(px)
- if cerr != nil {
- continue
- }
- y, cerr := strconv.Atoi(py)
- if cerr != nil {
- continue
- }
- identify_data := frames[index]
- df, cerr := os.CreateTemp(base_dir, TempTemplate+"."+mode)
- if cerr != nil {
- err = fmt.Errorf("Failed to create a temporary file in %s with error: %w", base_dir, cerr)
- return
- }
- err = os.Rename(filepath.Join(tdir, fname), df.Name())
- if err != nil {
- err = fmt.Errorf("Failed to rename a temporary file in %s with error: %w", tdir, err)
- return
- }
- df.Close()
- fmap[index+1] = df.Name()
- frame := ImageFrame{
- Number: index + 1, Width: width, Height: height, Left: x, Top: y, Is_opaque: identify_data.Is_opaque,
- }
- frame.Delay_ms = int32(max(min_gap, identify_data.Gap) * 10)
- err = check_resize(&frame, df.Name())
- if err != nil {
- return
- }
- ans = append(ans, &frame)
- }
- if len(ans) < len(frames) {
- err = fmt.Errorf("Failed to render %d out of %d frames", len(frames)-len(ans), len(frames))
- return
- }
- slices.SortFunc(ans, func(a, b *ImageFrame) int { return a.Number - b.Number })
- anchor_frame := uint(1)
- for i, frame := range ans {
- af, co := gifmeta.SetGIFFrameDisposal(uint(frame.Number), anchor_frame, byte(frames[i].Disposal))
- anchor_frame, frame.Compose_onto = af, int(co)
+ return nil, err
}
- return
+ return NewImageData(ic), nil
}
-func OpenImageFromPathWithMagick(path string) (ans *ImageData, err error) {
- identify_records, err := IdentifyWithMagick(path)
+func OpenImageFromReader(r io.Reader, opts ...imaging.DecodeOption) (ans *ImageData, s io.Reader, err error) {
+ ic, s, err := imaging.DecodeAll(r, opts...)
if err != nil {
- return nil, fmt.Errorf("Failed to identify image at %#v with error: %w", path, err)
+ return nil, nil, err
}
- frames, filenames, err := RenderWithMagick(path, &RenderOptions{}, identify_records)
- if err != nil {
- return nil, fmt.Errorf("Failed to render image at %#v with error: %w", path, err)
- }
- defer func() {
- for _, f := range filenames {
- os.Remove(f)
- }
- }()
-
- for _, frame := range frames {
- filename := filenames[frame.Number]
- data, err := os.ReadFile(filename)
- if err != nil {
- return nil, fmt.Errorf("Failed to read temp file for image %#v at %#v with error: %w", path, filename, err)
- }
- dest_rect := image.Rect(0, 0, frame.Width, frame.Height)
- if frame.Is_opaque {
- frame.Img = &imaging.NRGB{Pix: data, Stride: frame.Width * 3, Rect: dest_rect}
- } else {
- frame.Img = &image.NRGBA{Pix: data, Stride: frame.Width * 4, Rect: dest_rect}
- }
- }
- ans = &ImageData{
- Width: frames[0].Width, Height: frames[0].Height, Format_uppercase: identify_records[0].Fmt_uppercase, Frames: frames,
- }
- return ans, nil
-}
-
-// }}}
-
-func OpenImageFromPath(path string) (ans *ImageData, err error) {
- mt := utils.GuessMimeType(path)
- if DecodableImageTypes[mt] {
- f, err := os.Open(path)
- if err != nil {
- return nil, err
- }
- defer f.Close()
- ans, err = OpenNativeImageFromReader(f)
- if err != nil {
- return OpenImageFromPathWithMagick(path)
- }
- } else {
- return OpenImageFromPathWithMagick(path)
- }
- return
+ return NewImageData(ic), s, nil
}
diff --git a/tools/utils/images/serialize_test.go b/tools/utils/images/serialize_test.go
index 2860088894f..5a278115df6 100644
--- a/tools/utils/images/serialize_test.go
+++ b/tools/utils/images/serialize_test.go
@@ -12,7 +12,7 @@ import (
var _ = fmt.Print
func TestImageSerialize(t *testing.T) {
- img, err := OpenNativeImageFromReader(bytes.NewReader(kitty.KittyLogoAsPNGData))
+ img, _, err := OpenImageFromReader(bytes.NewReader(kitty.KittyLogoAsPNGData))
if err != nil {
t.Fatal(err)
}
diff --git a/tools/utils/images/to_rgba.go b/tools/utils/images/to_rgba.go
index 5e6a683017b..99287332750 100644
--- a/tools/utils/images/to_rgba.go
+++ b/tools/utils/images/to_rgba.go
@@ -8,6 +8,7 @@ import (
"math"
"github.com/kovidgoyal/imaging"
+ "github.com/kovidgoyal/imaging/nrgba"
"github.com/kovidgoyal/kitty/tools/tty"
)
@@ -57,7 +58,7 @@ func (self *Context) run_paste(src imaging.Scanner, background image.Image, pos
}
func (self *Context) paste_nrgba_onto_opaque(background *image.NRGBA, img image.Image, pos image.Point, bgcol *imaging.NRGBColor) {
- src := imaging.NewNRGBAScanner(img)
+ src := nrgba.NewNRGBAScanner(img)
if bgcol == nil {
self.run_paste(src, background, pos, func([]byte) {})
return
diff --git a/tools/utils/images/transforms.go b/tools/utils/images/transforms.go
deleted file mode 100644
index 8bf751414b8..00000000000
--- a/tools/utils/images/transforms.go
+++ /dev/null
@@ -1,58 +0,0 @@
-// License: GPLv3 Copyright: 2023, Kovid Goyal,
-
-package images
-
-import (
- "fmt"
-)
-
-var _ = fmt.Print
-
-func reverse_row(bytes_per_pixel int, pix []uint8) {
- if len(pix) <= bytes_per_pixel {
- return
- }
- i := 0
- j := len(pix) - bytes_per_pixel
- for i < j {
- pi := pix[i : i+bytes_per_pixel : i+bytes_per_pixel]
- pj := pix[j : j+bytes_per_pixel : j+bytes_per_pixel]
- for x := range bytes_per_pixel {
- pi[x], pj[x] = pj[x], pi[x]
- }
- i += bytes_per_pixel
- j -= bytes_per_pixel
- }
-}
-
-func (self *Context) FlipPixelsH(bytes_per_pixel, width, height int, pix []uint8) {
- stride := bytes_per_pixel * width
- 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
- if err := self.SafeParallel(0, num, func(ys <-chan int) {
- for y := range ys {
- upper := y
- lower := height - 1 - y
- a := upper * stride
- b := lower * stride
- as := pix[a : a+stride : a+stride]
- bs := pix[b : b+stride : b+stride]
- for i := range as {
- as[i], bs[i] = bs[i], as[i]
- }
- }
- }); err != nil {
- panic(err)
- }
-}
diff --git a/tools/utils/longest-common.go b/tools/utils/longest-common.go
index 1551395b06a..cbf46b862b3 100644
--- a/tools/utils/longest-common.go
+++ b/tools/utils/longest-common.go
@@ -58,14 +58,14 @@ func LongestCommon(next func() (string, bool), prefix bool) string {
return ""
}
if prefix {
- for i := 0; i < max_len; i++ {
+ for i := range max_len {
if xfix[i] != q[i] {
xfix = xfix[:i]
break
}
}
} else {
- for i := 0; i < max_len; i++ {
+ for i := range max_len {
xi := xfix_len - i - 1
si := q_len - i - 1
if xfix[xi] != q[si] {
diff --git a/tools/utils/paths.go b/tools/utils/paths.go
index 3c7e7e0273d..3dbced37d49 100644
--- a/tools/utils/paths.go
+++ b/tools/utils/paths.go
@@ -103,7 +103,7 @@ func ConfigDirForName(name string) (config_dir string) {
add(xh)
}
if dirs := os.Getenv("XDG_CONFIG_DIRS"); dirs != "" {
- for _, candidate := range strings.Split(dirs, ":") {
+ for candidate := range strings.SplitSeq(dirs, ":") {
add(candidate)
}
}
diff --git a/tools/utils/shm/fallocate_linux.go b/tools/utils/shm/fallocate_linux.go
deleted file mode 100644
index 8cd91b68d8e..00000000000
--- a/tools/utils/shm/fallocate_linux.go
+++ /dev/null
@@ -1,20 +0,0 @@
-// License: GPLv3 Copyright: 2023, Kovid Goyal,
-
-package shm
-
-import (
- "errors"
- "fmt"
-
- "golang.org/x/sys/unix"
-)
-
-var _ = fmt.Print
-
-func Fallocate_simple(fd int, size int64) (err error) {
- for {
- if err = unix.Fallocate(fd, 0, 0, size); !errors.Is(err, unix.EINTR) {
- return
- }
- }
-}
diff --git a/tools/utils/shm/fallocate_other.go b/tools/utils/shm/fallocate_other.go
deleted file mode 100644
index 3aa7ee5326b..00000000000
--- a/tools/utils/shm/fallocate_other.go
+++ /dev/null
@@ -1,16 +0,0 @@
-// License: GPLv3 Copyright: 2023, Kovid Goyal,
-
-//go:build !linux
-
-package shm
-
-import (
- "errors"
- "fmt"
-)
-
-var _ = fmt.Print
-
-func Fallocate_simple(fd int, size int64) (err error) {
- return errors.ErrUnsupported
-}
diff --git a/tools/utils/shm/shm.go b/tools/utils/shm/shm.go
deleted file mode 100644
index a01f1251c4c..00000000000
--- a/tools/utils/shm/shm.go
+++ /dev/null
@@ -1,252 +0,0 @@
-// License: GPLv3 Copyright: 2022, Kovid Goyal,
-
-package shm
-
-import (
- "encoding/binary"
- "errors"
- "fmt"
- "io"
- "io/fs"
- "os"
- "strings"
-
- "github.com/kovidgoyal/kitty/tools/cli"
-
- "golang.org/x/sys/unix"
-)
-
-var _ = fmt.Print
-var ErrPatternHasSeparator = errors.New("The specified pattern has file path separators in it")
-var ErrPatternTooLong = errors.New("The specified pattern for the SHM name is too long")
-
-type ErrNotSupported struct {
- err error
-}
-
-func (self *ErrNotSupported) Error() string {
- return fmt.Sprintf("POSIX shared memory not supported on this platform: with underlying error: %v", self.err)
-}
-
-// prefix_and_suffix splits pattern by the last wildcard "*", if applicable,
-// returning prefix as the part before "*" and suffix as the part after "*".
-func prefix_and_suffix(pattern string) (prefix, suffix string, err error) {
- for i := 0; i < len(pattern); i++ {
- if os.IsPathSeparator(pattern[i]) {
- return "", "", ErrPatternHasSeparator
- }
- }
- if pos := strings.LastIndexByte(pattern, '*'); pos != -1 {
- prefix, suffix = pattern[:pos], pattern[pos+1:]
- } else {
- prefix = pattern
- }
- return prefix, suffix, nil
-}
-
-type MMap interface {
- Close() error
- Unlink() error
- Slice() []byte
- Name() string
- IsFileSystemBacked() bool
- FileSystemName() string
- Stat() (fs.FileInfo, error)
- Flush() error
- Seek(offset int64, whence int) (ret int64, err error)
- Read(b []byte) (n int, err error)
- Write(b []byte) (n int, err error)
-}
-
-type AccessFlags int
-
-const (
- READ AccessFlags = iota
- WRITE
- COPY
-)
-
-func mmap(sz int, access AccessFlags, fd int, off int64) ([]byte, error) {
- flags := unix.MAP_SHARED
- prot := unix.PROT_READ
- switch access {
- case COPY:
- prot |= unix.PROT_WRITE
- flags = unix.MAP_PRIVATE
- case WRITE:
- prot |= unix.PROT_WRITE
- }
-
- b, err := unix.Mmap(fd, off, sz, prot, flags)
- if err != nil {
- return nil, err
- }
- return b, nil
-}
-
-func munmap(s []byte) error {
- return unix.Munmap(s)
-}
-
-func CreateTemp(pattern string, size uint64) (MMap, error) {
- return create_temp(pattern, size)
-}
-
-func truncate_or_unlink(ans *os.File, size uint64, unlink func(string) error) (err error) {
- fd := int(ans.Fd())
- sz := int64(size)
- if err = Fallocate_simple(fd, sz); err != nil {
- if !errors.Is(err, errors.ErrUnsupported) {
- return fmt.Errorf("fallocate() failed on fd from shm_open(%s) with size: %d with error: %w", ans.Name(), size, err)
- }
- for {
- if err = unix.Ftruncate(fd, sz); !errors.Is(err, unix.EINTR) {
- break
- }
- }
- }
- if err != nil {
- _ = ans.Close()
- _ = unlink(ans.Name())
- return fmt.Errorf("Failed to ftruncate() SHM file %s to size: %d with error: %w", ans.Name(), size, err)
- }
- return
-}
-
-const NUM_BYTES_FOR_SIZE = 4
-
-var ErrRegionTooSmall = errors.New("mmaped region too small")
-
-func WriteWithSize(self MMap, b []byte, at int) error {
- if len(self.Slice()) < at+len(b)+NUM_BYTES_FOR_SIZE {
- return ErrRegionTooSmall
- }
- binary.BigEndian.PutUint32(self.Slice()[at:], uint32(len(b)))
- copy(self.Slice()[at+NUM_BYTES_FOR_SIZE:], b)
- return nil
-}
-
-func ReadWithSize(self MMap, at int) ([]byte, error) {
- s := self.Slice()[at:]
- if len(s) < NUM_BYTES_FOR_SIZE {
- return nil, ErrRegionTooSmall
- }
- size := int(binary.BigEndian.Uint32(self.Slice()[at : at+NUM_BYTES_FOR_SIZE]))
- s = s[NUM_BYTES_FOR_SIZE:]
- if len(s) < size {
- return nil, ErrRegionTooSmall
- }
- return s[:size], nil
-}
-
-func ReadWithSizeAndUnlink(name string, file_callback ...func(fs.FileInfo) error) ([]byte, error) {
- mmap, err := Open(name, 0)
- if err != nil {
- return nil, err
- }
- if len(file_callback) > 0 {
- s, err := mmap.Stat()
- if err != nil {
- return nil, fmt.Errorf("Failed to stat SHM file with error: %w", err)
- }
- for _, f := range file_callback {
- err = f(s)
- if err != nil {
- return nil, err
- }
- }
- }
- defer func() {
- mmap.Close()
- _ = mmap.Unlink()
- }()
- slice, err := ReadWithSize(mmap, 0)
- if err != nil {
- return nil, err
- }
- ans := make([]byte, len(slice))
- copy(ans, slice)
- return ans, nil
-}
-
-func Read(self MMap, b []byte) (n int, err error) {
- pos, err := self.Seek(0, io.SeekCurrent)
- if err != nil {
- return 0, err
- }
- if pos < 0 {
- pos = 0
- }
- s := self.Slice()
- sz := int64(len(s))
- if pos >= sz {
- return 0, io.EOF
- }
- n = copy(b, s[pos:])
- _, err = self.Seek(int64(n), io.SeekCurrent)
- return
-}
-
-func Write(self MMap, b []byte) (n int, err error) {
- if len(b) == 0 {
- return 0, nil
- }
- pos, _ := self.Seek(0, io.SeekCurrent)
- if pos < 0 {
- pos = 0
- }
- s := self.Slice()
- if pos >= int64(len(s)) {
- return 0, io.ErrShortWrite
- }
- n = copy(s[pos:], b)
- if _, err = self.Seek(int64(n), io.SeekCurrent); err != nil {
- return n, err
- }
- if n < len(b) {
- return n, io.ErrShortWrite
- }
- return n, nil
-}
-
-func test_integration_with_python(args []string) (rc int, err error) {
- switch args[0] {
- default:
- return 1, fmt.Errorf("Unknown test type: %s", args[0])
- case "read":
- data, err := ReadWithSizeAndUnlink(args[1])
- if err != nil {
- return 1, err
- }
- _, err = os.Stdout.Write(data)
- if err != nil {
- return 1, err
- }
- case "write":
- data, err := io.ReadAll(os.Stdin)
- if err != nil {
- return 1, err
- }
- mmap, err := CreateTemp("shmtest-", uint64(len(data)+NUM_BYTES_FOR_SIZE))
- if err != nil {
- return 1, err
- }
- if err = WriteWithSize(mmap, data, 0); err != nil {
- return 1, err
- }
- mmap.Close()
- fmt.Println(mmap.Name())
- }
- return 0, nil
-}
-
-func TestEntryPoint(root *cli.Command) {
- root.AddSubCommand(&cli.Command{
- Name: "shm",
- OnlyArgsAllowed: true,
- Run: func(cmd *cli.Command, args []string) (rc int, err error) {
- return test_integration_with_python(args)
- },
- })
-
-}
diff --git a/tools/utils/shm/shm_fs.go b/tools/utils/shm/shm_fs.go
deleted file mode 100644
index 73512a89ba6..00000000000
--- a/tools/utils/shm/shm_fs.go
+++ /dev/null
@@ -1,191 +0,0 @@
-// License: GPLv3 Copyright: 2022, Kovid Goyal,
-//go:build linux || netbsd || openbsd || dragonfly
-
-package shm
-
-import (
- "crypto/sha256"
- "errors"
- "fmt"
- "io"
- "io/fs"
- "os"
- "path/filepath"
- "runtime"
- "strings"
-
- "github.com/kovidgoyal/kitty/tools/utils"
-
- "golang.org/x/sys/unix"
-)
-
-var _ = fmt.Print
-
-type file_based_mmap struct {
- f *os.File
- pos int64
- region []byte
- unlinked bool
- special_name string
-}
-
-func ShmUnlink(name string) error {
- if runtime.GOOS == "openbsd" {
- return os.Remove(openbsd_shm_path(name))
- }
- if strings.HasPrefix(name, "/") {
- name = name[1:]
- }
- return os.Remove(filepath.Join(SHM_DIR, name))
-}
-
-func file_mmap(f *os.File, size uint64, access AccessFlags, truncate bool, special_name string) (MMap, error) {
- if truncate {
- err := truncate_or_unlink(f, size, os.Remove)
- if err != nil {
- return nil, err
- }
- }
- region, err := mmap(int(size), access, int(f.Fd()), 0)
- if err != nil {
- f.Close()
- os.Remove(f.Name())
- return nil, err
- }
- return &file_based_mmap{f: f, region: region, special_name: special_name}, nil
-}
-
-func (self *file_based_mmap) Seek(offset int64, whence int) (ret int64, err error) {
- switch whence {
- case io.SeekStart:
- self.pos = offset
- case io.SeekEnd:
- self.pos = int64(len(self.region)) + offset
- case io.SeekCurrent:
- self.pos += offset
- }
- return self.pos, nil
-}
-
-func (self *file_based_mmap) Read(b []byte) (n int, err error) {
- return Read(self, b)
-}
-
-func (self *file_based_mmap) Write(b []byte) (n int, err error) {
- return Write(self, b)
-}
-
-func (self *file_based_mmap) Stat() (fs.FileInfo, error) {
- return self.f.Stat()
-}
-
-func (self *file_based_mmap) Name() string {
- if self.special_name != "" {
- return self.special_name
- }
- return filepath.Base(self.f.Name())
-}
-
-func (self *file_based_mmap) Flush() error {
- return unix.Msync(self.region, unix.MS_SYNC)
-}
-
-func (self *file_based_mmap) FileSystemName() string {
- return self.f.Name()
-}
-
-func (self *file_based_mmap) Slice() []byte {
- return self.region
-}
-
-func (self *file_based_mmap) Close() (err error) {
- if self.region != nil {
- self.f.Close()
- err = munmap(self.region)
- self.region = nil
- }
- return err
-}
-
-func (self *file_based_mmap) Unlink() (err error) {
- if self.unlinked {
- return nil
- }
- self.unlinked = true
- return os.Remove(self.f.Name())
-}
-
-func (self *file_based_mmap) IsFileSystemBacked() bool { return true }
-
-func openbsd_shm_path(name string) string {
- hash := sha256.Sum256(utils.UnsafeStringToBytes(name))
- return filepath.Join(SHM_DIR, utils.UnsafeBytesToString(hash[:])+".shm")
-}
-
-func file_path_from_name(name string) string {
- // See https://github.com/openbsd/src/blob/master/lib/libc/gen/shm_open.c
- if runtime.GOOS == "openbsd" {
- return openbsd_shm_path(name)
- }
- return filepath.Join(SHM_DIR, name)
-}
-
-func create_temp(pattern string, size uint64) (ans MMap, err error) {
- special_name := ""
- var prefix, suffix string
- prefix, suffix, err = prefix_and_suffix(pattern)
- if err != nil {
- return
- }
- var f *os.File
- try := 0
- for {
- name := prefix + utils.RandomFilename() + suffix
- path := file_path_from_name(name)
- f, err = os.OpenFile(path, os.O_EXCL|os.O_CREATE|os.O_RDWR, 0600)
- if err != nil {
- if errors.Is(err, fs.ErrExist) {
- try += 1
- if try > 10000 {
- return nil, &os.PathError{Op: "createtemp", Path: prefix + "*" + suffix, Err: fs.ErrExist}
- }
- continue
- }
- if errors.Is(err, fs.ErrNotExist) {
- return nil, &ErrNotSupported{err: err}
- }
- return
- }
- break
- }
- return file_mmap(f, size, WRITE, true, special_name)
-}
-
-func open(name string) (*os.File, error) {
- ans, err := os.OpenFile(file_path_from_name(name), os.O_RDONLY, 0)
- if err != nil {
- if errors.Is(err, fs.ErrNotExist) {
- if _, serr := os.Stat(SHM_DIR); serr != nil && errors.Is(serr, fs.ErrNotExist) {
- return nil, &ErrNotSupported{err: serr}
- }
- }
- return nil, err
- }
- return ans, nil
-}
-
-func Open(name string, size uint64) (MMap, error) {
- ans, err := open(name)
- if err != nil {
- return nil, err
- }
- if size == 0 {
- s, err := ans.Stat()
- if err != nil {
- ans.Close()
- return nil, fmt.Errorf("Failed to stat SHM file with error: %w", err)
- }
- size = uint64(s.Size())
- }
- return file_mmap(ans, size, READ, false, name)
-}
diff --git a/tools/utils/shm/shm_syscall.go b/tools/utils/shm/shm_syscall.go
deleted file mode 100644
index ea0e2447901..00000000000
--- a/tools/utils/shm/shm_syscall.go
+++ /dev/null
@@ -1,200 +0,0 @@
-// License: GPLv3 Copyright: 2022, Kovid Goyal,
-//go:build darwin || freebsd
-
-package shm
-
-import (
- "errors"
- "fmt"
- "io"
- "io/fs"
- "os"
- "strings"
- "unsafe"
-
- "github.com/kovidgoyal/kitty/tools/utils"
-
- "golang.org/x/sys/unix"
-)
-
-var _ = fmt.Print
-
-// ByteSliceFromString makes a zero terminated byte slice from the string
-func ByteSliceFromString(s string) []byte {
- a := make([]byte, len(s)+1)
- copy(a, s)
- return a
-}
-
-func BytePtrFromString(s string) *byte {
- a := ByteSliceFromString(s)
- return &a[0]
-}
-
-func shm_unlink(name string) (err error) {
- bname := BytePtrFromString(name)
- for {
- _, _, errno := unix.Syscall(unix.SYS_SHM_UNLINK, uintptr(unsafe.Pointer(bname)), 0, 0)
- if errno != unix.EINTR {
- if errno != 0 {
- if errno == unix.ENOENT {
- err = fs.ErrNotExist
- } else {
- err = fmt.Errorf("shm_unlink() failed with error: %w", errno)
- }
- }
- break
- }
- }
- return
-}
-
-func ShmUnlink(name string) error {
- return shm_unlink(name)
-}
-
-func shm_open(name string, flags, perm int) (ans *os.File, err error) {
- bname := BytePtrFromString(name)
- var fd uintptr
- var errno unix.Errno
- for {
- fd, _, errno = unix.Syscall(unix.SYS_SHM_OPEN, uintptr(unsafe.Pointer(bname)), uintptr(flags), uintptr(perm))
- if errno != unix.EINTR {
- if errno != 0 {
- err = fmt.Errorf("shm_open() failed with error: %w", errno)
- }
- break
- }
- }
- if err == nil {
- ans = os.NewFile(fd, name)
- }
- return
-}
-
-type syscall_based_mmap struct {
- f *os.File
- pos int64
- region []byte
- unlinked bool
-}
-
-func syscall_mmap(f *os.File, size uint64, access AccessFlags, truncate bool) (MMap, error) {
- if truncate {
- err := truncate_or_unlink(f, size, shm_unlink)
- if err != nil {
- return nil, fmt.Errorf("truncate failed with error: %w", err)
- }
- }
- region, err := mmap(int(size), access, int(f.Fd()), 0)
- if err != nil {
- _ = f.Close()
- _ = shm_unlink(f.Name())
- return nil, fmt.Errorf("mmap failed with error: %w", err)
- }
- return &syscall_based_mmap{f: f, region: region}, nil
-}
-
-func (self *syscall_based_mmap) Name() string {
- return self.f.Name()
-}
-func (self *syscall_based_mmap) Stat() (fs.FileInfo, error) {
- return self.f.Stat()
-}
-
-func (self *syscall_based_mmap) Flush() error {
- return unix.Msync(self.region, unix.MS_SYNC)
-}
-
-func (self *syscall_based_mmap) Slice() []byte {
- return self.region
-}
-
-func (self *syscall_based_mmap) Close() (err error) {
- if self.region != nil {
- self.f.Close()
- munmap(self.region)
- self.region = nil
- }
- return
-}
-
-func (self *syscall_based_mmap) Unlink() (err error) {
- if self.unlinked {
- return nil
- }
- self.unlinked = true
- return shm_unlink(self.Name())
-}
-
-func (self *syscall_based_mmap) Seek(offset int64, whence int) (ret int64, err error) {
- switch whence {
- case io.SeekStart:
- self.pos = offset
- case io.SeekEnd:
- self.pos = int64(len(self.region)) + offset
- case io.SeekCurrent:
- self.pos += offset
- }
- return self.pos, nil
-}
-
-func (self *syscall_based_mmap) Read(b []byte) (n int, err error) {
- return Read(self, b)
-}
-
-func (self *syscall_based_mmap) Write(b []byte) (n int, err error) {
- return Write(self, b)
-}
-
-func (self *syscall_based_mmap) IsFileSystemBacked() bool { return false }
-func (self *syscall_based_mmap) FileSystemName() string { return "" }
-
-func create_temp(pattern string, size uint64) (ans MMap, err error) {
- var prefix, suffix string
- prefix, suffix, err = prefix_and_suffix(pattern)
- if err != nil {
- return
- }
- if SHM_REQUIRED_PREFIX != "" && !strings.HasPrefix(pattern, SHM_REQUIRED_PREFIX) {
- // FreeBSD requires name to start with /
- prefix = SHM_REQUIRED_PREFIX + prefix
- }
- var f *os.File
- try := 0
- for {
- name := prefix + utils.RandomFilename() + suffix
- if len(name) > SHM_NAME_MAX {
- return nil, ErrPatternTooLong
- }
- f, err = shm_open(name, os.O_EXCL|os.O_CREATE|os.O_RDWR, 0600)
- if err != nil && (errors.Is(err, fs.ErrExist) || errors.Unwrap(err) == unix.EEXIST) {
- try += 1
- if try > 10000 {
- return nil, &os.PathError{Op: "createtemp", Path: prefix + "*" + suffix, Err: fs.ErrExist}
- }
- continue
- }
- break
- }
- if err != nil {
- return nil, err
- }
- return syscall_mmap(f, size, WRITE, true)
-}
-
-func Open(name string, size uint64) (MMap, error) {
- ans, err := shm_open(name, os.O_RDONLY, 0)
- if err != nil {
- return nil, err
- }
- if size == 0 {
- s, err := ans.Stat()
- if err != nil {
- ans.Close()
- return nil, fmt.Errorf("Failed to stat SHM file with error: %w", err)
- }
- size = uint64(s.Size())
- }
- return syscall_mmap(ans, size, READ, false)
-}
diff --git a/tools/utils/shm/shm_test.go b/tools/utils/shm/shm_test.go
deleted file mode 100644
index 1cd2b7de8b7..00000000000
--- a/tools/utils/shm/shm_test.go
+++ /dev/null
@@ -1,61 +0,0 @@
-// License: GPLv3 Copyright: 2022, Kovid Goyal,
-
-package shm
-
-import (
- "crypto/rand"
- "errors"
- "fmt"
- "io/fs"
- "os"
- "reflect"
- "testing"
-)
-
-var _ = fmt.Print
-
-func TestSHM(t *testing.T) {
- data := make([]byte, 13347)
- _, _ = rand.Read(data)
- mm, err := CreateTemp("test-kitty-shm-", uint64(len(data)))
- if err != nil {
- t.Fatal(err)
- }
-
- copy(mm.Slice(), data)
- err = mm.Flush()
- if err != nil {
- t.Fatalf("Failed to msync() with error: %v", err)
- }
- err = mm.Close()
- if err != nil {
- t.Fatalf("Failed to close with error: %v", err)
- }
-
- g, err := Open(mm.Name(), uint64(len(data)))
- if err != nil {
- t.Fatal(err)
- }
- data2 := g.Slice()
- if !reflect.DeepEqual(data, data2) {
- t.Fatalf("Could not read back written data: Written data length: %d Read data length: %d", len(data), len(data2))
- }
- err = g.Close()
- if err != nil {
- t.Fatalf("Failed to close with error: %v", err)
- }
- err = g.Unlink()
- if err != nil {
- t.Fatalf("Failed to unlink with error: %v", err)
- }
- g, err = Open(mm.Name(), uint64(len(data)))
- if err == nil {
- t.Fatalf("Unlinking failed could re-open the SHM data. Data equal: %v Data length: %d", reflect.DeepEqual(g.Slice(), data), len(g.Slice()))
- }
- if mm.IsFileSystemBacked() {
- _, err = os.Stat(mm.FileSystemName())
- if !errors.Is(err, fs.ErrNotExist) {
- t.Fatalf("Unlinking %s did not work", mm.Name())
- }
- }
-}
diff --git a/tools/utils/shm/specific_darwin.go b/tools/utils/shm/specific_darwin.go
deleted file mode 100644
index 99222e9523d..00000000000
--- a/tools/utils/shm/specific_darwin.go
+++ /dev/null
@@ -1,7 +0,0 @@
-// License: GPLv3 Copyright: 2022, Kovid Goyal,
-
-package shm
-
-const SHM_NAME_MAX = 30
-const SHM_REQUIRED_PREFIX = ""
-const SHM_DIR = ""
diff --git a/tools/utils/shm/specific_dragonfly.go b/tools/utils/shm/specific_dragonfly.go
deleted file mode 100644
index 7e5bdcd36c8..00000000000
--- a/tools/utils/shm/specific_dragonfly.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// License: GPLv3 Copyright: 2022, Kovid Goyal,
-
-package shm
-
-import (
- "fmt"
-)
-
-var _ = fmt.Print
-
-// https://www.dragonflybsd.org/cgi/web-man?command=shm_open§ion=3
-const SHM_DIR = "/var/run/shm"
diff --git a/tools/utils/shm/specific_freebsd.go b/tools/utils/shm/specific_freebsd.go
deleted file mode 100644
index b2c7b6b88ee..00000000000
--- a/tools/utils/shm/specific_freebsd.go
+++ /dev/null
@@ -1,7 +0,0 @@
-// License: GPLv3 Copyright: 2022, Kovid Goyal,
-
-package shm
-
-const SHM_NAME_MAX = 1023
-const SHM_REQUIRED_PREFIX = "/"
-const SHM_DIR = ""
diff --git a/tools/utils/shm/specific_linux.go b/tools/utils/shm/specific_linux.go
deleted file mode 100644
index cab8901e757..00000000000
--- a/tools/utils/shm/specific_linux.go
+++ /dev/null
@@ -1,11 +0,0 @@
-// License: GPLv3 Copyright: 2022, Kovid Goyal,
-
-package shm
-
-import (
- "fmt"
-)
-
-var _ = fmt.Print
-
-const SHM_DIR = "/dev/shm"
diff --git a/tools/utils/shm/specific_netbsd.go b/tools/utils/shm/specific_netbsd.go
deleted file mode 100644
index 209095a2ebc..00000000000
--- a/tools/utils/shm/specific_netbsd.go
+++ /dev/null
@@ -1,11 +0,0 @@
-// License: GPLv3 Copyright: 2022, Kovid Goyal,
-
-package shm
-
-import (
- "fmt"
-)
-
-var _ = fmt.Print
-
-const SHM_DIR = "/var/shm"
diff --git a/tools/utils/shm/specific_openbsd.go b/tools/utils/shm/specific_openbsd.go
deleted file mode 100644
index e1b604e62ee..00000000000
--- a/tools/utils/shm/specific_openbsd.go
+++ /dev/null
@@ -1,11 +0,0 @@
-// License: GPLv3 Copyright: 2022, Kovid Goyal,
-
-package shm
-
-import (
- "fmt"
-)
-
-var _ = fmt.Print
-
-const SHM_DIR = "/tmp"
diff --git a/tools/utils/short-uuid.go b/tools/utils/short-uuid.go
index ebdd43b6b01..c625a233ec1 100644
--- a/tools/utils/short-uuid.go
+++ b/tools/utils/short-uuid.go
@@ -21,10 +21,7 @@ func num_to_string(number *big.Int, alphabet []rune, alphabet_len *big.Int, pad_
if number.Sign() < 0 {
*number = zero
}
- capacity := 64
- if pad_to_length > capacity {
- capacity = pad_to_length
- }
+ capacity := max(pad_to_length, 64)
ans := make([]rune, 0, capacity)
for number.Cmp(&zero) == 1 {
number.DivMod(number, alphabet_len, &digit)
diff --git a/tools/vt/linebuf.go b/tools/vt/linebuf.go
index 73d02f1b93f..3cac1dbd4c2 100644
--- a/tools/vt/linebuf.go
+++ b/tools/vt/linebuf.go
@@ -18,7 +18,7 @@ type LineBuf struct {
func NewLineBuf(xnum, ynum uint) *LineBuf {
lm := make([]uint, ynum, ynum+extra_capacity)
var i uint
- for i = 0; i < ynum; i++ {
+ for i = range ynum {
lm[i] = i
}
return &LineBuf{
diff --git a/tools/wcswidth/wcswidth.go b/tools/wcswidth/wcswidth.go
index 02ea80a3205..332ac08f62e 100644
--- a/tools/wcswidth/wcswidth.go
+++ b/tools/wcswidth/wcswidth.go
@@ -49,7 +49,7 @@ func (self *WCWidthIterator) handle_csi(csi []byte) error {
num_string := utils.UnsafeBytesToString(csi[:len(csi)-1])
n, err := strconv.Atoi(num_string)
if err == nil && n > 0 {
- for i := 0; i < n; i++ {
+ for range n {
err = self.handle_rune(self.prev_ch)
if err != nil {
return err