diff --git a/docs/changelog.rst b/docs/changelog.rst index fd6c4e0fa..cff54ec87 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -170,6 +170,8 @@ Detailed list of changes - A new option :opt:`palette_generate` to automatically generate the 256 color palette from the first 16 colors (:pull:`9426`) +- Add a :opt:`progress_bar` option to draw a visual progress bar when a program reports progress using the OSC 9;4 escape sequence. The bar can be positioned on any edge of the window (left, right, top, bottom) and reuses the scrollbar's track and handle colors and opacity settings. In the indeterminate state, a handle animates back and forth (:iss:`9777`) + - For builtin key mappings automatically :ref:`fallback ` to matching the US-PC layout key when the pressed key has no matches and is a non-English character (:pull:`9671`) - Allow drag and drop of windows to re-arrange them, move them to another diff --git a/kitty/data-types.h b/kitty/data-types.h index d96f6800a..d7fb92aa3 100644 --- a/kitty/data-types.h +++ b/kitty/data-types.h @@ -116,6 +116,8 @@ typedef enum MouseShapes { } MouseShape; typedef enum { NONE, MENUBAR, WINDOW, ALL } WindowTitleIn; typedef enum { SCROLLBAR_NEVER, SCROLLBAR_ON_SCROLLED, SCROLLBAR_ON_HOVERED, SCROLLBAR_ON_SCROLL_AND_HOVER, SCROLLBAR_ALWAYS } ScrollbarVisibilityPolicy; +typedef enum { PROGRESS_BAR_HIDDEN, PROGRESS_BAR_LEFT, PROGRESS_BAR_RIGHT, PROGRESS_BAR_TOP, PROGRESS_BAR_BOTTOM } ProgressBarPosition; +typedef enum { PROGRESS_STATE_UNSET, PROGRESS_STATE_SET, PROGRESS_STATE_ERROR, PROGRESS_STATE_INDETERMINATE, PROGRESS_STATE_PAUSED } ProgressBarState; typedef enum { TILING, SCALED, MIRRORED, CLAMPED, CENTER_CLAMPED, CENTER_SCALED } BackgroundImageLayout; typedef struct ImageAnchorPosition { float canvas_x, canvas_y, image_x, image_y; diff --git a/kitty/options/definition.py b/kitty/options/definition.py index e5f31fc00..ee33c9d1f 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -519,6 +519,27 @@ use the current selection background color. Also, you can use an arbitrary color, such as :code:`#12af59` or :code:`red`. ''') +opt('progress_bar', 'top', ctype='progress_bar', choices=( + 'left', 'right', 'top', 'bottom', 'hidden'), long_text='''\ +When a program uses the OSC 9;4 escape sequence to report progress, draw a progress bar +in the specified position of the window. The bar uses the same track and handle colors and opacity settings +as the scrollbar (:opt:`scrollbar_track_color`, :opt:`scrollbar_handle_color`, +:opt:`scrollbar_track_hover_opacity`, :opt:`scrollbar_handle_opacity`, +:opt:`scrollbar_width`, :opt:`scrollbar_gap`, :opt:`scrollbar_radius`). +When the progress bar and scrollbar overlap, the progress bar is drawn underneath the scrollbar. + +:code:`left` + means a vertical progress bar on the left edge. +:code:`right` + means a vertical progress bar on the right edge. +:code:`top` + means a horizontal progress bar on the top edge. +:code:`bottom` + means a horizontal progress bar on the bottom edge. +:code:`hidden` + means disable the progress bar. +''') + opt('scrollback_pager', 'less --chop-long-lines --RAW-CONTROL-CHARS +INPUT_LINE_NUMBER', option_type='to_cmdline', long_text=''' diff --git a/kitty/options/parse.py b/kitty/options/parse.py index 6ece678d3..253f273e7 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -1201,6 +1201,14 @@ class Parser: choices_for_pointer_shape_when_grabbed = choices_for_default_pointer_shape + def progress_bar(self, val: str, ans: dict[str, typing.Any]) -> None: + val = val.lower() + if val not in self.choices_for_progress_bar: + raise ValueError(f"The value {val} is not a valid choice for progress_bar") + ans["progress_bar"] = val + + choices_for_progress_bar = frozenset(('left', 'right', 'top', 'bottom', 'hidden')) + def remember_window_position(self, val: str, ans: dict[str, typing.Any]) -> None: ans['remember_window_position'] = to_bool(val) diff --git a/kitty/options/to-c-generated.h b/kitty/options/to-c-generated.h index 0d875441b..ea1fd8633 100644 --- a/kitty/options/to-c-generated.h +++ b/kitty/options/to-c-generated.h @@ -434,6 +434,19 @@ convert_from_opts_scrollbar_track_color(PyObject *py_opts, Options *opts) { Py_DECREF(ret); } +static void +convert_from_python_progress_bar(PyObject *val, Options *opts) { + opts->progress_bar = progress_bar(val); +} + +static void +convert_from_opts_progress_bar(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "progress_bar"); + if (ret == NULL) return; + convert_from_python_progress_bar(ret, opts); + Py_DECREF(ret); +} + static void convert_from_python_scrollback_pager_history_size(PyObject *val, Options *opts) { opts->scrollback_pager_history_size = PyLong_AsUnsignedLong(val); @@ -1084,6 +1097,19 @@ convert_from_opts_tab_bar_style(PyObject *py_opts, Options *opts) { Py_DECREF(ret); } +static void +convert_from_python_tab_bar_show_new_tab_button(PyObject *val, Options *opts) { + opts->tab_bar_show_new_tab_button = PyObject_IsTrue(val); +} + +static void +convert_from_opts_tab_bar_show_new_tab_button(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "tab_bar_show_new_tab_button"); + if (ret == NULL) return; + convert_from_python_tab_bar_show_new_tab_button(ret, opts); + Py_DECREF(ret); +} + static void convert_from_python_tab_bar_background(PyObject *val, Options *opts) { opts->tab_bar_background = color_or_none_as_int(val); @@ -1516,6 +1542,8 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) { if (PyErr_Occurred()) return false; convert_from_opts_scrollbar_track_color(py_opts, opts); if (PyErr_Occurred()) return false; + convert_from_opts_progress_bar(py_opts, opts); + if (PyErr_Occurred()) return false; convert_from_opts_scrollback_pager_history_size(py_opts, opts); if (PyErr_Occurred()) return false; convert_from_opts_scrollback_fill_enlarged_window(py_opts, opts); @@ -1616,6 +1644,8 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) { if (PyErr_Occurred()) return false; convert_from_opts_tab_bar_style(py_opts, opts); if (PyErr_Occurred()) return false; + convert_from_opts_tab_bar_show_new_tab_button(py_opts, opts); + if (PyErr_Occurred()) return false; convert_from_opts_tab_bar_background(py_opts, opts); if (PyErr_Occurred()) return false; convert_from_opts_tab_bar_margin_color(py_opts, opts); diff --git a/kitty/options/to-c.h b/kitty/options/to-c.h index 91fae2062..36b61065c 100644 --- a/kitty/options/to-c.h +++ b/kitty/options/to-c.h @@ -71,6 +71,20 @@ scrollbar(PyObject *src) { return SCROLLBAR_ON_SCROLLED; } +static inline ProgressBarPosition +progress_bar(PyObject *src) { + const char *q = PyUnicode_AsUTF8(src); + if (!q) return PROGRESS_BAR_BOTTOM; + switch (q[0]) { + case 'l': return PROGRESS_BAR_LEFT; + case 'r': return PROGRESS_BAR_RIGHT; + case 't': return PROGRESS_BAR_TOP; + case 'b': return PROGRESS_BAR_BOTTOM; + case 'h': return PROGRESS_BAR_HIDDEN; + } + return PROGRESS_BAR_BOTTOM; +} + static inline unsigned undercurl_style(PyObject *x) { RAII_PyObject(thick, PyUnicode_FromString("thick")); diff --git a/kitty/options/types.py b/kitty/options/types.py index e0b87943d..fa2d156f4 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -28,6 +28,7 @@ choices_for_macos_show_window_title_in = typing.Literal['all', 'menubar', 'none' choices_for_palette_generate = typing.Literal['fixed', 'semantic', 'legacy'] choices_for_placement_strategy = typing.Literal['top-left', 'top', 'top-right', 'left', 'center', 'right', 'bottom-left', 'bottom', 'bottom-right'] choices_for_pointer_shape_when_grabbed = choices_for_default_pointer_shape +choices_for_progress_bar = typing.Literal['left', 'right', 'top', 'bottom', 'hidden'] choices_for_scrollbar = typing.Literal['scrolled', 'always', 'never', 'hovered', 'scrolled-and-hovered'] choices_for_strip_trailing_spaces = typing.Literal['always', 'never', 'smart'] choices_for_tab_bar_align = typing.Literal['left', 'center', 'right'] @@ -415,6 +416,7 @@ option_names = ( 'placement_strategy', 'pointer_shape_when_dragging', 'pointer_shape_when_grabbed', + 'progress_bar', 'remember_window_position', 'remember_window_size', 'remote_control_password', @@ -622,6 +624,7 @@ class Options: placement_strategy: choices_for_placement_strategy = 'center' pointer_shape_when_dragging: tuple[str, str] = ('beam', 'crosshair') pointer_shape_when_grabbed: choices_for_pointer_shape_when_grabbed = 'arrow' + progress_bar: choices_for_progress_bar = 'top' remember_window_position: bool = False remember_window_size: bool = True repaint_delay: int = 10 diff --git a/kitty/screen.c b/kitty/screen.c index 5ae68eee5..aaebcdc82 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -189,6 +189,9 @@ screen_reset(Screen *self) { memset(self->main_key_encoding_flags, 0, sizeof(self->main_key_encoding_flags)); memset(self->alt_key_encoding_flags, 0, sizeof(self->alt_key_encoding_flags)); self->display_window_char = 0; + self->progress_state = PROGRESS_STATE_UNSET; + self->progress_percent = 0; + self->progress_indeterminate_anim_at = 0; self->prompt_settings.val = 0; self->last_graphic_char = 0; self->main_savepoint.is_valid = false; @@ -4766,6 +4769,25 @@ set_window_char(Screen *self, PyObject *a) { Py_RETURN_NONE; } +static PyObject* +set_progress(Screen *self, PyObject *a) { + unsigned int state = 0, percent = 0; + if (!PyArg_ParseTuple(a, "II", &state, &percent)) return NULL; + ProgressBarState new_state = (ProgressBarState)(state > PROGRESS_STATE_PAUSED ? PROGRESS_STATE_UNSET : state); + uint8_t new_percent = (uint8_t)(percent > 100 ? 100 : percent); + if (self->progress_state != new_state || self->progress_percent != new_percent) { + self->progress_state = new_state; + self->progress_percent = new_percent; + // Start or stop indeterminate animation + if (new_state == PROGRESS_STATE_INDETERMINATE && self->progress_indeterminate_anim_at == 0) { + self->progress_indeterminate_anim_at = monotonic(); + } else if (new_state != PROGRESS_STATE_INDETERMINATE) { + self->progress_indeterminate_anim_at = 0; + } + self->is_dirty = true; + } + Py_RETURN_NONE; +} static PyObject* is_using_alternate_linebuf(Screen *self, PyObject *a UNUSED) { @@ -6007,6 +6029,7 @@ static PyMethodDef methods[] = { MND(cursor_position, METH_VARARGS) MND(erase_last_command, METH_VARARGS) MND(set_window_char, METH_VARARGS) + MND(set_progress, METH_VARARGS) MND(set_mode, METH_VARARGS) MND(reset_mode, METH_VARARGS) MND(reset, METH_NOARGS) diff --git a/kitty/screen.h b/kitty/screen.h index d4b43d30c..139ac025c 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -169,6 +169,9 @@ typedef struct { unsigned int val; } prompt_settings; char display_window_char; + ProgressBarState progress_state; + uint8_t progress_percent; // 0-100 + monotonic_t progress_indeterminate_anim_at; // animation start time for indeterminate progress struct { char ch; uint8_t *canvas; diff --git a/kitty/shaders.c b/kitty/shaders.c index 09c4c4561..51f2c2c58 100644 --- a/kitty/shaders.c +++ b/kitty/shaders.c @@ -1077,6 +1077,190 @@ draw_scrollbar(const UIRenderData *ui) { } } +static bool +has_progress_bar(Screen *screen) { + if (OPT(progress_bar) == PROGRESS_BAR_HIDDEN) return false; + return screen && screen->progress_state != PROGRESS_STATE_UNSET; +} + +static void +draw_progress_handle(const UIRenderData *ui, color_type bar_color, float opacity, unsigned bar_radius, + GLsizei track_left, GLsizei track_top, GLsizei track_width, GLsizei track_height, + float handle_start, float handle_size, bool is_horizontal) { + // handle_start and handle_size are fractions of the track length (0..1) + // For horizontal: handle moves left-to-right; For vertical: handle moves top-to-bottom + // Use lroundf to avoid sub-pixel jitter at the leading edge + GLsizei handle_left, handle_top; + GLsizei handle_w, handle_h; + if (is_horizontal) { + GLsizei handle_start_px = (GLsizei)lroundf(handle_start * track_width); + GLsizei handle_end_px = (GLsizei)lroundf((handle_start + handle_size) * track_width); + GLsizei handle_w_px = handle_end_px - handle_start_px; + if (handle_w_px < 1) handle_w_px = 1; + handle_left = track_left + handle_start_px; + handle_top = track_top; + handle_w = handle_w_px; + handle_h = track_height; + } else { + GLsizei handle_start_px = (GLsizei)lroundf(handle_start * track_height); + GLsizei handle_end_px = (GLsizei)lroundf((handle_start + handle_size) * track_height); + GLsizei handle_h_px = handle_end_px - handle_start_px; + if (handle_h_px < 1) handle_h_px = 1; + handle_left = track_left; + handle_top = track_top + handle_start_px; + handle_w = track_width; + handle_h = handle_h_px; + } + + if (bar_radius > 0) { + bind_program(ROUNDED_RECT_PROGRAM); + color_vec4(rounded_rect_program_layout.uniforms.color, bar_color, opacity); + color_vec4(rounded_rect_program_layout.uniforms.background_color, 0, 0.0f); + + float y = (float)ui->full_framebuffer_height - (float)(handle_top + handle_h); + glUniform4f(rounded_rect_program_layout.uniforms.rect, + (float)handle_left, y, + (float)handle_w, (float)handle_h); + + float thickness = (float)(is_horizontal ? handle_h : handle_w); + glUniform2f(rounded_rect_program_layout.uniforms.params, thickness, (float)bar_radius); + + save_viewport_using_top_left_origin(handle_left, handle_top, handle_w, handle_h, ui->full_framebuffer_height); + draw_quad(true, 0); + restore_viewport(); + } else { + // Use GL coordinates within the track viewport, snapped to pixel boundaries + GLsizei track_len = is_horizontal ? track_width : track_height; + float start_snapped = (float)lroundf(handle_start * track_len) / (float)track_len; + float end_snapped = (float)lroundf((handle_start + handle_size) * track_len) / (float)track_len; + float start_gl = -1.0f + 2.0f * start_snapped; + float end_gl = -1.0f + 2.0f * end_snapped; + bind_program(TINT_PROGRAM); + set_color_uniform_with_opacity(bar_color, opacity); + if (is_horizontal) { + // edges: left, top, right, bottom + save_viewport_using_top_left_origin(track_left, track_top, track_width, track_height, ui->full_framebuffer_height); + glUniform4f(tint_program_layout.uniforms.edges, start_gl, 1.f, end_gl, -1.f); + } else { + save_viewport_using_top_left_origin(track_left, track_top, track_width, track_height, ui->full_framebuffer_height); + // For vertical: start_gl is top in GL coords (inverted y), so bottom_gl = -end_gl, top_gl = -start_gl + glUniform4f(tint_program_layout.uniforms.edges, -1.f, -start_gl, 1.f, -end_gl); + } + draw_quad(true, 0); + restore_viewport(); + } +} + +static void +draw_progress_bar(const UIRenderData *ui) { + Screen *screen = ui->screen; + Window *window = ui->window; + if (!window || !has_progress_bar(screen)) return; + + const ProgressBarPosition pos = OPT(progress_bar); + const bool is_horizontal = (pos == PROGRESS_BAR_TOP || pos == PROGRESS_BAR_BOTTOM); + color_type bar_color = scrollbar_color(screen, OPT(scrollbar_handle_color)); + color_type track_color = scrollbar_color(screen, OPT(scrollbar_track_color)); + float opacity = OPT(scrollbar_handle_opacity); + float track_opacity = OPT(scrollbar_track_hover_opacity); + GLsizei bar_thickness_px = (GLsizei)(OPT(scrollbar_width) * ui->cell_width); + GLsizei gap_px = (GLsizei)(OPT(scrollbar_gap) * ui->cell_width); + unsigned bar_radius = (unsigned)(OPT(scrollbar_radius) * ui->cell_width); + if (bar_thickness_px < 1) return; + + // Calculate window boundaries including padding + GLsizei window_left_edge = ui->screen_left - (GLsizei)window->render_data.geometry.spaces.left; + GLsizei window_top_edge = ui->screen_top - (GLsizei)window->render_data.geometry.spaces.top; + GLsizei window_width = ui->screen_width + (GLsizei)(window->render_data.geometry.spaces.left + window->render_data.geometry.spaces.right); + GLsizei window_height = ui->screen_height + (GLsizei)(window->render_data.geometry.spaces.top + window->render_data.geometry.spaces.bottom); + + // Position the track depending on the chosen edge + GLsizei track_left, track_top, track_width, track_height; + switch (pos) { + case PROGRESS_BAR_BOTTOM: + track_left = window_left_edge + gap_px; + track_width = window_width - 2 * gap_px; + track_height = bar_thickness_px; + track_top = window_top_edge + window_height - bar_thickness_px - gap_px; + break; + case PROGRESS_BAR_TOP: + track_left = window_left_edge + gap_px; + track_width = window_width - 2 * gap_px; + track_height = bar_thickness_px; + track_top = window_top_edge + gap_px; + break; + case PROGRESS_BAR_LEFT: + track_left = window_left_edge + gap_px; + track_width = bar_thickness_px; + track_top = window_top_edge + gap_px; + track_height = window_height - 2 * gap_px; + break; + case PROGRESS_BAR_RIGHT: + track_left = window_left_edge + window_width - bar_thickness_px - gap_px; + track_width = bar_thickness_px; + track_top = window_top_edge + gap_px; + track_height = window_height - 2 * gap_px; + break; + default: + return; + } + if (track_width <= 0 || track_height <= 0) return; + + // Calculate fill fraction and indeterminate animation + float fill_fraction = 0.0f; + bool is_indeterminate = false; + switch (screen->progress_state) { + case PROGRESS_STATE_SET: + case PROGRESS_STATE_PAUSED: + fill_fraction = screen->progress_percent / 100.0f; + break; + case PROGRESS_STATE_ERROR: + fill_fraction = 1.0f; + break; + case PROGRESS_STATE_INDETERMINATE: + is_indeterminate = true; + break; + default: + return; + } + + // Draw track (background) + save_viewport_using_top_left_origin(track_left, track_top, track_width, track_height, ui->full_framebuffer_height); + if (track_opacity > 0) { + bind_program(TINT_PROGRAM); + set_color_uniform_with_opacity(track_color, track_opacity); + glUniform4f(tint_program_layout.uniforms.edges, -1.f, 1.f, 1.f, -1.f); + draw_quad(true, 0); + } + restore_viewport(); + + // Draw fill or indeterminate handle + // For vertical bars, progress grows from bottom to top, so invert the fraction + if (is_indeterminate) { + // Animate a handle bouncing back and forth like a scrollbar thumb + const float handle_size = 0.15f; // 15% of track length + const monotonic_t cycle = s_double_to_monotonic_t(2.0); // 2 second full cycle (forth and back) + monotonic_t elapsed = screen->progress_indeterminate_anim_at > 0 ? monotonic() - screen->progress_indeterminate_anim_at : 0; + double t = (double)(elapsed % cycle) / (double)cycle; // 0..1 over one cycle + // Triangle wave: goes 0→1→0 over one cycle + float pos_frac = (float)(t < 0.5 ? t * 2.0 : 2.0 - t * 2.0); + if (!is_horizontal) pos_frac = 1.0f - pos_frac; // vertical: bounce bottom-to-top first + float handle_start = pos_frac * (1.0f - handle_size); + if (opacity > 0.0f) { + draw_progress_handle(ui, bar_color, opacity, bar_radius, + track_left, track_top, track_width, track_height, + handle_start, handle_size, is_horizontal); + } + } else if (fill_fraction > 0.0f && opacity > 0.0f) { + float handle_start = is_horizontal ? 0.0f : 1.0f - fill_fraction; + draw_progress_handle(ui, bar_color, opacity, bar_radius, + track_left, track_top, track_width, track_height, + handle_start, fill_fraction, is_horizontal); + } +} + + + static void draw_window_logo(const UIRenderData *ui) { struct { unsigned width, height; int left, top; } w; @@ -1122,7 +1306,7 @@ draw_window_logo(const UIRenderData *ui) { bool screen_needs_rendering_in_layers(OSWindow *os_window, Window *w, Screen *screen) { - const bool has_ui = w && ((screen->start_visual_bell_at | screen->start_drag_overlay_at) || has_scrollbar(w, screen) || has_hyperlink_target(os_window, w, screen) || has_window_number(w, screen) || w->window_logo.id); + const bool has_ui = w && ((screen->start_visual_bell_at | screen->start_drag_overlay_at) || has_scrollbar(w, screen) || has_progress_bar(screen) || has_hyperlink_target(os_window, w, screen) || has_window_number(w, screen) || w->window_logo.id); GraphicsManager *grman = screen->paused_rendering.expires_at && screen->paused_rendering.grman ? screen->paused_rendering.grman : screen->grman; return has_ui || grman_has_images(grman); } @@ -1183,6 +1367,7 @@ draw_cells_with_layers(const UIRenderData *ui, ssize_t vao_idx) { draw_visual_bell(ui); draw_drag_preview_overlay(ui); + draw_progress_bar(ui); draw_scrollbar(ui); draw_hyperlink_target(ui); draw_window_number(ui); diff --git a/kitty/state.h b/kitty/state.h index 3f2635f38..f652e626b 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -87,6 +87,7 @@ typedef struct Options { float scrollbar_width, scrollbar_radius, scrollbar_gap, scrollbar_min_handle_height, scrollbar_hitbox_expansion; float scrollbar_hover_width, scrollbar_handle_opacity, scrollbar_track_opacity, scrollbar_track_hover_opacity; color_type scrollbar_handle_color, scrollbar_track_color; + ProgressBarPosition progress_bar; float text_contrast, text_gamma_adjustment; bool text_old_gamma; @@ -121,6 +122,7 @@ typedef struct Options { char_type *url_excluded_characters; bool detect_urls; bool tab_bar_hidden; + bool tab_bar_show_new_tab_button; double font_size; struct { double outer, inner; diff --git a/kitty/window.py b/kitty/window.py index 97dd88bf9..11b732162 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -1280,6 +1280,7 @@ class Window: log_error(f'Ignoring malformed OSC 9;4 progress report: {raw_data!r}') return self.progress.update(*parts[:2]) + self.screen.set_progress(self.progress.state.value, self.progress.percent) if (tab := self.tabref()) is not None: tab.update_progress() self.clear_progress_if_needed() @@ -1291,6 +1292,7 @@ class Window: if timer_id is not None: # this is a timer callback self.clear_progress_timer = 0 if self.progress.clear_progress(): + self.screen.set_progress(0, 0) if (tab := self.tabref()) is not None: tab.update_progress() else: @@ -1469,7 +1471,11 @@ class Window: self.screen.send_escape_code_to_child(ESC_OSC, f'{code};rgb:{r:04x}/{g:04x}/{b:04x}') def on_reset(self) -> None: - pass + from .progress import ProgressState + if self.progress.state is not ProgressState.unset: + self.progress.update(0) # unset + if (tab := self.tabref()) is not None: + tab.update_progress() def notify_child_of_resize(self) -> None: pty_size = self.last_reported_pty_size