From cf33820973eb1de4c8de66a3b29a7df6abce5e7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E9=B8=BD?= <18717730+xiaobai050@users.noreply.github.com> Date: Mon, 14 Sep 2026 22:41:26 +0800 Subject: [PATCH 1/3] Custom shaders: Support tinting complete inactive windows --- docs/changelog.rst | 3 +++ docs/custom-shaders.rst | 23 ++++++++++++++++++ kitty/shaders.c | 8 +++++++ kitty/shaders/custom/focus-highlight.slang | 18 +++++++++++++-- kitty/shaders/custom/types.slang | 3 +++ kitty_tests/slang.py | 27 ++++++++++++++++++++++ 6 files changed, 80 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f19e5af15..20b28288f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -204,6 +204,9 @@ Detailed list of changes - Support for :doc:`/custom-shaders` for adding various graphical effects (:iss:`10344`) +- Custom shaders: Allow tinting inactive windows toward a configurable color, + preserving the tab bar and active window padding. + - Various throughput performance improvements for a 15-35% real world improvement depending on workload - Add :opt:`window_border_radius` for rounded window borders (:pull:`10421`) diff --git a/docs/custom-shaders.rst b/docs/custom-shaders.rst index 7670a66f6..8b82c1dae 100644 --- a/docs/custom-shaders.rst +++ b/docs/custom-shaders.rst @@ -79,6 +79,29 @@ mouse shader. In the first group we have the crt shader and its color has been customised. The second group defines the spotlight shader and specifies exactly when the spotlight animation should start and stop. +For example, to blend inactive windows toward gray instead of black, save the +following as :file:`shaders/dim-gray.pipeline` in your kitty config directory and +set ``custom_shaders dim-gray`` and ``inactive_text_alpha 1`` in :file:`kitty.conf`:: + + startgroup + var float HIGHLIGHT_INTENSITY = 0 + var float BORDER_WIDTH = 0 + var float INACTIVE_DIM = 0.74 + var float3 INACTIVE_DIM_COLOR = float3(0.2158605) + var bool DIM_CENTRAL_AREA_ONLY = true + var bool INCLUDE_WINDOW_PADDING = true + shaders focus-highlight + endgroup + +This uses the same shader as ``dim-inactive-windows``. ``INACTIVE_DIM`` is the +fraction of the original color retained: ``0.74`` blends in 26% of the target +color. ``INACTIVE_DIM_COLOR`` is in linear RGB; ``0.2158605`` corresponds to +sRGB ``#808080``. The default target is black, preserving the existing dimming +effect. The two optional flags preserve the tab bar and include the active +window's padding in its unchanged area. Explicit cell backgrounds and images +are blended together with text, while transparency and colors reported to +terminal applications are preserved. + Anatomy of a custom shader ---------------------------- diff --git a/kitty/shaders.c b/kitty/shaders.c index 78bf80b95..faafde04e 100644 --- a/kitty/shaders.c +++ b/kitty/shaders.c @@ -2433,6 +2433,7 @@ run_custom_end_shader(OSWindow *os_window, float sx, float sy, monotonic_t now) float mouse_pos[4]; float mouse_button_pressed[4]; float active_window_geometry[4]; + float active_window_padding[4]; float bell_window_geometry[4]; float central_area[4]; float cursor_trail_corners_x[4]; @@ -2475,6 +2476,13 @@ run_custom_end_shader(OSWindow *os_window, float sx, float sy, monotonic_t now) if (t->num_windows) { Window *w = t->windows + t->active_window; active_win_geom = w->render_data.geometry; + if (os_window->viewport_width && os_window->viewport_height) { + const float width = (float)os_window->viewport_width, height = (float)os_window->viewport_height; + d->active_window_padding[0] = (float)(w->padding.left + w->size_mismatch_padding.left) / width; + d->active_window_padding[1] = (float)(w->padding.bottom + w->size_mismatch_padding.bottom) / height; + d->active_window_padding[2] = (float)(w->padding.right + w->size_mismatch_padding.right) / width; + d->active_window_padding[3] = (float)(w->padding.top + w->size_mismatch_padding.top) / height; + } Screen *s = w->render_data.screen; if (s) { active_bg = colorprofile_to_color(s->color_profile, s->color_profile->overridden.default_bg, s->color_profile->configured.default_bg).rgb; diff --git a/kitty/shaders/custom/focus-highlight.slang b/kitty/shaders/custom/focus-highlight.slang index 651b9ec57..10a7f16f4 100644 --- a/kitty/shaders/custom/focus-highlight.slang +++ b/kitty/shaders/custom/focus-highlight.slang @@ -16,14 +16,28 @@ static const float HIGHLIGHT_INTENSITY = 0.30; static const float BORDER_WIDTH = 0.04; // Peak additive brightness multiplier at the window border edge. static const float BORDER_GLOW = 0.55; -// Dim factor applied to pixels outside the active window (1.0 = unchanged, 0.0 = black). +// Fraction of the original color retained outside the active window (1.0 = unchanged, 0.0 = blend target). static const float INACTIVE_DIM = 0.3; +// Blend target in linear RGB; black preserves the original dimming behavior. +static const float3 INACTIVE_DIM_COLOR = float3(0.0); +// Keep the tab bar outside the effect when requested by a pipeline. +static const bool DIM_CENTRAL_AREA_ONLY = false; +// Include the active window's padding in its unchanged/highlighted area. +static const bool INCLUDE_WINDOW_PADDING = false; public float4 fragment_main(float4 color, KittyTextures t, KittyCustomShaderData d) { + if (DIM_CENTRAL_AREA_ONLY) { + float4 area = d.central_area; + if (any(t.pos < area.xy) || any(t.pos > area.xy + area.zw)) return color; + } float4 g = d.active_window_geometry; // Pass through unchanged if no active window geometry is known. if (g.z <= 0.0 || g.w <= 0.0) return color; + if (INCLUDE_WINDOW_PADDING) { + g.xy -= d.active_window_padding.xy; + g.zw += d.active_window_padding.xy + d.active_window_padding.zw; + } float2 uv = t.pos; float2 winMin = g.xy; @@ -48,5 +62,5 @@ fragment_main(float4 color, KittyTextures t, KittyCustomShaderData d) { } // Slightly dim inactive areas to draw the eye to the focused window. float dim = lerp(1.0, INACTIVE_DIM, strength); - return float4(color.rgb * dim, color.a); + return float4(lerp(INACTIVE_DIM_COLOR, color.rgb, dim), color.a); } diff --git a/kitty/shaders/custom/types.slang b/kitty/shaders/custom/types.slang index 4b2a96ab1..d73d32859 100644 --- a/kitty/shaders/custom/types.slang +++ b/kitty/shaders/custom/types.slang @@ -22,6 +22,9 @@ public struct KittyCustomShaderData { // .xy = lower-left corner of the window, .zw = width and height. // All components are zero when no active window was rendered this frame. public float4 active_window_geometry; + // Effective padding of the active window, including cell-size compensation. + // .xy = left/bottom, .zw = right/top, in viewport UV units. + public float4 active_window_padding; // Geometry of the most recently belled window in UV co-ordinates (origin bottom-left). // .xy = lower-left corner of the window, .zw = width and height. // If the belled window is not currently visible in the active tab/layout, equals central_area. diff --git a/kitty_tests/slang.py b/kitty_tests/slang.py index 5d08875f9..1a9fcb26f 100644 --- a/kitty_tests/slang.py +++ b/kitty_tests/slang.py @@ -33,6 +33,33 @@ _SUPPORT_SHADER_NAMES = frozenset(('types', 'pipeline')) class TestSlang(BaseTest): + def test_inactive_window_tint_pipeline(self): + if not shutil.which(slangc()[0]): + self.skipTest(f'slangc ({slangc()[0]}) not found in PATH') + + pipeline = parse_pipeline_definition( + """ + startgroup + var float HIGHLIGHT_INTENSITY = 0 + var float BORDER_WIDTH = 0 + var float INACTIVE_DIM = 0.74 + var float3 INACTIVE_DIM_COLOR = float3(0.2158605) + var bool DIM_CENTRAL_AREA_ONLY = true + var bool INCLUDE_WINDOW_PADDING = true + shaders focus-highlight + endgroup + """.splitlines(), + 'dim-gray', + ) + with tempfile.TemporaryDirectory() as cache_dir: + clear_caches() + try: + vertex, fragment, _ = build_custom_shader_pipeline_glsl(pipeline, cache_dir=cache_dir) + finally: + clear_caches() + self.assertTrue(vertex) + self.assertTrue(fragment) + def test_slang_parser(self): def check(src: str, expected: SlangFile) -> None: actual = parse_slang_text(src) From 7b41e333990e42aa4ff1a078e8155a97de97d4fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E9=B8=BD?= <18717730+xiaobai050@users.noreply.github.com> Date: Mon, 14 Sep 2026 22:41:26 +0800 Subject: [PATCH 2/3] Custom shaders: Redraw static effects only on demand --- docs/changelog.rst | 3 ++- docs/custom-shaders.rst | 8 ++++++-- kitty/child-monitor.c | 7 ++++++- kitty/shaders.c | 2 +- kitty_tests/slang.py | 4 +++- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 20b28288f..85d91e2ce 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -205,7 +205,8 @@ Detailed list of changes - Support for :doc:`/custom-shaders` for adding various graphical effects (:iss:`10344`) - Custom shaders: Allow tinting inactive windows toward a configurable color, - preserving the tab bar and active window padding. + preserving the tab bar and active window padding. Support static effects + without periodic redraws. - Various throughput performance improvements for a 15-35% real world improvement depending on workload diff --git a/docs/custom-shaders.rst b/docs/custom-shaders.rst index 8b82c1dae..9a2ee0b3b 100644 --- a/docs/custom-shaders.rst +++ b/docs/custom-shaders.rst @@ -84,6 +84,7 @@ following as :file:`shaders/dim-gray.pipeline` in your kitty config directory an set ``custom_shaders dim-gray`` and ``inactive_text_alpha 1`` in :file:`kitty.conf`:: startgroup + animation_step 0 var float HIGHLIGHT_INTENSITY = 0 var float BORDER_WIDTH = 0 var float INACTIVE_DIM = 0.74 @@ -100,7 +101,8 @@ sRGB ``#808080``. The default target is black, preserving the existing dimming effect. The two optional flags preserve the tab bar and include the active window's padding in its unchanged area. Explicit cell backgrounds and images are blended together with text, while transparency and colors reported to -terminal applications are preserved. +terminal applications are preserved. ``animation_step 0`` makes this static +effect redraw only when content or shader events change. Anatomy of a custom shader @@ -269,7 +271,9 @@ Directives inside a group: How many milliseconds between animation frames. Lower values produce smoother animation at the cost of more GPU draws. Defaults to ``50`` ms (20 fps). The value is clamped to be no smaller than - :opt:`repaint_delay`. + :opt:`repaint_delay`. Set this to ``0`` for static effects that should + redraw only when content or shader events change, without periodic frames. + Animation stop events and duration limits still apply. ``attach`` Mark this group as *attached* to the preceding group. An attached group diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index 6ac645b38..fb6d3d28b 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -957,13 +957,18 @@ prepare_to_render_os_window( events |= (1u << SHADER_ANIM_EVENT_TAB_CHANGE) | (1u << SHADER_ANIM_EVENT_WINDOW_FOCUS_IN) | (1u << SHADER_ANIM_EVENT_WINDOW_FOCUS_OUT); if (*active_window_id && *active_window_id != os_window->last_active_window_id) events |= (1u << SHADER_ANIM_EVENT_WINDOW_FOCUS_IN) | (1u << SHADER_ANIM_EVENT_WINDOW_FOCUS_OUT); + const bool was_active = os_window->has_active_custom_shaders; + const bool was_animating = os_window->shader_anim_min_step < MONOTONIC_T_MAX; + const bool animation_ended = now >= os_window->shader_anim_next_end_at; monotonic_t min_step = update_custom_shader_animations(events, now, os_window); os_window->shader_anim_event_registry = 0; if (os_window->has_active_custom_shaders) { os_window->needs_layers = true; - needs_render = true; + if (events || min_step < MONOTONIC_T_MAX) needs_render = true; } + if (was_active != os_window->has_active_custom_shaders || was_animating || animation_ended) needs_render = true; if (min_step < MONOTONIC_T_MAX) set_maximum_wait(min_step); + if (os_window->shader_anim_next_end_at < MONOTONIC_T_MAX) set_maximum_wait(os_window->shader_anim_next_end_at - now); } return needs_render || was_previously_rendered_with_layers != os_window->needs_layers; } diff --git a/kitty/shaders.c b/kitty/shaders.c index faafde04e..e069bd4a7 100644 --- a/kitty/shaders.c +++ b/kitty/shaders.c @@ -2998,7 +2998,7 @@ transfer_pipeline_to_struct(PyObject *pg, CustomShaderPipeline *p) { PyObject *astep = PyDict_GetItemString(g, "animation_step"); cg->animation_step = (astep && PyLong_Check(astep)) ? (monotonic_t)PyLong_AsLong(astep) : ANIMATION_SAMPLE_WAIT; - cg->animation_step = MAX(cg->animation_step, OPT(repaint_delay)); + cg->animation_step = cg->animation_step == 0 ? MONOTONIC_T_MAX : MAX(cg->animation_step, OPT(repaint_delay)); PyObject *ae_events = PyDict_GetItemString(g, "animation_end_events"); if (is_seq(ae_events)) cg->animation_end_events = parse_anim_event_sequence(ae_events); diff --git a/kitty_tests/slang.py b/kitty_tests/slang.py index 1a9fcb26f..41948dab0 100644 --- a/kitty_tests/slang.py +++ b/kitty_tests/slang.py @@ -40,6 +40,7 @@ class TestSlang(BaseTest): pipeline = parse_pipeline_definition( """ startgroup + animation_step 0 var float HIGHLIGHT_INTENSITY = 0 var float BORDER_WIDTH = 0 var float INACTIVE_DIM = 0.74 @@ -54,11 +55,12 @@ class TestSlang(BaseTest): with tempfile.TemporaryDirectory() as cache_dir: clear_caches() try: - vertex, fragment, _ = build_custom_shader_pipeline_glsl(pipeline, cache_dir=cache_dir) + vertex, fragment, metadata = build_custom_shader_pipeline_glsl(pipeline, cache_dir=cache_dir) finally: clear_caches() self.assertTrue(vertex) self.assertTrue(fragment) + self.ae(metadata['pipeline']['groups'][0]['animation_step'], 0) def test_slang_parser(self): def check(src: str, expected: SlangFile) -> None: From 18023eaafa05e4be979cfb579638f12ca68c553d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E9=B8=BD?= <18717730+xiaobai050@users.noreply.github.com> Date: Mon, 14 Sep 2026 22:41:26 +0800 Subject: [PATCH 3/3] Custom shaders: Clear cached programs when disabling shaders --- docs/changelog.rst | 2 +- kitty/shaders/slang.py | 3 +++ kitty_tests/slang.py | 21 +++++++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 85d91e2ce..048ecb3a0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -206,7 +206,7 @@ Detailed list of changes - Custom shaders: Allow tinting inactive windows toward a configurable color, preserving the tab bar and active window padding. Support static effects - without periodic redraws. + without periodic redraws, and fix re-enabling a previously disabled shader. - Various throughput performance improvements for a 15-35% real world improvement depending on workload diff --git a/kitty/shaders/slang.py b/kitty/shaders/slang.py index b0a97017a..72e258538 100644 --- a/kitty/shaders/slang.py +++ b/kitty/shaders/slang.py @@ -274,6 +274,7 @@ class LoadShaderPrograms: slot_pipelines = pmap.get(slot) if not slot_pipelines: compile_program(prog, (), (), {}, allow_recompile) + self.last_built_custom_shaders.pop(prog, None) else: try: pipeline = merge_pipelines(slot_pipelines) @@ -283,6 +284,7 @@ class LoadShaderPrograms: except Exception as e: log_error(f'Failed to build custom shader for slot {slot} with error: {e}') compile_program(prog, (), (), {}, allow_recompile) + self.last_built_custom_shaders.pop(prog, None) else: try: if self.last_built_custom_shaders.get(prog) != (vert, frag, metadata): @@ -291,6 +293,7 @@ class LoadShaderPrograms: except Exception as e: log_error(f'Failed to load custom shader for slot {slot} with error: {e}') compile_program(prog, (), (), {}, allow_recompile) + self.last_built_custom_shaders.pop(prog, None) do(CUSTOM_END_PROGRAM, 'end') compile_program(-2, (), (), {}) # initialize programs diff --git a/kitty_tests/slang.py b/kitty_tests/slang.py index 41948dab0..360863371 100644 --- a/kitty_tests/slang.py +++ b/kitty_tests/slang.py @@ -8,10 +8,12 @@ import os import shutil import subprocess import tempfile +from unittest.mock import patch from kitty.constants import slangc from kitty.shaders.slang import ( EntryPoint, + LoadShaderPrograms, SlangFile, Stage, build_custom_shader_pipeline_glsl, @@ -62,6 +64,25 @@ class TestSlang(BaseTest): self.assertTrue(fragment) self.ae(metadata['pipeline']['groups'][0]['animation_step'], 0) + def test_custom_shader_reenable(self): + from kitty.fast_data_types import CUSTOM_END_PROGRAM + from kitty.options.types import defaults + + loader = LoadShaderPrograms() + loader.last_built_custom_shaders = {} + pipeline = parse_pipeline_definition(['startgroup', 'animation_step 0', 'shaders focus-highlight', 'endgroup'], 'test') + self.ae(pipeline['groups'][0]['animation_step'], 0) + with ( + patch('kitty.shaders.slang.parse_pipeline', return_value=pipeline), + patch('kitty.shaders.slang.build_custom_shader_pipeline_glsl', return_value=('vertex', 'fragment', {})), + patch('kitty.shaders.slang.compile_program') as compile_program, + ): + for shaders in (('test',), (), ('test',)): + loader.opts = defaults._replace(custom_shaders=shaders) + loader.compile_custom_shaders(allow_recompile=True) + operations = [c.args[1] for c in compile_program.call_args_list if c.args[0] == CUSTOM_END_PROGRAM] + self.ae(operations, [('vertex',), (), ('vertex',)]) + def test_slang_parser(self): def check(src: str, expected: SlangFile) -> None: actual = parse_slang_text(src)