diff --git a/docs/changelog.rst b/docs/changelog.rst index 976479549..f7ac1c810 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -181,6 +181,8 @@ Detailed list of changes - Draw a progress bar at the top of the window when a program reports progress using the OSC 9;4 escape sequence, controlled by :opt:`progress_bar` (:iss:`9777`) +- Allow specifying multiple background images for :opt:`background_image` that are stored on GPU to allow fast image switching (:pull:`9836`) + - :doc:`Remote control `: Expose :code:`session_name` and :code:`last_focused_at` in the output of ``kitten @ ls`` for each window (:iss:`9732`, :iss:`9799`) - Allow optionally dragging URLs with the mouse, see :sc:`start_simple_selection` (:pull:`9804`) diff --git a/kitty/boss.py b/kitty/boss.py index b59bf3804..ed6bbd025 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -3141,11 +3141,6 @@ class Boss: if is_macos: from .fast_data_types import cocoa_recreate_global_menu cocoa_recreate_global_menu() - # Update misc options - try: - set_background_image(opts.background_image, tuple(self.os_window_map), True, opts.background_image_layout) - except Exception as e: - log_error(f'Failed to set background image with error: {e}') for tm in self.all_tab_managers: tm.apply_options() # Update colors @@ -3544,9 +3539,12 @@ class Boss: def set_background_image( self, path: str | None, os_windows: tuple[int, ...], configured: bool, layout: str | None, png_data: bytes = b'', - linear_interpolation: bool | None = None, tint: float | None = None, tint_gaps: float | None = None + linear_interpolation: bool | None = None, tint: float | None = None, tint_gaps: float | None = None, + global_index: int = -1, is_increment: bool = False, ) -> None: - set_background_image(path, os_windows, configured, layout, png_data, linear_interpolation, tint, tint_gaps) + set_background_image( + path, os_windows, configured, layout, png_data, linear_interpolation, tint, tint_gaps, + global_index, is_increment) # Can be called with kitty -o "map f1 send_test_notification" def send_test_notification(self) -> None: diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index 7f4b6685a..b6e5d7127 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -761,7 +761,7 @@ prepare_to_render_os_window(OSWindow *os_window, monotonic_t now, unsigned int * bool was_previously_rendered_with_layers = os_window->needs_layers; os_window->needs_layers = ( !global_state.supports_framebuffer_srgb || effective_os_window_alpha(os_window) < 1.f || - os_window->live_resize.in_progress || (os_window->bgimage && os_window->bgimage->texture_id > 0) + os_window->live_resize.in_progress || (background_image_for_os_window(os_window) != NULL) ); if (TD.screen && os_window->num_tabs && !os_window->has_too_few_tabs) { if (!os_window->tab_bar_data_updated) { @@ -983,7 +983,7 @@ render_os_window(OSWindow *w, monotonic_t now, bool scan_for_animated_images) { if (!w->fonts_data) { log_error("No fonts data found for window id: %llu", w->id); return false; } if (prepare_to_render_os_window(w, now, &active_window_id, &active_window_bg, &num_visible_windows, &all_windows_have_same_bg, scan_for_animated_images)) needs_render = true; if (w->last_active_window_id != active_window_id || w->last_active_tab != w->active_tab || w->focused_at_last_render != w->is_focused) needs_render = true; - if (w->render_calls < 3 && w->bgimage && w->bgimage->texture_id) needs_render = true; + if (w->render_calls < 3 && background_image_for_os_window(w) != NULL) needs_render = true; if (needs_render) render_prepared_os_window(w, active_window_id, active_window_bg, num_visible_windows, all_windows_have_same_bg); if (w->is_focused) change_menubar_title(w->window_title); return needs_render; diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 1852e33fa..54b53959f 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -728,6 +728,8 @@ def set_background_image( linear: bool | None = None, tint: float | None = None, tint_gaps: float | None = None, + global_index: int = -1, + is_increment: bool = False, ) -> None: pass diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 94d59d3de..37b33915e 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -1956,8 +1956,13 @@ this option by reloading the config is not supported. opt('background_image', 'none', - option_type='config_or_absolute_path', ctype='!background_image', - long_text='Path to a background image. Must be in PNG/JPEG/WEBP/TIFF/GIF/BMP format.' + option_type='background_images', ctype='!background_images', + long_text='Glob pattern matching one or more background images. Must be in PNG/JPEG/WEBP/TIFF/GIF/BMP format.' + ' The first one is used as the default image, you can change the current image using :ac:`change_background_image`.' + ' Multiple images are stored in GPU VRAM (on demand) so that transitioning between them is instant.' + ' In order to move between images in the list of background images, use the remote control command' + ' :code:`set-backround-image`, see `kitten set-backround-image --help` for details. You can map the command' + ' to a key like this: :code:`map f1 remote_control set-backround-image +1`.' ' Note that when using :ref:`auto_color_scheme` this option is overridden by the color scheme file and must be set inside it to take effect.' ) diff --git a/kitty/options/parse.py b/kitty/options/parse.py index 0b8ef2b08..d3abbaeef 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -8,22 +8,23 @@ from kitty.conf.utils import ( to_color, to_color_or_none, unit_float ) from kitty.options.utils import ( - action_alias, active_tab_title_template, allow_hyperlinks, bell_on_tab, box_drawing_scale, - clear_all_mouse_actions, clear_all_shortcuts, clipboard_control, clone_source_strategies, - config_or_absolute_path, confirm_close, copy_on_select, cursor_blink_interval, cursor_text_color, - cursor_trail_decay, deprecated_adjust_line_height, deprecated_hide_window_decorations_aliases, - deprecated_macos_show_window_title_in_menubar_alias, deprecated_scrollback_indicator_opacity, - deprecated_send_text, disable_ligatures, edge_width, env, filter_notification, font_features, - hide_window_decorations, macos_option_as_alt, macos_titlebar_color, menu_map, modify_font, - mouse_hide_wait, narrow_symbols, notify_on_cmd_finish, optional_edge_width, parse_font_spec, - parse_map, parse_mouse_map, paste_actions, pointer_shape_when_dragging, remote_control_password, - resize_debounce_time, scrollback_lines, scrollback_pager_history_size, scrollbar_color, - shell_integration, show_hyperlink_targets, store_multiple, symbol_map, tab_activity_symbol, - tab_bar_edge, tab_bar_margin_height, tab_bar_min_tabs, tab_fade, tab_font_style, tab_separator, - tab_title_template, text_fg_override_threshold, titlebar_color, to_cursor_shape, - to_cursor_unfocused_shape, to_font_size, to_layout_names, to_modifiers, - transparent_background_colors, underline_exclusion, url_prefixes, url_style, visual_bell_duration, - visual_window_select_characters, window_border_width, window_logo_scale, window_size + action_alias, active_tab_title_template, allow_hyperlinks, background_images, bell_on_tab, + box_drawing_scale, clear_all_mouse_actions, clear_all_shortcuts, clipboard_control, + clone_source_strategies, config_or_absolute_path, confirm_close, copy_on_select, + cursor_blink_interval, cursor_text_color, cursor_trail_decay, deprecated_adjust_line_height, + deprecated_hide_window_decorations_aliases, deprecated_macos_show_window_title_in_menubar_alias, + deprecated_scrollback_indicator_opacity, deprecated_send_text, disable_ligatures, edge_width, env, + filter_notification, font_features, hide_window_decorations, macos_option_as_alt, + macos_titlebar_color, menu_map, modify_font, mouse_hide_wait, narrow_symbols, notify_on_cmd_finish, + optional_edge_width, parse_font_spec, parse_map, parse_mouse_map, paste_actions, + pointer_shape_when_dragging, remote_control_password, resize_debounce_time, scrollback_lines, + scrollback_pager_history_size, scrollbar_color, shell_integration, show_hyperlink_targets, + store_multiple, symbol_map, tab_activity_symbol, tab_bar_edge, tab_bar_margin_height, + tab_bar_min_tabs, tab_fade, tab_font_style, tab_separator, tab_title_template, + text_fg_override_threshold, titlebar_color, to_cursor_shape, to_cursor_unfocused_shape, + to_font_size, to_layout_names, to_modifiers, transparent_background_colors, underline_exclusion, + url_prefixes, url_style, visual_bell_duration, visual_window_select_characters, window_border_width, + window_logo_scale, window_size ) @@ -77,7 +78,7 @@ class Parser: ans['background_blur'] = int(val) def background_image(self, val: str, ans: dict[str, typing.Any]) -> None: - ans['background_image'] = config_or_absolute_path(val) + ans['background_image'] = background_images(val) def background_image_layout(self, val: str, ans: dict[str, typing.Any]) -> None: val = val.lower() diff --git a/kitty/options/to-c-generated.h b/kitty/options/to-c-generated.h index 5acded04c..5d67018a2 100644 --- a/kitty/options/to-c-generated.h +++ b/kitty/options/to-c-generated.h @@ -1203,7 +1203,7 @@ convert_from_opts_dynamic_background_opacity(PyObject *py_opts, Options *opts) { static void convert_from_python_background_image(PyObject *val, Options *opts) { - background_image(val, opts); + background_images(val, opts); } static void diff --git a/kitty/options/to-c.h b/kitty/options/to-c.h index 36b61065c..aab7756f8 100644 --- a/kitty/options/to-c.h +++ b/kitty/options/to-c.h @@ -167,7 +167,28 @@ bganchor(PyObject *anchor_name) { } static inline void -background_image(PyObject *src, Options *opts) { STR_SETTER(background_image); } +free_background_images(Options *opts) { + if (opts->background_images.paths) { + for (size_t i = 0; i < opts->background_images.count; i++) free(opts->background_images.paths[i]); + free(opts->background_images.paths); + } + opts->background_images.paths = NULL; + opts->background_images.count = 0; +} + +static inline void +background_images(PyObject *src, Options *opts) { + free_background_images(opts); + static unsigned generation = 0; + opts->background_images.generation = ++generation; + if (!PyTuple_Check(src) || PyTuple_GET_SIZE(src) == 0) return; + opts->background_images.paths = calloc(PyTuple_GET_SIZE(src), sizeof(opts->background_images.paths[0])); + if (opts->background_images.paths) { + opts->background_images.count = PyTuple_GET_SIZE(src); + for (size_t i = 0; i < opts->background_images.count; i++) opts->background_images.paths[i] = strdup( + PyUnicode_AsUTF8(PyTuple_GET_ITEM(src, i))); + } +} static inline void bell_path(PyObject *src, Options *opts) { STR_SETTER(bell_path); } @@ -554,8 +575,9 @@ free_allocs_in_options(Options *opts) { free_menu_map(opts); free_url_prefixes(opts); free_font_features(opts); + free_background_images(opts); #define F(x) free(opts->x); opts->x = NULL; F(select_by_word_characters); F(url_excluded_characters); F(select_by_word_characters_forward); - F(background_image); F(bell_path); F(bell_theme); F(default_window_logo); + F(bell_path); F(bell_theme); F(default_window_logo); #undef F } diff --git a/kitty/options/types.py b/kitty/options/types.py index 71547ed42..ca38b3658 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -529,7 +529,7 @@ class Options: allow_remote_control: choices_for_allow_remote_control = 'no' background: Color = Color(0, 0, 0) background_blur: int = 0 - background_image: str | None = None + background_image: tuple[str, ...] = () background_image_layout: choices_for_background_image_layout = 'tiled' background_image_linear: bool = False background_opacity: float = 1.0 diff --git a/kitty/options/utils.py b/kitty/options/utils.py index e72b5259b..7cf8db700 100644 --- a/kitty/options/utils.py +++ b/kitty/options/utils.py @@ -855,6 +855,15 @@ def config_or_absolute_path(x: str, env: dict[str, str] | None = None) -> str | return resolve_abs_or_config_path(x, env) +def background_images(x: str) -> tuple[str, ...]: + if x.lower() in ('none', ''): + return () + from glob import glob + x = resolve_abs_or_config_path(x, None) + return tuple(x for x in sorted(glob(x)) if x.rpartition('.')[-1].lower() in ( + 'jpeg', 'jpg', 'png', 'webp', 'tiff', 'bmp', 'gif')) + + def filter_notification(val: str, current_val: dict[str, str]) -> Iterable[tuple[str, str]]: yield val, '' diff --git a/kitty/rc/set_background_image.py b/kitty/rc/set_background_image.py index d09e2088d..d987de45c 100644 --- a/kitty/rc/set_background_image.py +++ b/kitty/rc/set_background_image.py @@ -35,7 +35,7 @@ class SetBackgroundImage(RemoteCommand): protocol_spec = __doc__ = ''' data+/str: Chunk of at most 512 bytes of PNG data, base64 encoded. Must send an empty chunk to indicate end of image. \ - Or the special value - to indicate image must be removed. + Or the special value - to indicate image must be removed. Or the value index:idx to change image. match/str: Window to change opacity in layout/choices.{layout_choices.replace(",", ".")}: The image layout all/bool: Boolean indicating operate on all windows @@ -46,8 +46,14 @@ class SetBackgroundImage(RemoteCommand): desc = ( 'Set the background image for the specified OS windows. You must specify the path to an image that' ' will be used as the background. If you specify the special value :code:`none` then any existing image will' - ' be removed. Supported image formats are: ' - ) + ', '.join(SUPPORTED_IMAGE_FORMATS) + ' be removed.' + f' Supported image formats are: {", ".join(SUPPORTED_IMAGE_FORMATS)}' + '\n\n' + 'You can also specify an index to change the background image to one of the pre-configured set of background images.' + ' The index is either a number, or a number preceeded by the plus or minus sign which acts as an increment.' + ' If the index falls outside the range of configured images, the first configured image is used.' + ' For example: set-background -- -1 or set-background 3' + ) options_spec = f'''\ --all -a type=bool-set @@ -57,7 +63,8 @@ cause the image to be changed in all windows. --configured -c type=bool-set -Change the configured background image which is used for new OS windows. +Change the configured background image which is used for new OS windows. Note that an index with this option +is not supported. Specifying :code:`none` will erase *all* configured background images. --layout @@ -73,7 +80,7 @@ default=false Don't wait for a response from kitty. This means that even if setting the background image failed, the command will exit with a success code. ''' + '\n\n' + MATCH_WINDOW_OPTION - args = RemoteCommand.Args(spec='PATH_TO_PNG_IMAGE', count=1, json_field='data', special_parse='!read_window_logo(io_data, args[0])', + args = RemoteCommand.Args(spec='PATH_TO_IMAGE_OR_INDEX', count=1, json_field='data', special_parse='!read_background_image(io_data, args[0])', completion=ImageCompletion) reads_streaming_data = True @@ -92,6 +99,10 @@ failed, the command will exit with a success code. if path.lower() == 'none': ret['data'] = '-' return ret + import re + if re.match(r'[+-]?\d+', path) is not None: + ret['data'] = f'index:{path}' + return ret if not is_png(path): self.fatal(f'{path} is not a PNG image') @@ -109,12 +120,20 @@ failed, the command will exit with a success code. def response_from_kitty(self, boss: Boss, window: Window | None, payload_get: PayloadGetType) -> ResponseType: data = payload_get('data') - windows = self.windows_for_payload(boss, window, payload_get, window_match_name='match') - os_windows = tuple({w.os_window_id for w in windows if w}) - layout = payload_get('layout') + path = None + global_index = -1 + is_increment = False if data == '-': - path = None tfile = BytesIO() + elif data and data.startswith('index:'): + if payload_get('configured'): + ex = ValueError('Cannot use --configured with an image index') + ex.hide_traceback = True # type: ignore + raise ex + tfile = BytesIO() + data = data[len('index:'):] + global_index = int(data) + is_increment = data[0] in '+-' else: q = self.handle_streamed_data(standard_b64decode(data) if data else b'', payload_get) if isinstance(q, AsyncResponse): @@ -122,8 +141,13 @@ failed, the command will exit with a success code. path = '/image/from/remote/control' tfile = q + windows = self.windows_for_payload(boss, window, payload_get, window_match_name='match') + os_windows = tuple({w.os_window_id for w in windows if w}) + layout = payload_get('layout') try: - boss.set_background_image(path, os_windows, payload_get('configured'), layout, tfile.getvalue()) + boss.set_background_image( + path, os_windows, payload_get('configured'), layout, tfile.getvalue(), + global_index=global_index, is_increment=is_increment) except ValueError as err: err.hide_traceback = True # type: ignore raise diff --git a/kitty/shaders.c b/kitty/shaders.c index c541f84c8..c305f1bdc 100644 --- a/kitty/shaders.c +++ b/kitty/shaders.c @@ -450,7 +450,7 @@ pick_cursor_color(color_type cell_fg, color_type cell_bg, color_type *cursor_fg, static bool has_bgimage(OSWindow *w) { - return w->bgimage && w->bgimage->texture_id > 0; + return background_image_for_os_window(w) != NULL; } static color_type @@ -1526,13 +1526,14 @@ draw_cursor_trail(CursorTrail *trail, Window *active_window) { // OSWindow {{{ static void draw_bg_image(OSWindow *os_window, Tab *tab) { - if (!has_bgimage(os_window)) return; + BackgroundImage *bg = background_image_for_os_window(os_window); + if (!bg) return; BackgroundImageRenderSettings s = { .os_window.width = os_window->viewport_width, .os_window.height = os_window->viewport_height, - .instance_id = os_window->bgimage->id, .layout=OPT(background_image_layout), + .instance_id = bg->id, .layout=OPT(background_image_layout), .linear=OPT(background_image_linear), .bgcolor=OPT(background), .opacity=effective_os_window_alpha(os_window), }; - GLfloat iwidth = os_window->bgimage->width, iheight = os_window->bgimage->height; + GLfloat iwidth = bg->width, iheight = bg->height; GLfloat vwidth = s.os_window.width, vheight = s.os_window.height; if (CENTER_SCALED == OPT(background_image_layout)) { GLfloat ifrac = iwidth / iheight; @@ -1570,7 +1571,7 @@ draw_bg_image(OSWindow *os_window, Tab *tab) { glUniform1i(bgimage_program_layout.uniforms.image, GRAPHICS_UNIT); color_vec4(bgimage_program_layout.uniforms.background, s.bgcolor, s.opacity); glActiveTexture(GL_TEXTURE0 + GRAPHICS_UNIT); - glBindTexture(GL_TEXTURE_2D, os_window->bgimage->texture_id); + glBindTexture(GL_TEXTURE_2D, bg->texture_id); draw_quad(false, 0); unbind_program(); } diff --git a/kitty/state.c b/kitty/state.c index 507ccca16..d3809c662 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -209,6 +209,70 @@ free_bgimage(BackgroundImage **bgimage, bool release_texture) { bgimage = NULL; } +static void +free_global_background_images(bool release_texture) { + if (global_state.background_images.images) { + for (size_t i = 0; i < global_state.background_images.count; i++) + free_bgimage(&global_state.background_images.images[i], release_texture); + free(global_state.background_images.images); + global_state.background_images.images = NULL; + } + zero_at_ptr(&global_state.background_images); +} + +static void +ensure_background_images_generation(bool release_texture) { + if (global_state.background_images.generation == OPT(background_images).generation) return; + free_global_background_images(release_texture); + global_state.background_images.generation = OPT(background_images).generation; + global_state.background_images.images = calloc( + OPT(background_images).count, sizeof(global_state.background_images.images[0])); + if (!global_state.background_images.images) fatal("Out of memory"); +} + +static unsigned bg_image_id_counter = 0; + +static BackgroundImage* +global_background_image(size_t idx) { + ensure_background_images_generation(true); + while (global_state.background_images.count <= idx && OPT(background_images).count) { + RAII_ALLOC(char, path, OPT(background_images).paths[0]); // transfer ownership + remove_i_from_array(OPT(background_images.paths), 0, OPT(background_images).count); + BackgroundImage *img = calloc(1, sizeof(BackgroundImage)); + if (!img) fatal("Out of memory"); + if (image_path_to_bitmap(path, &img->bitmap, &img->width, &img->height, &img->mmap_size)) { + img->refcnt++; + img->id = ++bg_image_id_counter; + global_state.background_images.images[global_state.background_images.count++] = img; + send_bgimage_to_gpu(OPT(background_image_layout), img); + } else free(img); + } + return global_state.background_images.count > idx ? global_state.background_images.images[idx] : NULL; +} + +BackgroundImage* +background_image_for_os_window(OSWindow *w) { + if (w->background_image.no_image) return NULL; + if (w->background_image.override) return w->background_image.override; + BackgroundImage *ans = NULL; + if (w->background_image.global_bg_images_idx && ( + ans = global_background_image(w->background_image.global_bg_images_idx))) return ans; + return global_background_image(0); +} + +static size_t +increment_bg_image_idx(size_t idx, int delta) { + if (delta == 0) return idx; + if (delta > 0) { + size_t new_idx = idx + delta; + return global_background_image(new_idx) ? new_idx : 0; + } + if (-delta <= (ssize_t)idx) return idx + delta; + // wrap to last image, which means we need to load all + global_background_image(global_state.background_images.count + OPT(background_images).count + 1); + return global_state.background_images.count ? global_state.background_images.count - 1 : 0; +} + OSWindow* add_os_window(void) { WITH_OS_WINDOW_REFS @@ -219,23 +283,6 @@ add_os_window(void) { ans->tab_bar_render_data.vao_idx = create_cell_vao(); ans->background_opacity.alpha = OPT(background_opacity); ans->created_at = monotonic(); - - bool wants_bg = OPT(background_image) && OPT(background_image)[0] != 0; - if (wants_bg) { - if (!global_state.bgimage) { - global_state.bgimage = calloc(1, sizeof(BackgroundImage)); - if (!global_state.bgimage) fatal("Out of memory allocating the global bg image object"); - global_state.bgimage->refcnt++; - if (image_path_to_bitmap(OPT(background_image), &global_state.bgimage->bitmap, &global_state.bgimage->width, &global_state.bgimage->height, &global_state.bgimage->mmap_size)) { - send_bgimage_to_gpu(OPT(background_image_layout), global_state.bgimage); - } - } - if (global_state.bgimage->texture_id) { - ans->bgimage = global_state.bgimage; - ans->bgimage->refcnt++; - } - } - END_WITH_OS_WINDOW_REFS return ans; } @@ -496,8 +543,8 @@ destroy_os_window_item(OSWindow *w) { Py_CLEAR(w->window_title); Py_CLEAR(w->tab_bar_render_data.screen); remove_vao(w->tab_bar_render_data.vao_idx); free(w->tabs); w->tabs = NULL; - free_bgimage(&w->bgimage, true); - zero_at_ptr(&w->bgimage); + free_bgimage(&w->background_image.override, true); + zero_at_ptr(&w->background_image); if (w->indirect_output.framebuffer_id) free_framebuffer(&w->indirect_output.framebuffer_id); } @@ -951,7 +998,7 @@ PYWRAP1(os_window_has_background_image) { id_type os_window_id; PA("K", &os_window_id); WITH_OS_WINDOW(os_window_id) - if (os_window->bgimage && os_window->bgimage->texture_id > 0) { Py_RETURN_TRUE; } + if (background_image_for_os_window(os_window) != NULL) Py_RETURN_TRUE; END_WITH_OS_WINDOW Py_RETURN_FALSE; } @@ -1322,8 +1369,12 @@ pyset_background_image(PyObject *self UNUSED, PyObject *args, PyObject *kw) { PyObject *os_window_ids; int configured = 0; char *png_data = NULL; Py_ssize_t png_data_size = 0; - static char *kwds[] = {"path", "os_window_ids", "configured", "layout_name", "png_data", "linear", "tint", "tint_gaps", NULL}; - if (!PyArg_ParseTupleAndKeywords(args, kw, "zO!|pOy#OOO", kwds, &path, &PyTuple_Type, &os_window_ids, &configured, &layout_name, &png_data, &png_data_size, &pylinear, &pytint, &pytint_gaps)) return NULL; + int global_index = -1; int is_increment = 0; + static char *kwds[] = { + "path", "os_window_ids", "configured", "layout_name", "png_data", "linear", "tint", "tint_gaps", "global_index", + "is_increment", + NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kw, "zO!|pOy#OOOip", kwds, &path, &PyTuple_Type, &os_window_ids, &configured, &layout_name, &png_data, &png_data_size, &pylinear, &pytint, &pytint_gaps, &global_index, &is_increment)) return NULL; size_t size; BackgroundImageLayout layout = PyUnicode_Check(layout_name) ? bglayout(layout_name) : OPT(background_image_layout); BackgroundImage *bgimage = NULL; @@ -1341,15 +1392,22 @@ pyset_background_image(PyObject *self UNUSED, PyObject *args, PyObject *kw) { free(bgimage); return NULL; } - static uint32_t bgimage_id_counter = 0; - bgimage->id = ++bgimage_id_counter; + bgimage->id = ++bg_image_id_counter; send_bgimage_to_gpu(layout, bgimage); bgimage->refcnt++; } if (configured) { - free_bgimage(&global_state.bgimage, true); - global_state.bgimage = bgimage; - if (bgimage) bgimage->refcnt++; + if (bgimage) { + if (global_background_image(0)) { + free_bgimage(&global_state.background_images.images[0], true); + } else { + free_global_background_images(true); + global_state.background_images.images = calloc(1, sizeof(global_state.background_images.images[0])); + if (!global_state.background_images.images) fatal("Out of memory"); + } + global_state.background_images.images[0] = bgimage; + bgimage->refcnt++; + } else free_global_background_images(true); OPT(background_image_layout) = layout; if (pylinear && pylinear != Py_None) convert_from_python_background_image_linear(pylinear, &global_state.opts); if (pytint && pytint != Py_None) convert_from_python_background_tint(pytint, &global_state.opts); @@ -1358,11 +1416,20 @@ pyset_background_image(PyObject *self UNUSED, PyObject *args, PyObject *kw) { for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(os_window_ids); i++) { id_type os_window_id = PyLong_AsUnsignedLongLong(PyTuple_GET_ITEM(os_window_ids, i)); WITH_OS_WINDOW(os_window_id) + unsigned idx = os_window->background_image.global_bg_images_idx; make_os_window_context_current(os_window); - free_bgimage(&os_window->bgimage, true); - os_window->bgimage = bgimage; + free_bgimage(&os_window->background_image.override, true); + zero_at_ptr(&os_window->background_image); os_window->render_calls = 0; - if (bgimage) bgimage->refcnt++; + if (bgimage) { + if (!configured) { // configured means we use the zero index global image + os_window->background_image.override = bgimage; + bgimage->refcnt++; + } + } else if (is_increment) { + os_window->background_image.global_bg_images_idx = increment_bg_image_idx(idx, global_index); + } else if (global_index < 0) os_window->background_image.no_image = true; + else os_window->background_image.global_bg_images_idx = global_index; END_WITH_OS_WINDOW } if (bgimage) free_bgimage(&bgimage, true); @@ -1700,9 +1767,6 @@ finalize(void) { } if (detached_windows.windows) free(detached_windows.windows); detached_windows.capacity = 0; -#define F(x) free(OPT(x)); OPT(x) = NULL; - F(background_image); F(bell_path); F(bell_theme); F(default_window_logo); -#undef F Py_CLEAR(global_state.options_object); free_animation(OPT(animation.cursor)); free_animation(OPT(animation.visual_bell)); @@ -1710,9 +1774,8 @@ finalize(void) { // that freeing the texture will work during shutdown and // the GPU driver should take care of it when the OpenGL context is // destroyed. - free_bgimage(&global_state.bgimage, false); + free_global_background_images(false); free_window_logo_table(&global_state.all_window_logos); - global_state.bgimage = NULL; free_drag_source(); Py_CLEAR(global_state.drop_dest.data); zero_at_ptr(&global_state.drop_dest); diff --git a/kitty/state.h b/kitty/state.h index baf26c905..0f743864e 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -93,7 +93,11 @@ typedef struct Options { float text_contrast, text_gamma_adjustment; bool text_old_gamma; - char *background_image, *default_window_logo; + char *default_window_logo; + struct { + char **paths; size_t count; + unsigned generation; + } background_images; BackgroundImageLayout background_image_layout; ImageAnchorPosition window_logo_position; bool background_image_linear; @@ -409,7 +413,11 @@ typedef struct OSWindow { int viewport_width, viewport_height, window_width, window_height; double viewport_x_ratio, viewport_y_ratio; Tab *tabs; - BackgroundImage *bgimage; + struct { + size_t global_bg_images_idx; + BackgroundImage *override; + bool no_image; + } background_image; struct { uint32_t framebuffer_id, attached_texture_generation; } indirect_output; @@ -460,7 +468,11 @@ typedef struct GlobalState { id_type os_window_id_counter, tab_id_counter, window_id_counter; PyObject *boss; - BackgroundImage *bgimage; + struct { + BackgroundImage **images; + size_t count; + unsigned generation; + } background_images; OSWindow *os_windows; size_t num_os_windows, capacity; OSWindow *callback_os_window; @@ -625,3 +637,4 @@ void cancel_current_drag_source(void); bool change_drag_image(int idx); int start_window_drag(Window *w); int notify_drag_data_ready(id_type os_window_id, const char *mime_type); +BackgroundImage* background_image_for_os_window(OSWindow *w); diff --git a/tools/cmd/at/set_window_logo.go b/tools/cmd/at/set_window_logo.go index 59066fc91..40f7578ec 100644 --- a/tools/cmd/at/set_window_logo.go +++ b/tools/cmd/at/set_window_logo.go @@ -76,3 +76,33 @@ func read_window_logo(io_data *rc_io_data, path string) (func(io_data *rc_io_dat return false, nil }, nil } + +func is_index_arg(s string) bool { + if len(s) == 0 { + return false + } + start := 0 + if s[0] == '+' || s[0] == '-' { + start = 1 + } + if start >= len(s) { + return false + } + for _, c := range s[start:] { + if c < '0' || c > '9' { + return false + } + } + return true +} + +func read_background_image(io_data *rc_io_data, path string) (func(io_data *rc_io_data) (bool, error), error) { + if is_index_arg(path) { + io_data.rc.Stream = false + return func(io_data *rc_io_data) (bool, error) { + set_payload_data(io_data, "index:"+path) + return true, nil + }, nil + } + return read_window_logo(io_data, path) +}