Compare commits

..

47 Commits

Author SHA1 Message Date
Kovid Goyal
40f9945ea7 version 0.46.1 2026-03-16 13:20:03 +05:30
Kovid Goyal
f4d4a80ca3 Merge branch 'dependabot/go_modules/all-go-deps-48d5999cd7' of https://github.com/kovidgoyal/kitty 2026-03-16 09:19:20 +05:30
Kovid Goyal
0c90b66f91 ... 2026-03-16 09:18:50 +05:30
dependabot[bot]
1f2149eb6f Bump golang.org/x/sys from 0.41.0 to 0.42.0 in the all-go-deps group
Bumps the all-go-deps group with 1 update: [golang.org/x/sys](https://github.com/golang/sys).


Updates `golang.org/x/sys` from 0.41.0 to 0.42.0
- [Commits](https://github.com/golang/sys/compare/v0.41.0...v0.42.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-version: 0.42.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-go-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 03:43:04 +00:00
Kovid Goyal
ddfd8d7f45 Wayland: Dont set window icons on layer shell windows as some compositors complain about them 2026-03-16 08:28:36 +05:30
copilot-swe-agent[bot]
66ffb6895c Fix macOS keyboard focus not restored when switching back from another space
Fixes #9665
Fixes #9666
2026-03-15 14:57:46 +05:30
Kovid Goyal
b3b7d0596d Dont use neighboring tab colors for tab bar margins on translucent OS windows
The colors are rendered semi-transparent which effectively means they blend
with background unlike the neighboring colors and end up not matching
2026-03-15 09:22:43 +05:30
copilot-swe-agent[bot]
b93f1badd1 Fix tab bar border artifacts with background_opacity on non-cell-aligned windows
Fixes #9663: visible gray/black bars appear at the left and right
sides of the tab bar when background_opacity is set and the window width
is not a multiple of the cell width.

Three fixes in update_blank_rects:

1. Fix off-by-one: use g.right instead of g.right-1 for the right blank
   rect start position. The old code caused a 1-pixel overlap with tab
   content and spuriously added a right blank rect even for
   perfectly-aligned windows.

2. Fix blank rect colors: only use tab_bar_left/right_edge_color when
   tab_bar_margin_width > 0 (explicit configured margin). When
   margin_width=0 (default), use default_bg which blends with the window
   background instead of showing a solid tab-colored bar.

3. Fix inner margin BOTTOM_EDGE height: use tab_bar.top instead of vh
   so the inner margin blank rect covers only the inner margin area.

Fixes #9664

Co-authored-by: kovidgoyal <1308621+kovidgoyal@users.noreply.github.com>
2026-03-15 09:00:41 +05:30
Kovid Goyal
30756bb819 Cleanup previous PR 2026-03-15 08:38:59 +05:30
Kovid Goyal
d244a697bf Merge branch 'dictation_fix' of https://github.com/alexeyshurygin/kitty 2026-03-15 08:34:46 +05:30
Alexey Shurygin
827c8dd614 Address PR review feedback 2026-03-15 01:33:38 +03:00
Alexey Shurygin
b66c6c4932 Fix: Dictation does not start on double Fn key press on Mac OS 2026-03-15 01:15:05 +03:00
Kovid Goyal
e72f49952d Linux: Fix regression that broke drag and drop from GTK applications
Fixes #9656
2026-03-13 20:19:33 +05:30
copilot-swe-agent[bot]
a97f468a02 Add remove_duplicate_mimes() and call it from update_allowed_mimes_for_drop()
Fixes #9657
2026-03-13 20:01:43 +05:30
Kovid Goyal
c332211997 Possible fix for #9656 2026-03-13 19:16:44 +05:30
Kovid Goyal
98931d99b0 Wayland: Fix momentum scrolling not working on compositors that send a stop frame with no axis information
Fixes #9653
2026-03-13 08:47:45 +05:30
Kovid Goyal
5585b773bf Merge branch 'master' of https://github.com/1024th/kitty 2026-03-13 08:23:39 +05:30
1024th
ffe8cef1a8 Fix font detection command on linux in setup.py 2026-03-13 01:03:20 +08:00
Kovid Goyal
c0b549fee8 Make XI2 scroll offset type detection more robust
Since we have to use heuristics, lets at least collect stats for a few
events before deciding.
2026-03-12 21:44:20 +05:30
Kovid Goyal
99639f1373 XInput2: Assume any fractional scroll value means the device is high res
Because as far as I can tell there is no reliable way to detect high res
scroll devices under XWayland. Sigh.
2026-03-12 20:32:15 +05:30
Kovid Goyal
5ba3d10471 X11: Fix a regression that caused some high res scroll devices to be treated as line based scroll devices
Apparently when running under XWayland, we cant rely on libinput to
detect highres scroll devices. Sigh. Linux input is such a disaster.
Dunno if this will break something else, hopefully not.

Fixes #9649
2026-03-12 19:23:41 +05:30
Kovid Goyal
39683e0c34 ... 2026-03-12 18:33:52 +05:30
Kovid Goyal
2c395a1ef9 Fix #9650 2026-03-12 18:03:53 +05:30
Kovid Goyal
01a70e06c4 ... 2026-03-12 13:53:41 +05:30
Kovid Goyal
0614f05335 Use new SIMD function for non blank checking 2026-03-12 13:50:35 +05:30
Kovid Goyal
7b9b8834a8 ... 2026-03-12 13:48:20 +05:30
copilot-swe-agent[bot]
f4bf9cf1c9 Add not_index_byte and not_index_byte2 functions to simdstring package
Fixes #9646
2026-03-12 13:47:05 +05:30
Kovid Goyal
d8af7e2c88 Add an option to control highlighting of moved lines 2026-03-12 12:59:46 +05:30
copilot-swe-agent[bot]
f45345c7a7 Add colorMoved support to kitty diff kitten
Fixes #3241
Fixes #9644
2026-03-12 12:44:21 +05:30
Kovid Goyal
c78592174d ... 2026-03-12 12:23:58 +05:30
Kovid Goyal
3f16aab664 Merge branch 'patch-1' of https://github.com/ChangMarkusYu/kitty 2026-03-12 12:23:34 +05:30
Chang Yu
9f45daf300 Fix bookorat link in integrations.rst 2026-03-11 22:26:17 -07:00
Kovid Goyal
a0709acdde DRYer 2026-03-12 10:06:20 +05:30
Kovid Goyal
b9d7a661ce Dont import ctypes unless type checking
ctypes in Python 3.14 is broken on Intel macs. It uses libffi which is
broken on Intel macs without extra security entitlements. At least
com.apple.security.cs.allow-unsigned-executable-memory and possibly
com.apple.security.cs.disable-executable-page-protection

Rather than add these entitlements, we simply do not import ctypes as it
is not actually used on macs anyway.

Fixes #9643
2026-03-12 09:58:49 +05:30
Kovid Goyal
c2447abd30 Update changelog 2026-03-12 09:22:02 +05:30
Kovid Goyal
36810862ed Update changelog 2026-03-12 08:57:57 +05:30
Kovid Goyal
01104bac65 Fix a regression that broke `kitten update-self`
Fixes #9642
2026-03-12 08:33:20 +05:30
Kovid Goyal
99ff621b21 Merge branch 'fix/clear-dock-badge-on-interaction' of https://github.com/pietervdheijden/kitty 2026-03-12 08:14:19 +05:30
Kovid Goyal
633d833907 Merge branch 'fix-paste' of https://github.com/waresnew/kitty 2026-03-12 08:04:05 +05:30
Pieter van der Heijden
e9de88221f Clear macOS dock badge on keypress and mouse click
The dock badge set by macos_dock_badge_on_bell is currently only
cleared when the app transitions from inactive to active via
NSApplicationDidBecomeActiveNotification. This means if a bell
occurs while kitty is already the active app (e.g. in a background
tmux pane or non-focused kitty tab), the badge persists until the
user switches away and back.

Clear the badge on any user interaction (keypress or mouse click)
so it is dismissed as soon as the user engages with kitty, regardless
of whether kitty was already active.
2026-03-11 21:41:06 +01:00
newwares
50a69cb093 Fix outdated check in paste confirmation 2026-03-11 16:37:48 -04:00
Kovid Goyal
a1e02c8858 ... 2026-03-11 21:24:49 +05:30
Kovid Goyal
e64fbe145e ... 2026-03-11 21:24:24 +05:30
Kovid Goyal
121eec48b5 Make the changelog link to options rather than PRs 2026-03-11 21:21:33 +05:30
Kovid Goyal
9aa938c6cd Make the error message a bit more clear 2026-03-11 21:13:24 +05:30
Kovid Goyal
4ad6d30ab8 Add command palette shortcut to changelog 2026-03-11 21:03:57 +05:30
Kovid Goyal
74f532bd07 Update major changes 2026-03-11 08:53:58 +05:30
32 changed files with 663 additions and 49 deletions

View File

@@ -9,6 +9,21 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
Recent major new features
---------------------------
Mousing [0.46]
~~~~~~~~~~~~~~~
kitty already had excellent mouse support, but now it is taking it to the next
level. The kitty scrollback buffer grew support for :opt:`smooth scrolling
<pixel_scroll>` and :opt:`momentum based scrolling <momentum_scroll>`
for a natural, smooth and kinetic scrolling experience.
Additionally, you can now :opt:`drag kitty tabs around <tab_bar_drag_threshold>` with the mouse
to re-order them, move them to another kitty OS Window or even detach them into
their own OS Window.
Finally, a long requested feature, the ability to resize kitty windows (aka
splits) with the mouse was implemented.
Choose files, fast [0.45]
~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -150,6 +165,31 @@ consumption to do the same tasks.
Detailed list of changes
-------------------------------------
0.46.1 [2026-03-16]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- diff kitten: Highlight moved lines using a different background color (:opt:`kitten-diff.mark_moved_lines`) (:iss:`3241`)
- Fix a regression that broke ``kitten update-self`` (:iss:`9642`)
- macOS: Clear bell alert badge on dock icon on mouse/keyboard activity (:iss:`9640`)
- Fix a regression that broke accept anyway shortcut in the paste confirmation dialog (:pull:`9640`)
- Fix kitty hanging on startup on Intel macs (:iss:`9643`)
- X11: Fix a regression that caused some high res scroll devices to be treated as line based scroll devices (:iss:`9649`)
- Wayland: Fix momentum scrolling not working on compositors that send a stop frame with no axis information (:iss:`9653`)
- Linux: Fix regression that broke drag and drop from GTK applications (:iss:`9656`)
- macOS: Fix using Fn key for start dictation not working (:iss:`9661`)
- Don't use neighboring tab colors for tab bar margins in translucent windows (:iss:`9663`)
- macOS: Fix OS window focus not restored when switching spaces (:iss:`9665`)
0.46.0 [2026-03-11]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -161,7 +201,7 @@ Detailed list of changes
- macOS: Implement support for Apple dictation to input text in kitty (:iss:`3732`)
- Allow dragging tabs in the tab bar to re-order, move to another OS Window or
- Allow dragging tabs (opt:`tab_bar_drag_threshold`) in the tab bar to re-order, move to another OS Window or
detach (:pull:`9296`)
- Allow dragging window borders to resize kitty windows in all the different
@@ -170,7 +210,7 @@ Detailed list of changes
- Allow showing :opt:`configurable window titles <window_title_bar>` for individual kitty
windows via a window title bar (:pull:`9450`)
- A command palette to browse and trigger all mapped and unmapped actions
- A command palette (:sc:`command_palette`) to browse and trigger all mapped and unmapped actions
(:pull:`9545`)
- choose-files kitten: Fix JXL image preview not working (:iss:`9323`)

View File

@@ -14,9 +14,9 @@ Image and document viewers
Powered by kitty's :doc:`graphics-protocol` there exist many tools for viewing
images and other types of documents directly in your terminal, even over SSH.
.. _tool_bookorat:
.. _tool_bookokrat:
`bookorat <https://github.com/bugzmanov/bookokrat>`
`bookokrat <https://github.com/bugzmanov/bookokrat>`_
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
A terminal PDF/EPUB viewer

View File

@@ -311,6 +311,24 @@ static NSDictionary<NSString*,NSNumber*> *global_shortcuts = nil;
}
}
- (void)applicationDidBecomeActive:(NSNotification *)notification {
(void)notification;
// When the application becomes active after switching spaces (e.g., swiping
// back from a fullscreen app on another space), macOS may not send
// windowDidBecomeKey: for the already-key window. This leaves GLFW thinking
// no window has focus (since windowDidResignKey: was sent when leaving).
// Ensure GLFW's focus state is updated to match the actual key window.
NSWindow *keyWindow = [NSApp keyWindow];
if (keyWindow && !_glfw.focusedWindowId) {
for (_GLFWwindow *window = _glfw.windowListHead; window; window = window->next) {
if (window->ns.object == keyWindow) {
if (_glfw.focusedWindowId != window->id) _glfwInputWindowFocus(window, true);
break;
}
}
}
}
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
{
(void)sender;
@@ -839,6 +857,24 @@ is_apple_jis_layout_function_key(NSEvent *event) {
return [event keyCode] == 0x66 /* kVK_JIS_Eisu */ || [event keyCode] == 0x68 /* kVK_JIS_Kana */;
}
static bool
has_apple_fn_global_shortcut(void) {
NSDictionary *hitoolbox_settings = [[NSUserDefaults standardUserDefaults] persistentDomainForName:@"com.apple.HIToolbox"];
id obj = [hitoolbox_settings objectForKey:@"AppleFnUsageType"];
if (![obj isKindOfClass:[NSNumber class]]) return false;
// Non-zero AppleFnUsageType means macOS has reserved Fn/Globe for a
// system action such as input source switching, emoji picker, or dictation.
return [obj integerValue] != 0;
}
static bool
is_apple_fn_global_shortcut(NSEvent *event) {
if ([event keyCode] != 0x3f /* kVK_Function */) return false;
NSEventModifierFlags mods = USEFUL_MODS([event modifierFlags]);
if (mods != 0 && mods != NSEventModifierFlagFunction) return false;
return has_apple_fn_global_shortcut();
}
GLFWAPI GLFWapplicationshouldhandlereopenfun glfwSetApplicationShouldHandleReopen(GLFWapplicationshouldhandlereopenfun callback) {
GLFWapplicationshouldhandlereopenfun previous = handle_reopen_callback;
handle_reopen_callback = callback;
@@ -949,6 +985,10 @@ int _glfwPlatformInit(bool *supports_window_occlusion)
debug_key("-------------- flags changed -----------------\n");
debug_key("%s\n", [[event description] UTF8String]);
last_keydown_shortcut_event.virtual_key_code = 0xffff;
if (!_glfw.ignoreOSKeyboardProcessing && !_glfw.keyboard_grabbed && is_apple_fn_global_shortcut(event)) {
debug_key("flagsChanged triggered global fn shortcut ignoring\n");
return event;
}
// switching to the next input source is only confirmed when all modifier keys are released
if (last_keydown_shortcut_event.input_source_switch_modifiers) {
if (!([event modifierFlags] & last_keydown_shortcut_event.input_source_switch_modifiers))

View File

@@ -99,6 +99,22 @@ polymorphic_string_as_utf8(id string) {
return [characters UTF8String];
}
static bool
forward_dictation_selector_to_app(SEL selector, id sender) {
static SEL start_dictation_selector = NULL, stop_dictation_selector = NULL;
if (start_dictation_selector == NULL) {
start_dictation_selector = NSSelectorFromString(@"startDictation:");
stop_dictation_selector = NSSelectorFromString(@"stopDictation:");
}
if (selector != start_dictation_selector && selector != stop_dictation_selector) return false;
if ([NSApp respondsToSelector:selector]) {
debug_key("Forwarding %s to NSApp\n", [NSStringFromSelector(selector) UTF8String]);
[NSApp performSelector:selector withObject:sender];
return true;
}
return false;
}
static uint32_t
vk_code_to_functional_key_code(uint8_t key_code) { // {{{
switch(key_code) {
@@ -842,6 +858,7 @@ static void update_titlebar_button_visibility_after_fullscreen_transition(_GLFWw
// With the default macOS keybindings, pressing certain key combinations
// (e.g. Ctrl+/, Ctrl+Cmd+Down/Left/Right) will produce a beep sound.
debug_key("\n\tTextInputCtx: doCommandBySelector: (%s)\n", [NSStringFromSelector(selector) UTF8String]);
if (forward_dictation_selector_to_app(selector, nil)) return;
}
@end // }}}
@@ -1930,6 +1947,17 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) {
- (void)doCommandBySelector:(SEL)selector
{
debug_key("\n\tdoCommandBySelector: (%s)\n", [NSStringFromSelector(selector) UTF8String]);
if (forward_dictation_selector_to_app(selector, self)) return;
}
- (void)startDictation:(id)sender
{
forward_dictation_selector_to_app(_cmd, sender);
}
- (void)stopDictation:(id)sender
{
forward_dictation_selector_to_app(_cmd, sender);
}
- (BOOL)isAccessibilityElement

10
glfw/wl_init.c vendored
View File

@@ -206,27 +206,35 @@ pointer_handle_frame(void *data UNUSED, struct wl_pointer *pointer UNUSED) {
_GLFWwindow* window = _glfw.wl.pointerFocus;
if (!window) return;
GLFWScrollEvent ev = {.keyboard_modifiers=_glfw.wl.xkb.states.modifiers};
bool found = false;
if (info.discrete.y_axis_type != AXIS_EVENT_UNKNOWN) {
ev.unscaled.y = info.discrete.y;
if (info.discrete.y_axis_type == AXIS_EVENT_VALUE120) ev.offset_type = GLFW_SCROLL_OFFEST_V120;
found = true;
} else if (info.continuous.y_axis_type != AXIS_EVENT_UNKNOWN) {
ev.offset_type = GLFW_SCROLL_OFFEST_HIGHRES;
ev.unscaled.y = info.continuous.y;
found = true;
}
if (info.discrete.x_axis_type != AXIS_EVENT_UNKNOWN) {
ev.unscaled.x = info.discrete.x;
if (info.discrete.x_axis_type == AXIS_EVENT_VALUE120) ev.offset_type = GLFW_SCROLL_OFFEST_V120;
found = true;
} else if (info.continuous.x_axis_type != AXIS_EVENT_UNKNOWN) {
ev.offset_type = GLFW_SCROLL_OFFEST_HIGHRES;
ev.unscaled.x = info.continuous.x;
found = true;
}
bool stopped = info.y_stop_received || info.x_stop_received;
if (!found && stopped) ev.offset_type = window->wl.prev_frame_offset_type;
ev.unscaled.x *= -1;
const double scale = ev.offset_type == GLFW_SCROLL_OFFEST_HIGHRES ? _glfwWaylandWindowScale(window) : 1;
ev.x_offset = scale * ev.unscaled.x; ev.y_offset = scale * ev.unscaled.y;
glfw_handle_scroll_event_for_momentum(
window, &ev, info.y_stop_received || info.x_stop_received, info.source_type == WL_POINTER_AXIS_SOURCE_FINGER);
window, &ev, stopped, info.source_type == WL_POINTER_AXIS_SOURCE_FINGER);
window->wl.prev_frame_offset_type = ev.offset_type;
/* clear pointer_curr_axis_info for next frame */
memset(&info, 0, sizeof(info));
}

1
glfw/wl_platform.h vendored
View File

@@ -212,6 +212,7 @@ typedef struct _GLFWwindowWayland
uint32_t source_type;
monotonic_t x_start_time, x_stop_time, y_stop_time, y_start_time;
} pointer_curr_axis_info;
GLFWOffsetType prev_frame_offset_type;
_GLFWcursor* currentCursor;
double cursorPosX, cursorPosY, allCursorPosX, allCursorPosY;

10
glfw/wl_window.c vendored
View File

@@ -1588,6 +1588,14 @@ void _glfwPlatformSetWindowTitle(_GLFWwindow* window, const char* title)
void
_glfwPlatformSetWindowIcon(_GLFWwindow* window, int count, const GLFWimage* images) {
if (is_layer_shell(window)) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: Cannot set window icon on layer shell surfaces");
return;
}
if (!window->wl.xdg.toplevel) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: Ignoring attempt to set window icon on window without a toplevel");
return;
}
if (!_glfw.wl.xdg_toplevel_icon_manager_v1) {
static bool warned_once = false;
if (!warned_once) {
@@ -2577,7 +2585,7 @@ _glfwPlatformReadAvailableDropData(GLFWwindow *w, GLFWDropEvent *ev, char *buffe
for (size_t o = 0; o < offer->dd_count; o++) {
if (offer->requested_drop_data[o].fd == fd) {
ssize_t ret;
do { ret = read(fd, buffer, sz); } while (ret < 0 && errno == EINTR);
do { ret = read(fd, buffer, sz); } while (ret < 0 && (errno == EINTR || errno == EAGAIN));
if (ret <= 0) removeWatch(&_glfw.wl.eventLoopData, offer->requested_drop_data[o].watch_id);
return ret < 0 ? -errno : ret;
}

6
glfw/x11_init.c vendored
View File

@@ -216,8 +216,12 @@ read_xi_scroll_devices(void) {
if (_glfw.x11.xi.num_scroll_devices >= arraysz(_glfw.x11.xi.scroll_devices)) continue;
d = &_glfw.x11.xi.scroll_devices[_glfw.x11.xi.num_scroll_devices++];
*d = (XIScrollDevice){
.is_highres=is_highres, .is_finger_based=is_finger_based, .deviceid=device->deviceid, .sourceid=scroll->sourceid,
.is_finger_based=is_finger_based, .deviceid=device->deviceid, .sourceid=scroll->sourceid,
};
if (is_highres) {
d->type_detected = true;
d->offset_type = GLFW_SCROLL_OFFEST_HIGHRES;
}
memcpy(d->name, device->name, MIN(sizeof(d->name)-1, strlen(device->name)));
}
if (d->num_valuators >= arraysz(d->valuators)) continue;

4
glfw/x11_platform.h vendored
View File

@@ -239,12 +239,14 @@ typedef struct XIScrollValuator {
} XIScrollValuator;
typedef struct XIScrollDevice {
bool is_highres;
bool is_finger_based;
bool type_detected;
int deviceid, sourceid;
XIScrollValuator valuators[8];
unsigned num_valuators;
char name[32];
unsigned num_events;
GLFWOffsetType offset_type;
} XIScrollDevice;
typedef struct XdndSelectionRequest {

38
glfw/x11_window.c vendored
View File

@@ -29,6 +29,7 @@
#define _GNU_SOURCE
#include "internal.h"
#include "math.h"
#include "backend_utils.h"
#include "linux_notify.h"
#include "../kitty/monotonic.h"
@@ -1414,6 +1415,11 @@ handle_mouse_move_event(_GLFWwindow *window, const int x, const int y) {
window->x11.lastCursorPosY = y;
}
static bool
number_has_fractional_part(double x) {
return fabs(x - round(x)) >= 1e-6;
}
static void
handle_xi_motion_event(_GLFWwindow *window, XIDeviceEvent *de) {
XIScrollDevice *d = NULL;
@@ -1441,17 +1447,35 @@ handle_xi_motion_event(_GLFWwindow *window, XIDeviceEvent *de) {
scroll_valuator_found = true;
double delta = value - v->value;
v->value = value;
if (v->is_vertical) delta *= -1;
delta *= -1;
double *off = v->is_vertical ? &yOffset : &xOffset;
*off = delta;
if (!d->is_highres) {
if (v->increment == 120.) type = GLFW_SCROLL_OFFEST_V120;
else {
type = GLFW_SCROLL_OFFSET_LINES;
if (v->increment != 0) *off /= v->increment;
d->num_events++;
if (!d->type_detected) {
if (v->increment == 120.) {
d->type_detected = true;
d->offset_type = GLFW_SCROLL_OFFEST_V120;
} else {
bool delta_is_fractional = number_has_fractional_part(delta);
if (delta_is_fractional) {
if (fabs(delta * 120 - round(delta * 120)) < 0.01) {
d->type_detected = d->num_events > 2;
d->offset_type = GLFW_SCROLL_OFFEST_V120;
} else {
d->type_detected = true;
d->offset_type = GLFW_SCROLL_OFFEST_HIGHRES;
}
} else {
d->type_detected = d->num_events > 2;
d->offset_type = GLFW_SCROLL_OFFSET_LINES;
}
}
}
if (d->offset_type == GLFW_SCROLL_OFFSET_LINES) {
if (v->increment != 0) *off /= v->increment;
}
}
type = d->offset_type;
if (xOffset != 0 || yOffset != 0) {
// Get keyboard modifiers
int mods = translateState(de->mods.effective);
@@ -1465,7 +1489,7 @@ handle_xi_motion_event(_GLFWwindow *window, XIDeviceEvent *de) {
};
// For high-resolution, finger-based scrolling, use timer-based momentum scrolling
if (d->is_highres && d->is_finger_based && type == GLFW_SCROLL_OFFEST_HIGHRES) {
if (d->is_finger_based && type == GLFW_SCROLL_OFFEST_HIGHRES) {
// Reset the timer on each scroll event
x11_cancel_momentum_scroll_timer();

2
go.mod
View File

@@ -24,7 +24,7 @@ require (
github.com/zeebo/xxh3 v1.1.0
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b
golang.org/x/image v0.36.0
golang.org/x/sys v0.41.0
golang.org/x/sys v0.42.0
golang.org/x/text v0.34.0
howett.net/plist v1.0.1
)

4
go.sum
View File

@@ -74,8 +74,8 @@ golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -790,7 +790,7 @@ func (h *Handler) triggerSelected() {
func main(cmd *cli.Command, opts *Options, args []string) (rc int, err error) {
if tty.IsTerminal(os.Stdin.Fd()) {
return 1, fmt.Errorf("This kitten must only be run via a mapping in kitty.conf")
return 1, fmt.Errorf("This kitten must only be run via the command_palette action mapped to a shortcut in kitty.conf")
}
output := tui.KittenOutputSerializer()
lp, err := loop.New()

View File

@@ -65,6 +65,10 @@ Can be specified multiple times to use multiple patterns. For example::
''',
)
opt('mark_moved_lines', 'yes', option_type='to_bool', long_text='''
Highlight lines that are moved, that is removed from the left and added to the right
differently, using the :opt:`moved_bg` color.''')
opt('word_diff_mode', 'words', choices=('words', 'central'),
long_text='''
The algorithm to use for highlighting which parts of changed lines differ.
@@ -152,6 +156,12 @@ opt('dark_highlight_added_bg', '#31503d', option_type='to_color')
opt('added_margin_bg', '#cdffd8', option_type='to_color')
opt('dark_added_margin_bg', '#31503d', option_type='to_color')
opt('moved_bg', '#fffde7', option_type='to_color', long_text='Moved text backgrounds (same text that was removed in one place and added in another)')
opt('dark_moved_bg', '#003333', option_type='to_color')
opt('moved_margin_bg', '#fff3b0', option_type='to_color')
opt('dark_moved_margin_bg', '#00495b', option_type='to_color')
opt('filler_bg', '#fafbfc', option_type='to_color', long_text='Filler (empty) line background')
opt('dark_filler_bg', '#262c36', option_type='to_color')

View File

@@ -14,6 +14,7 @@ import (
"sync"
parallel "github.com/kovidgoyal/go-parallel"
"github.com/kovidgoyal/kitty/tools/simdstring"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/images"
"github.com/kovidgoyal/kitty/tools/utils/shlex"
@@ -336,6 +337,7 @@ func (self *Hunk) finalize(left_lines, right_lines []string) error {
type Patch struct {
all_hunks []*Hunk
largest_line_number, added_count, removed_count int
left_moved_lines, right_moved_lines *utils.Set[int]
}
func (self *Patch) Len() int { return len(self.all_hunks) }
@@ -451,6 +453,54 @@ func (self *Patch) compute_centers(left_lines, right_lines []string) error {
return nil
}
// Use SIMD to efficiently find non-blank lines: a line is non-blank if it
// contains at least one character that is not a space or tab.
func is_non_blank(text string) bool {
return simdstring.NotIndexByte2String(text, ' ', '\t') >= 0
}
func (self *Patch) detect_moved_lines(left_lines, right_lines []string) {
// Build maps from line text to lists of line numbers for removed and added lines.
removed := make(map[string][]int, len(left_lines)) // text -> left line numbers
added := make(map[string][]int, len(right_lines)) // text -> right line numbers
for _, hunk := range self.all_hunks {
for _, chunk := range hunk.chunks {
if !chunk.is_context {
for i := 0; i < chunk.left_count; i++ {
lnum := chunk.left_start + i
text := left_lines[lnum]
if is_non_blank(text) {
removed[text] = append(removed[text], lnum)
}
}
for i := 0; i < chunk.right_count; i++ {
rnum := chunk.right_start + i
text := right_lines[rnum]
if is_non_blank(text) {
added[text] = append(added[text], rnum)
}
}
}
}
}
// Lines that appear in both removed and added sets are moved lines. When a
// line appears multiple times on each side, only min(left_count,
// right_count) occurrences are marked as moved.
self.left_moved_lines = utils.NewSet[int]()
self.right_moved_lines = utils.NewSet[int]()
for text, lnums := range removed {
if rnums, ok := added[text]; ok {
count := min(len(lnums), len(rnums))
for _, lnum := range lnums[:count] {
self.left_moved_lines.Add(lnum)
}
for _, rnum := range rnums[:count] {
self.right_moved_lines.Add(rnum)
}
}
}
}
func parse_patch(raw string, left_lines, right_lines []string) (ans *Patch, err error) {
ans = &Patch{all_hunks: make([]*Hunk, 0, 32)}
var current_hunk *Hunk
@@ -486,6 +536,9 @@ func parse_patch(raw string, left_lines, right_lines []string) (ans *Patch, err
ans.largest_line_number = ans.all_hunks[len(ans.all_hunks)-1].largest_line_number
}
err = ans.compute_centers(left_lines, right_lines)
if err == nil && conf.Mark_moved_lines {
ans.detect_moved_lines(left_lines, right_lines)
}
return
}

View File

@@ -40,6 +40,7 @@ type HalfScreenLine struct {
marked_up_margin_text string
marked_up_text string
is_filler bool
is_moved bool
cached_wcswidth int
}
@@ -81,6 +82,9 @@ func (self *LogicalLine) render_screen_line(n int, lp *loop.Loop, margin_size, c
if sl.left.is_filler {
left_margin = format_as_sgr.margin_filler + left_margin
left_text = format_as_sgr.filler + left_text
} else if sl.left.is_moved {
left_margin = format_as_sgr.moved_margin + left_margin
left_text = format_as_sgr.moved + left_text
} else {
switch self.line_type {
case CHANGE_LINE, IMAGE_LINE:
@@ -104,6 +108,9 @@ func (self *LogicalLine) render_screen_line(n int, lp *loop.Loop, margin_size, c
if sl.right.is_filler {
right_margin = format_as_sgr.margin_filler + right_margin
right_text = format_as_sgr.filler + right_text
} else if sl.right.is_moved {
right_margin = format_as_sgr.moved_margin + right_margin
right_text = format_as_sgr.moved + right_text
} else {
switch self.line_type {
case CHANGE_LINE, IMAGE_LINE:
@@ -156,7 +163,7 @@ func place_in(text string, sz int) string {
}
var format_as_sgr struct {
title, margin, added, removed, added_margin, removed_margin, filler, margin_filler, hunk_margin, hunk, selection, search string
title, margin, added, removed, added_margin, removed_margin, filler, margin_filler, hunk_margin, hunk, selection, search, moved, moved_margin string
}
var statusline_format, added_count_format, removed_count_format, message_format func(...any) string
@@ -175,6 +182,8 @@ type ResolvedColors struct {
Margin_bg style.RGBA
Margin_fg style.RGBA
Margin_filler_bg style.NullableColor
Moved_bg style.RGBA
Moved_margin_bg style.RGBA
Removed_bg style.RGBA
Removed_margin_bg style.RGBA
Search_bg style.RGBA
@@ -202,6 +211,8 @@ func create_formatters() {
rc.Margin_bg = conf.Dark_margin_bg
rc.Margin_fg = conf.Dark_margin_fg
rc.Margin_filler_bg = conf.Dark_margin_filler_bg
rc.Moved_bg = conf.Dark_moved_bg
rc.Moved_margin_bg = conf.Dark_moved_margin_bg
rc.Removed_bg = conf.Dark_removed_bg
rc.Removed_margin_bg = conf.Dark_removed_margin_bg
rc.Search_bg = conf.Dark_search_bg
@@ -223,6 +234,8 @@ func create_formatters() {
rc.Margin_bg = conf.Margin_bg
rc.Margin_fg = conf.Margin_fg
rc.Margin_filler_bg = conf.Margin_filler_bg
rc.Moved_bg = conf.Moved_bg
rc.Moved_margin_bg = conf.Moved_margin_bg
rc.Removed_bg = conf.Removed_bg
rc.Removed_margin_bg = conf.Removed_margin_bg
rc.Search_bg = conf.Search_bg
@@ -248,6 +261,8 @@ func create_formatters() {
format_as_sgr.added_margin = only_open(fmt.Sprintf("fg=%s bg=%s", rc.Margin_fg.AsRGBSharp(), rc.Added_margin_bg.AsRGBSharp()))
format_as_sgr.removed = only_open("bg=" + rc.Removed_bg.AsRGBSharp())
format_as_sgr.removed_margin = only_open(fmt.Sprintf("fg=%s bg=%s", rc.Margin_fg.AsRGBSharp(), rc.Removed_margin_bg.AsRGBSharp()))
format_as_sgr.moved = only_open("bg=" + rc.Moved_bg.AsRGBSharp())
format_as_sgr.moved_margin = only_open(fmt.Sprintf("fg=%s bg=%s", rc.Margin_fg.AsRGBSharp(), rc.Moved_margin_bg.AsRGBSharp()))
format_as_sgr.title = only_open(fmt.Sprintf("fg=%s bg=%s bold", rc.Title_fg.AsRGBSharp(), rc.Title_bg.AsRGBSharp()))
format_as_sgr.margin = only_open(fmt.Sprintf("fg=%s bg=%s", rc.Margin_fg.AsRGBSharp(), rc.Margin_bg.AsRGBSharp()))
format_as_sgr.hunk = only_open(fmt.Sprintf("fg=%s bg=%s", rc.Margin_fg.AsRGBSharp(), rc.Hunk_bg.AsRGBSharp()))
@@ -533,6 +548,8 @@ type DiffData struct {
available_cols, margin_size int
left_lines, right_lines []string
left_moved_lines *utils.Set[int]
right_moved_lines *utils.Set[int]
}
func hunk_title(hunk *Hunk) string {
@@ -567,7 +584,7 @@ func splitlines(text string, width int) []string {
return style.WrapTextAsLines(text, width, style.WrapOptions{})
}
func render_half_line(line_number int, line, ltype string, available_cols int, center Center, ans []HalfScreenLine) []HalfScreenLine {
func render_half_line(line_number int, line, ltype string, available_cols int, center Center, is_moved bool, ans []HalfScreenLine) []HalfScreenLine {
var regions []Region
if ltype == "remove" {
regions = center.left_regions
@@ -583,7 +600,7 @@ func render_half_line(line_number int, line, ltype string, available_cols int, c
}
lnum := strconv.Itoa(line_number + 1)
for _, sc := range splitlines(line, available_cols) {
ans = append(ans, HalfScreenLine{marked_up_margin_text: lnum, marked_up_text: sc})
ans = append(ans, HalfScreenLine{marked_up_margin_text: lnum, marked_up_text: sc, is_moved: is_moved})
lnum = ""
}
return ans
@@ -601,13 +618,15 @@ func lines_for_diff_chunk(data *DiffData, _ int, chunk *Chunk, _ int, ans []*Log
}
if i < chunk.left_count {
left_lnum = chunk.left_start + i
ll = render_half_line(left_lnum, data.left_lines[left_lnum], "remove", data.available_cols, center, ll)
left_is_moved := data.left_moved_lines != nil && data.left_moved_lines.Has(left_lnum)
ll = render_half_line(left_lnum, data.left_lines[left_lnum], "remove", data.available_cols, center, left_is_moved, ll)
left_lnum++
}
if i < chunk.right_count {
right_lnum = chunk.right_start + i
rl = render_half_line(right_lnum, data.right_lines[right_lnum], "add", data.available_cols, center, rl)
right_is_moved := data.right_moved_lines != nil && data.right_moved_lines.Has(right_lnum)
rl = render_half_line(right_lnum, data.right_lines[right_lnum], "add", data.available_cols, center, right_is_moved, rl)
right_lnum++
}
@@ -663,7 +682,10 @@ func lines_for_diff(left_path string, right_path string, patch *Patch, columns,
return append(ans, &ht), nil
}
available_cols := columns/2 - margin_size
data := DiffData{left_path: left_path, right_path: right_path, available_cols: available_cols, margin_size: margin_size}
data := DiffData{
left_path: left_path, right_path: right_path, available_cols: available_cols, margin_size: margin_size,
left_moved_lines: patch.left_moved_lines, right_moved_lines: patch.right_moved_lines,
}
if left_path != "" {
data.left_lines, err = highlighted_lines_for_path(left_path)
if err != nil {
@@ -720,7 +742,7 @@ func all_lines(path string, columns, margin_size int, is_add bool, ans []*Logica
}
for line_number, line := range lines {
hlines := make([]HalfScreenLine, 0, 8)
hlines = render_half_line(line_number, line, ltype, available_cols, Center{}, hlines)
hlines = render_half_line(line_number, line, ltype, available_cols, Center{}, false, hlines)
l := ll
if is_add {
l.right_reference.linenum = line_number + 1

View File

@@ -67,3 +67,4 @@ void get_cocoa_key_equivalent(uint32_t, int, char *key, size_t key_sz, int*);
void set_cocoa_pending_action(CocoaPendingAction action, const char*);
void cocoa_report_live_notifications(const char* ident);
void cocoa_set_dock_badge(const char *label);
void cocoa_clear_dock_badge_if_set(void);

View File

@@ -1341,15 +1341,23 @@ cocoa_show_progress_bar_on_dock_icon(PyObject *self UNUSED, PyObject *args) {
// Dock badge {{{
static bool dock_badge_is_set = false;
void
cocoa_set_dock_badge(const char *label) {
@autoreleasepool {
NSDockTile *dockTile = [NSApp dockTile];
[dockTile setBadgeLabel:label ? @(label) : nil];
[dockTile display];
dock_badge_is_set = (label != NULL);
}
}
void
cocoa_clear_dock_badge_if_set(void) {
if (dock_badge_is_set) cocoa_set_dock_badge(NULL);
}
// }}}
static PyMethodDef module_methods[] = {

View File

@@ -22,7 +22,7 @@ class Version(NamedTuple):
appname: str = 'kitty'
kitty_face = '🐱'
version: Version = Version(0, 46, 0)
version: Version = Version(0, 46, 1)
str_version: str = '.'.join(map(str, version))
_plat = sys.platform.lower()
is_macos: bool = 'darwin' in _plat

View File

@@ -1,7 +1,6 @@
#!/usr/bin/env python
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import ctypes
import os
import sys
from collections.abc import Callable, Generator
@@ -218,6 +217,7 @@ def set_font_family(opts: Options | None = None, override_font_size: float | Non
if TYPE_CHECKING:
import ctypes
CBufType = ctypes.Array[ctypes.c_ubyte]
else:
CBufType = None

View File

@@ -518,6 +518,9 @@ update_modifier_state_on_modifier_key_event(GLFWkeyevent *ev, int key_modifier,
static void
key_callback(GLFWwindow *w, GLFWkeyevent *ev) {
if (!set_callback_window(w)) return;
#ifdef __APPLE__
cocoa_clear_dock_badge_if_set();
#endif
#ifndef __APPLE__
bool is_left;
int key_modifier = key_to_modifier(ev->key, &is_left);
@@ -554,6 +557,9 @@ cursor_enter_callback(GLFWwindow *w, int entered) {
static void
mouse_button_callback(GLFWwindow *w, int button, int action, int mods) {
if (!set_callback_window(w)) return;
#ifdef __APPLE__
cocoa_clear_dock_badge_if_set();
#endif
monotonic_t now = monotonic();
cursor_active_callback(now);
mods_at_last_key_or_button_event = mods;
@@ -676,6 +682,23 @@ is_droppable_mime(const char *mime) {
return 0;
}
static size_t
remove_duplicate_mimes(const char **mimes, size_t count) {
// Use simple O(n²) scan since lists are typically small
size_t new_count = 0;
for (size_t i = 0; i < count; i++) {
bool is_duplicate = false;
for (size_t j = 0; j < new_count; j++) {
if (strcmp(mimes[i], mimes[j]) == 0) { is_duplicate = true; break; }
}
if (!is_duplicate) {
if (new_count != i) SWAP(mimes[i], mimes[new_count]);
new_count++;
}
}
return new_count;
}
static void
update_allowed_mimes_for_drop(GLFWDropEvent *ev) {
if (ev->mimes && ev->num_mimes) {
@@ -728,6 +751,7 @@ read_drop_data(GLFWwindow *window, GLFWDropEvent *ev) {
PyObject *data = chunk;
RAII_PyObject(existing, PyDict_GetItemString(global_state.drop_dest.data, ev->mimes[0]));
if (existing) {
existing = Py_NewRef(existing); // because PyBytes_Concat steals a reference
PyBytes_Concat(&existing, chunk);
data = existing;
}
@@ -775,6 +799,7 @@ on_drop(GLFWwindow *window, GLFWDropEvent *ev) {
break;
}
update_allowed_mimes_for_drop(ev);
ev->num_mimes = remove_duplicate_mimes(ev->mimes, ev->num_mimes);
global_state.drop_dest.num_left = ev->num_mimes;
if (!global_state.drop_dest.num_left || !(global_state.drop_dest.data = PyDict_New())) {
ev->finish_drop(window, GLFW_DRAG_OPERATION_GENERIC);
@@ -1064,6 +1089,7 @@ set_os_window_icon(PyObject UNUSED *self, PyObject *args) {
if(!PyArg_ParseTuple(args, "K|O", &id, &what)) return NULL;
OSWindow *os_window = os_window_for_id(id);
if (!os_window) { PyErr_Format(PyExc_KeyError, "No OS Window with id: %llu", id); return NULL; }
if (os_window->is_layer_shell && global_state.is_wayland) Py_RETURN_NONE;
if (!what || what == Py_None) {
glfwSetWindowIcon(os_window->handle, 0, NULL);
Py_RETURN_NONE;
@@ -1704,7 +1730,7 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) {
glfwCocoaSetWindowResizeCallback(glfw_window, cocoa_os_window_resized);
#endif
send_prerendered_sprites_for_window(w);
if (logo.pixels && logo.width && logo.height) glfwSetWindowIcon(glfw_window, 1, &logo);
if (logo.pixels && logo.width && logo.height && (!lsc || !global_state.is_wayland)) glfwSetWindowIcon(glfw_window, 1, &logo);
set_glfw_mouse_pointer_shape_in_window(glfw_window, OPT(default_pointer_shape));
update_os_window_viewport(w, false);
glfwSetWindowPosCallback(glfw_window, window_pos_callback);

View File

@@ -1388,7 +1388,7 @@ pixel_scroll_enabled_for_screen(const Screen *screen) {
void
scroll_event(const GLFWScrollEvent *ev) {
debug("\x1b[36mScroll\x1b[m %s x: %f y: %f momentum: %s modifiers: %s\n", scroll_offset_type(ev->offset_type), ev->x_offset, ev->y_offset, scroll_phase(ev->momentum_type), format_mods(ev->keyboard_modifiers));
debug("\x1b[36mScroll\x1b[m type=%s x: %f y: %f momentum: %s modifiers: %s\n", scroll_offset_type(ev->offset_type), ev->x_offset, ev->y_offset, scroll_phase(ev->momentum_type), format_mods(ev->keyboard_modifiers));
static id_type window_for_momentum_scroll = 0;
static bool main_screen_for_momentum_scroll = false;
// allow scroll events even if window is not currently focused (in
@@ -1461,14 +1461,14 @@ scroll_event(const GLFWScrollEvent *ev) {
write_escape_code_to_child(screen, ESC_CSI, mouse_event_buf);
}
}
} else {
if (screen->linebuf == screen->main_linebuf) {
screen_history_scroll(screen, abs(s), upwards);
if (screen->selections.in_progress) update_drag(w);
} else {
if (screen->linebuf == screen->main_linebuf) {
screen_history_scroll(screen, abs(s), upwards);
if (screen->selections.in_progress) update_drag(w);
}
else fake_scroll(w, abs(s), upwards);
}
else fake_scroll(w, abs(s), upwards);
}
}
}
}
if (ev->x_offset != 0.0) {

View File

@@ -1795,7 +1795,9 @@ opt('tab_bar_margin_color', 'none',
long_text='''
Color for the tab bar margin area. Defaults to using the terminal background
color for margins above and below the tab bar. For side margins the default
color is chosen to match the background color of the neighboring tab.
color is chosen to match the background color of the neighboring tab, unless
the window is translucent, in which case the default background is used as it
looks better.
''')
opt(

View File

@@ -19,6 +19,7 @@ from .fast_data_types import (
Color,
Region,
Screen,
background_opacity_of,
cell_size_for_window,
get_boss,
get_options,
@@ -687,7 +688,7 @@ class TabBar:
if opts.tab_bar_margin_height.outer:
blank_rects.append(Border(0, tab_bar.bottom, vw, vh, bg))
if opts.tab_bar_margin_height.inner:
blank_rects.append(Border(0, central.bottom, vw, vh, bg))
blank_rects.append(Border(0, central.bottom, vw, tab_bar.top, bg))
else: # top
if opts.tab_bar_margin_height.outer:
blank_rects.append(Border(0, 0, vw, tab_bar.top, bg))
@@ -695,13 +696,14 @@ class TabBar:
blank_rects.append(Border(0, tab_bar.bottom, vw, central.top, bg))
g = self.window_geometry
left_bg = right_bg = bg
if opts.tab_bar_margin_color is None or opts.tab_bar_margin_width == 0:
if opts.tab_bar_margin_color is None and (
opacity := background_opacity_of(self.os_window_id)) is not None and opacity >= 1:
left_bg = BorderColor.tab_bar_left_edge_color
right_bg = BorderColor.tab_bar_right_edge_color
if g.left > 0:
blank_rects.append(Border(0, g.top, g.left, g.bottom, left_bg))
if g.right - 1 < vw:
blank_rects.append(Border(g.right - 1, g.top, vw, g.bottom, right_bg))
if g.right < vw:
blank_rects.append(Border(g.right, g.top, vw, g.bottom, right_bg))
self.blank_rects = tuple(blank_rects)
def layout(self) -> None:

View File

@@ -1251,7 +1251,7 @@ class Window:
try:
parts = tuple(map(int, raw_data.split(';')))[1:]
except Exception:
log_error(f'Ignoring malmormed OSC 9;4 progress report: {raw_data!r}')
log_error(f'Ignoring malformed OSC 9;4 progress report: {raw_data!r}')
return
self.progress.update(*parts[:2])
if (tab := self.tabref()) is not None:
@@ -2018,7 +2018,7 @@ class Window:
def handle_dangerous_paste_confirmation(self, unsanitized: bytes, sanitized: bytes, choice: str) -> None:
if choice == 's':
self.paste_text(sanitized)
elif choice == 'p':
elif choice == 'a':
self.paste_text(unsanitized)
def handle_large_paste_confirmation(self, btext: bytes, confirmed: bool) -> None:

View File

@@ -2,10 +2,14 @@
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
import json
import os
import shutil
import stat
import subprocess
import sys
import tempfile
import textwrap
import unittest
from functools import partial
@@ -35,6 +39,103 @@ class TestBuild(BaseTest):
for name in 'cell border bgimage tint graphics'.split():
Program(name)
def test_macos_dictation_forwarding(self) -> None:
from kitty.constants import glfw_path, is_macos
if not is_macos or not shutil.which('clang'):
self.skipTest('Dictation smoke test is macOS only and requires clang')
cocoa_module = glfw_path('cocoa')
probe = textwrap.dedent('''\
#import <AppKit/AppKit.h>
#import <dlfcn.h>
#import <objc/runtime.h>
#import <objc/message.h>
static int start_calls = 0;
static int stop_calls = 0;
static id last_sender = nil;
static void fake_start_dictation(id self, SEL _cmd, id sender) {
(void)self; (void)_cmd;
start_calls++;
last_sender = sender;
}
static void fake_stop_dictation(id self, SEL _cmd, id sender) {
(void)self; (void)_cmd;
stop_calls++;
last_sender = sender;
}
static void require_true(BOOL condition, const char *message) {
if (!condition) {
fprintf(stderr, "FAIL: %s\\n", message);
exit(1);
}
}
int main(void) {
@autoreleasepool {
[NSApplication sharedApplication];
void *handle = dlopen(@@COCOA_MODULE@@, RTLD_NOW | RTLD_GLOBAL);
require_true(handle != NULL, dlerror());
SEL start = NSSelectorFromString(@"startDictation:");
SEL stop = NSSelectorFromString(@"stopDictation:");
Method start_method = class_getInstanceMethod([NSApplication class], start);
Method stop_method = class_getInstanceMethod([NSApplication class], stop);
require_true(start_method != NULL, "NSApplication startDictation: missing");
require_true(stop_method != NULL, "NSApplication stopDictation: missing");
method_setImplementation(start_method, (IMP)fake_start_dictation);
method_setImplementation(stop_method, (IMP)fake_stop_dictation);
Class view_cls = NSClassFromString(@"GLFWContentView");
Class context_cls = NSClassFromString(@"GLFWTextInputContext");
require_true(view_cls != Nil, "GLFWContentView class not loaded");
require_true(context_cls != Nil, "GLFWTextInputContext class not loaded");
SEL init_with_glfw_window = NSSelectorFromString(@"initWithGlfwWindow:");
id view = ((id (*)(id, SEL, void *)) objc_msgSend)([view_cls alloc], init_with_glfw_window, NULL);
require_true(view != nil, "GLFWContentView initWithGlfwWindow: failed");
require_true([view respondsToSelector:start], "GLFWContentView does not expose startDictation:");
require_true([view respondsToSelector:stop], "GLFWContentView does not expose stopDictation:");
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[view performSelector:start withObject:@"menu sender"];
#pragma clang diagnostic pop
require_true(start_calls == 1, "startDictation: action was not forwarded to NSApplication");
require_true([(id)last_sender isEqual:@"menu sender"], "startDictation: forwarded wrong sender");
[view doCommandBySelector:start];
require_true(start_calls == 2, "doCommandBySelector:startDictation: was swallowed");
require_true(last_sender == view, "doCommandBySelector:startDictation: should forward self as sender");
id context = [view inputContext];
require_true(context != nil, "GLFWContentView inputContext missing");
require_true([context isKindOfClass:context_cls], "GLFWContentView inputContext has wrong class");
[context doCommandBySelector:stop];
require_true(stop_calls == 1, "GLFWTextInputContext did not forward stopDictation:");
require_true(last_sender == nil, "GLFWTextInputContext should forward nil sender");
printf("dictation forwarding probe passed\\n");
}
return 0;
}
''').replace('@@COCOA_MODULE@@', json.dumps(cocoa_module))
with tempfile.TemporaryDirectory() as tdir:
src = os.path.join(tdir, 'dictation_probe.m')
exe = os.path.join(tdir, 'dictation_probe')
with open(src, 'w') as f:
f.write(probe)
cp = subprocess.run(
['clang', '-framework', 'AppKit', src, '-o', exe],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
)
self.assertEqual(cp.returncode, 0, cp.stdout)
cp = subprocess.run([exe], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
self.assertEqual(cp.returncode, 0, cp.stdout)
self.assertIn('dictation forwarding probe passed', cp.stdout)
def test_glfw_modules(self) -> None:
from kitty.constants import glfw_path, is_macos
linux_backends = ['x11']

View File

@@ -949,7 +949,7 @@ def add_builtin_fonts(args: Options) -> None:
break
else:
lines = subprocess.check_output([
'fc-match', '--format', '%{file}\n%{postscriptname}', f'term:postscriptname={psname}', 'file', 'postscriptname']).decode().splitlines()
'fc-list', '--format', '%{file}\n%{postscriptname}', f':postscriptname={psname}']).decode().splitlines()
if len(lines) != 2:
raise SystemExit(f'fc-match returned unexpected output: {lines}')
if lines[1] != psname:
@@ -1286,11 +1286,15 @@ def build_static_kittens(
cmd = go + ['build', '-v']
vcs_rev = args.vcs_rev or get_vcs_rev()
ld_flags: List[str] = []
binary_data_flags = [f"-X kitty.VCSRevision={vcs_rev}"]
with open('go.mod') as f:
m = re.search(r'^module\s+(\S+)', f.read(), flags=re.M)
assert m is not None
modpath = m.group(1).strip()
binary_data_flags = [f"-X {modpath}.VCSRevision={vcs_rev}"]
if for_freeze:
binary_data_flags.append("-X kitty.IsFrozenBuild=true")
binary_data_flags.append(f"-X {modpath}.IsFrozenBuild=true")
if for_platform:
binary_data_flags.append("-X kitty.IsStandaloneBuild=true")
binary_data_flags.append(f"-X {modpath}.IsStandaloneBuild=true")
if not args.debug:
ld_flags.append('-s')
ld_flags.append('-w')

View File

@@ -64,3 +64,45 @@ func BenchmarkIndexByte2(b *testing.B) {
t(pos, "scalar")
}
}
func BenchmarkNotIndexByte(b *testing.B) {
t := func(pos int, which string) {
// Fill with 'a' and place 'q' (a non-matching byte) at the target position
data := haystack('a', 'q', pos)
f := NotIndexByte
switch which {
case "scalar":
f = not_index_byte_scalar
}
b.Run(fmt.Sprintf("%s_sz=%d", which, pos), func(b *testing.B) {
for b.Loop() {
f(data, 'a')
}
})
}
for _, pos := range sizes {
t(pos, "simdstring")
t(pos, "scalar")
}
}
func BenchmarkNotIndexByte2(b *testing.B) {
t := func(pos int, which string) {
// Fill with 'a' and place 'q' (neither 'a' nor 'x') at the target position
data := haystack('a', 'q', pos)
f := NotIndexByte2
switch which {
case "scalar":
f = not_index_byte2_scalar
}
b.Run(fmt.Sprintf("%s_sz=%d", which, pos), func(b *testing.B) {
for b.Loop() {
f(data, 'a', 'x')
}
})
}
for _, pos := range sizes {
t(pos, "simdstring")
t(pos, "scalar")
}
}

View File

@@ -404,6 +404,12 @@ func encode_cmgt16b(a, b, dest Register) (ans uint32) {
return 0x271<<21 | b.ARMId()<<16 | 0xd<<10 | a.ARMId()<<5 | dest.ARMId()
}
func encode_not16b(src, dest Register) uint32 {
// NOT Vd.16B, Vn.16B (alias of MVN)
// Encoding: 0 Q 1 01110 size 10000 00101 10 Rn Rd (Q=1, size=00 for .16B)
return 0x6E205800 | (src.ARMId() << 5) | dest.ARMId()
}
func (f *Function) MaskForCountDestructive(vec, ans Register) {
// vec is clobbered by this function
f.Comment("Count the number of bytes to the first 0xff byte and put the result in", ans)
@@ -688,6 +694,24 @@ func (f *Function) Or(a, b, dest Register) {
f.AddTrailingComment(dest, "=", a, "|", b, "(bitwise)")
}
func (f *Function) NotSelf(r Register) {
if f.ISA.Goarch == ARM64 {
f.Comment("Go assembler doesn't support the VMVN instruction, below we have: NOT", r.ARMFullWidth()+",", r.ARMFullWidth())
f.instr("WORD", fmt.Sprintf("$0x%x", encode_not16b(r, r)))
f.AddTrailingComment(r, "= ~", r, "(bitwise NOT)")
return
}
all_ones := f.Vec(r.Size)
defer f.ReleaseReg(all_ones)
f.AllOnesRegister(all_ones)
if r.Size == 128 {
f.instr("PXOR", all_ones, r)
} else {
f.instr("VPXOR", all_ones, r, r)
}
f.AddTrailingComment(r, "= ~", r, "(bitwise NOT)")
}
func (f *Function) And(a, b, dest Register) {
if f.ISA.Goarch == ARM64 {
f.instr("VAND", a.ARMFullWidth(), b.ARMFullWidth(), dest.ARMFullWidth())
@@ -1504,6 +1528,54 @@ func (s *State) indexc0() {
}
func (s *State) not_index_byte_body(f *Function) {
b := f.Vec()
f.Set1Epi8("b", b)
test_bytes := func(bytes_to_test, test_ans Register) {
f.CmpEqEpi8(bytes_to_test, b, test_ans)
f.NotSelf(test_ans)
}
s.index_func(f, test_bytes)
}
func (s *State) not_index_byte() {
f := s.NewFunction("not_index_byte_asm", "Find the index of the first byte that is not b", []FunctionParam{{"data", ByteSlice}, {"b", types.Byte}}, []FunctionParam{{"ans", types.Int}})
if s.ISA.HasSIMD {
s.not_index_byte_body(f)
}
f = s.NewFunction("not_index_byte_string_asm", "Find the index of the first byte that is not b", []FunctionParam{{"data", types.String}, {"b", types.Byte}}, []FunctionParam{{"ans", types.Int}})
if s.ISA.HasSIMD {
s.not_index_byte_body(f)
}
}
func (s *State) not_index_byte2_body(f *Function) {
b1 := f.Vec()
b2 := f.Vec()
f.Set1Epi8("b1", b1)
f.Set1Epi8("b2", b2)
test_bytes := func(bytes_to_test, test_ans Register) {
f.CmpEqEpi8(bytes_to_test, b1, test_ans)
f.CmpEqEpi8(bytes_to_test, b2, bytes_to_test)
f.Or(test_ans, bytes_to_test, test_ans)
f.NotSelf(test_ans)
}
s.index_func(f, test_bytes)
}
func (s *State) not_index_byte2() {
f := s.NewFunction("not_index_byte2_asm", "Find the index of the first byte that is neither b1 nor b2", []FunctionParam{{"data", ByteSlice}, {"b1", types.Byte}, {"b2", types.Byte}}, []FunctionParam{{"ans", types.Int}})
if s.ISA.HasSIMD {
s.not_index_byte2_body(f)
}
f = s.NewFunction("not_index_byte2_string_asm", "Find the index of the first byte that is neither b1 nor b2", []FunctionParam{{"data", types.String}, {"b1", types.Byte}, {"b2", types.Byte}}, []FunctionParam{{"ans", types.Int}})
if s.ISA.HasSIMD {
s.not_index_byte2_body(f)
}
}
func (s *State) Generate() {
s.test_load()
s.test_set1_epi8()
@@ -1516,6 +1588,8 @@ func (s *State) Generate() {
s.indexbyte2()
s.indexc0()
s.indexbyte()
s.not_index_byte()
s.not_index_byte2()
s.OutputFunction()
}

View File

@@ -15,8 +15,7 @@ var VectorSize = 1
// Return the index at which b first occurs in data. If not found -1 is returned.
var IndexByte func(data []byte, b byte) int = index_byte_scalar
// Return the index at which either a or b first occurs in text. If neither is
// found -1 is returned.
// Return the index at which b first occurs in text. If not found -1 is returned.
var IndexByteString func(text string, b byte) int = index_byte_string_scalar
// Return the index at which either a or b first occurs in data. If neither is
@@ -33,6 +32,18 @@ var IndexC0 func(data []byte) int = index_c0_scalar
// Return the index at which the first C0 byte is found or -1 when no such bytes are present.
var IndexC0String func(data string) int = index_c0_string_scalar
// Return the index of the first byte in data that is not equal to b. If all bytes equal b, -1 is returned.
var NotIndexByte func(data []byte, b byte) int = not_index_byte_scalar
// Return the index of the first byte in text that is not equal to b. If all bytes equal b, -1 is returned.
var NotIndexByteString func(text string, b byte) int = not_index_byte_string_scalar
// Return the index of the first byte in data that is neither a nor b. If all bytes are a or b, -1 is returned.
var NotIndexByte2 func(data []byte, a, b byte) int = not_index_byte2_scalar
// Return the index of the first byte in text that is neither a nor b. If all bytes are a or b, -1 is returned.
var NotIndexByte2String func(text string, a, b byte) int = not_index_byte2_string_scalar
func init() {
switch runtime.GOARCH {
case "amd64":
@@ -51,6 +62,10 @@ func init() {
IndexByte2String = index_byte2_string_asm_256
IndexC0 = index_c0_asm_256
IndexC0String = index_c0_string_asm_256
NotIndexByte = not_index_byte_asm_256
NotIndexByteString = not_index_byte_string_asm_256
NotIndexByte2 = not_index_byte2_asm_256
NotIndexByte2String = not_index_byte2_string_asm_256
VectorSize = 32
} else if Have128bit {
IndexByte = index_byte_asm_128
@@ -59,6 +74,10 @@ func init() {
IndexByte2String = index_byte2_string_asm_128
IndexC0 = index_c0_asm_128
IndexC0String = index_c0_string_asm_128
NotIndexByte = not_index_byte_asm_128
NotIndexByteString = not_index_byte_string_asm_128
NotIndexByte2 = not_index_byte2_asm_128
NotIndexByte2String = not_index_byte2_string_asm_128
VectorSize = 16
}
}

View File

@@ -244,6 +244,65 @@ func TestSIMDStringOps(t *testing.T) {
index_test([]byte("abc"), 'x')
index_test([]byte("abc"), 'b')
not_index_test := func(haystack []byte, needle byte) {
var actual int
expected := not_index_byte_scalar(haystack, needle)
for _, sz := range sizes {
switch sz {
case 16:
actual = not_index_byte_asm_128(haystack, needle)
case 32:
actual = not_index_byte_asm_256(haystack, needle)
}
if actual != expected {
t.Fatalf("not_index failed in: %#v (%d != %d) at size: %d with needle: %#v", string(haystack), expected, actual, sz, needle)
}
}
}
not_index_test(nil, 'a')
not_index_test([]byte{}, 'a')
not_index_test([]byte("aaa"), 'a')
not_index_test([]byte("aaab"), 'a')
not_index_test([]byte("baaa"), 'a')
not_index_test([]byte("abc"), 'a')
for _, sz := range []int{0, 16, 32, 64, 79} {
q := strings.Repeat("a", sz) + "b"
not_index_test([]byte(q), 'a')
not_index_test([]byte(q), 'b')
not_index_test([]byte(strings.Repeat("a", sz)), 'a')
}
not_index2_test := func(haystack []byte, a, b byte) {
var actual int
expected := not_index_byte2_scalar(haystack, a, b)
for _, sz := range sizes {
switch sz {
case 16:
actual = not_index_byte2_asm_128(haystack, a, b)
case 32:
actual = not_index_byte2_asm_256(haystack, a, b)
}
if actual != expected {
t.Fatalf("not_index2 failed in: %#v (%d != %d) at size: %d with needles: %#v %#v", string(haystack), expected, actual, sz, a, b)
}
}
}
not_index2_test(nil, 'a', 'b')
not_index2_test([]byte{}, 'a', 'b')
not_index2_test([]byte("aabb"), 'a', 'b')
not_index2_test([]byte("aabbc"), 'a', 'b')
not_index2_test([]byte("caabb"), 'a', 'b')
for _, sz := range []int{0, 16, 32, 64, 79} {
q := strings.Repeat("ab", sz) + "c"
not_index2_test([]byte(q), 'a', 'b')
not_index2_test([]byte(strings.Repeat("ab", sz)), 'a', 'b')
for align := range 32 {
not_index2_test([]byte(strings.Repeat(" ", align)+q), 'a', 'b')
}
}
}
func TestIntrinsics(t *testing.T) {

View File

@@ -57,3 +57,39 @@ func index_c0_string_scalar(data string) int {
}
return -1
}
func not_index_byte_scalar(data []byte, b byte) int {
for i, ch := range data {
if ch != b {
return i
}
}
return -1
}
func not_index_byte_string_scalar(data string, b byte) int {
for i := 0; i < len(data); i++ {
if data[i] != b {
return i
}
}
return -1
}
func not_index_byte2_scalar(data []byte, a, b byte) int {
for i, ch := range data {
if ch != a && ch != b {
return i
}
}
return -1
}
func not_index_byte2_string_scalar(data string, a, b byte) int {
for i := 0; i < len(data); i++ {
if data[i] != a && data[i] != b {
return i
}
}
return -1
}