diff --git a/docs/changelog.rst b/docs/changelog.rst index ef5a4201d..367862cab 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -167,6 +167,9 @@ Detailed list of changes - Allow dragging window borders to resize kitty windows in all the different layouts, controlled by :opt:`window_drag_tolerance` (:pull:`9447`) +- Allow showing :opt:`configurable window titles ` for individual kitty + windows via a window title bar (:pull:`9450`) + - A command palette to browse and trigger all mapped and unmapped actions (:pull:`9545`) diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index bb85e9576..fbf655ab9 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -813,7 +813,7 @@ prepare_to_render_os_window(OSWindow *os_window, monotonic_t now, unsigned int * if (WD.screen->start_visual_bell_at != 0) needs_render = true; // Prepare window title bar screen data for GPU WindowRenderData *trd = &w->window_title_render_data; - if (trd->screen) { + if (trd->screen && trd->geometry.bottom > trd->geometry.top && trd->geometry.right > trd->geometry.left) { trd->screen->cursor_render_info.is_visible = false; if (send_cell_data_to_gpu(trd->vao_idx, trd->screen, os_window)) needs_render = true; } @@ -878,7 +878,7 @@ render_prepared_os_window(OSWindow *os_window, unsigned int active_window_id, co draw_cells(&WD, os_window, is_active_window, false, num_of_visible_windows == 1, w); if (WD.screen->start_visual_bell_at != 0) set_maximum_wait(ANIMATION_SAMPLE_WAIT); WindowRenderData *trd = &w->window_title_render_data; - if (trd->screen && num_visible_windows > 1 && trd->geometry.right > trd->geometry.left && trd->geometry.bottom > trd->geometry.top) + if (trd->screen && trd->geometry.right > trd->geometry.left && trd->geometry.bottom > trd->geometry.top) draw_cells(trd, os_window, i == tab->active_window, true, false, NULL); } } diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 13fec807c..a7908e2bc 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1389,6 +1389,9 @@ class Screen: def insert_characters(self, num: int) -> None: pass + def delete_characters(self, num: int) -> None: ... + def erase_characters(self, num: int) -> None: ... + def line_edge_colors(self) -> Tuple[int, int]: pass diff --git a/kitty/layout/base.py b/kitty/layout/base.py index a42b6652d..3e5d0b946 100644 --- a/kitty/layout/base.py +++ b/kitty/layout/base.py @@ -369,13 +369,12 @@ class Layout: self.update_visibility(all_windows) self.blank_rects = [] # Set show_title_bar flag on each visible window before layout - opts = get_options() - min_windows = opts.window_title_bar_min_windows - visible_groups = list(all_windows.iter_all_layoutable_groups(only_visible=True)) - num_visible = len(visible_groups) + min_windows = get_options().window_title_bar_min_windows + visible_groups = tuple(all_windows.iter_all_layoutable_groups(only_visible=True)) + show_title_bar = min_windows > 0 and len(visible_groups) >= min_windows for wg in visible_groups: for w in wg.windows: - w.show_title_bar = min_windows > 0 and num_visible >= min_windows + w.show_title_bar = show_title_bar self.do_layout(all_windows) def layout_single_window_group(self, wg: WindowGroup, add_blank_rects: bool = True) -> None: diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 61423ce74..39b1fdb2b 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -1472,6 +1472,7 @@ opt('window_title_bar', 'top', long_text=''' Control the position of the window title bar relative to the window content. Use :opt:`window_title_bar_min_windows` to control when title bars are shown. +Use :opt:`window_title_template` to format the displayed window title. ''' ) diff --git a/kitty/options/parse.py b/kitty/options/parse.py index 37c1c7572..2c541a988 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -1515,9 +1515,6 @@ class Parser: choices_for_window_title_bar = frozenset(('top', 'bottom')) - def window_title_bar_min_windows(self, val: str, ans: dict[str, typing.Any]) -> None: - ans['window_title_bar_min_windows'] = positive_int(val) - def window_title_bar_active_background(self, val: str, ans: dict[str, typing.Any]) -> None: ans['window_title_bar_active_background'] = to_color_or_none(val) @@ -1538,6 +1535,9 @@ class Parser: def window_title_bar_inactive_foreground(self, val: str, ans: dict[str, typing.Any]) -> None: ans['window_title_bar_inactive_foreground'] = to_color_or_none(val) + def window_title_bar_min_windows(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['window_title_bar_min_windows'] = positive_int(val) + def window_title_template(self, val: str, ans: dict[str, typing.Any]) -> None: ans['window_title_template'] = tab_title_template(val) diff --git a/kitty/options/types.py b/kitty/options/types.py index 5d8216cee..429a643ed 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -501,12 +501,12 @@ option_names = ( 'window_resize_step_cells', 'window_resize_step_lines', 'window_title_bar', - 'window_title_bar_min_windows', 'window_title_bar_active_background', 'window_title_bar_active_foreground', 'window_title_bar_align', 'window_title_bar_inactive_background', 'window_title_bar_inactive_foreground', + 'window_title_bar_min_windows', 'window_title_template', ) @@ -702,12 +702,12 @@ class Options: window_resize_step_cells: int = 2 window_resize_step_lines: int = 2 window_title_bar: choices_for_window_title_bar = 'top' - window_title_bar_min_windows: int = 0 window_title_bar_active_background: kitty.fast_data_types.Color | None = None window_title_bar_active_foreground: kitty.fast_data_types.Color | None = None window_title_bar_align: choices_for_window_title_bar_align = 'center' window_title_bar_inactive_background: kitty.fast_data_types.Color | None = None window_title_bar_inactive_foreground: kitty.fast_data_types.Color | None = None + window_title_bar_min_windows: int = 0 window_title_template: str = '{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.window}{progress_percent}{title}' action_alias: dict[str, str] = {} env: dict[str, str] = {} diff --git a/kitty/tabs.py b/kitty/tabs.py index 0f8d1dc70..a5b47c4b3 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -61,7 +61,6 @@ from .typing_compat import EdgeLiteral, SessionTab, SessionType, TypedDict from .utils import cmdline_for_hold, color_as_int, log_error, platform_window_id, resolved_shell, shlex_split, which from .window import CwdRequest, Watchers, Window, WindowCreationSpec, WindowDict, global_watchers from .window_list import WindowList -from .window_title_bar import WindowTitleBarManager P = ParamSpec('P') T = TypeVar('T') @@ -171,7 +170,6 @@ class Tab: # {{{ self.name = getattr(session_tab, 'name', '') self.enabled_layouts = [x.lower() for x in getattr(session_tab, 'enabled_layouts', None) or get_options().enabled_layouts] self.borders = Borders(self.os_window_id, self.id) - self.window_title_bar_manager = WindowTitleBarManager(self.os_window_id, self.id) self.windows: WindowList = WindowList(self) self._last_used_layout: str | None = None self._current_layout_name: str | None = None @@ -441,8 +439,15 @@ class Tab: # {{{ self.name = title or '' self.mark_tab_bar_dirty() + def update_window_title_bars(self) -> None: + active_group = self.windows.active_group + for wg in self.windows.iter_all_layoutable_groups(only_visible=True): + is_active = wg is active_group + for w in wg.windows: + w.update_title_bar(is_active=is_active) + def title_changed(self, window: Window) -> None: - self.window_title_bar_manager.update(self.windows) + self.update_window_title_bars() if window is self.active_window: tm = self.tab_manager_ref() if tm is not None: @@ -471,7 +476,7 @@ class Tab: # {{{ current_layout=ly, tab_bar_rects=tm.tab_bar_rects, draw_window_borders=draw_borders ) - self.window_title_bar_manager.update(self.windows) + self.update_window_title_bars() def create_layout_object(self, name: str) -> Layout: return create_layout_object_for(name, self.os_window_id, self.id) diff --git a/kitty/window.py b/kitty/window.py index 818d94de8..6efb8e510 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -659,6 +659,7 @@ class Window: creation_spec: WindowCreationSpec | None = None created_in_session_name: str = '' serialized_id: int = 0 + show_title_bar: bool = False # must be set before calling set_geometry @classmethod @contextmanager @@ -733,7 +734,6 @@ class Window: self.tabref: Callable[[], TabType | None] = weakref.ref(tab) self.destroyed = False self.geometry: WindowGeometry = WindowGeometry(0, 0, 0, 0, 0, 0) - self.show_title_bar: bool = False self._title_bar_screen: Any = None self.needs_layout = True self.is_visible_in_layout: bool = True @@ -1029,8 +1029,8 @@ class Window: # Handle title bar screen if show_tb: - from .window_title_bar import WindowTitleBarScreen if self._title_bar_screen is None: + from .window_title_bar import WindowTitleBarScreen self._title_bar_screen = WindowTitleBarScreen(self.os_window_id, cell_width, cell_height) tb_geom = WindowGeometry( left=g.left, top=tb_top, right=g.right, bottom=tb_bottom, @@ -1053,8 +1053,7 @@ class Window: update_ime_position_for_window(self.id, True) def update_title_bar(self, is_active: bool = False) -> None: - pts = self._title_bar_screen - if pts is None: + if (pts := self._title_bar_screen) is None: return from .progress import ProgressState from .window_title_bar import WindowTitleData @@ -1076,20 +1075,18 @@ class Window: needs_attention=self.needs_attention, has_activity_since_last_focus=has_activity, ) - rendered_title = pts.render(data, progress_percent) - # If template evaluates to empty string, zero title bar geometry to hide it - if not rendered_title: - set_window_title_bar_render_data( - self.os_window_id, self.tab_id, self.id, pts.screen, - 0, 0, 0, 0, - ) - else: + if pts.render(data, progress_percent): g = pts.geometry set_window_title_bar_render_data( self.os_window_id, self.tab_id, self.id, pts.screen, g.left, g.top, g.right, g.bottom, ) + else: + set_window_title_bar_render_data( + self.os_window_id, self.tab_id, self.id, pts.screen, + 0, 0, 0, 0, + ) def close(self) -> None: get_boss().mark_window_for_close(self) diff --git a/kitty/window_title_bar.py b/kitty/window_title_bar.py index 921e7a35a..7b40ebdbf 100644 --- a/kitty/window_title_bar.py +++ b/kitty/window_title_bar.py @@ -12,9 +12,9 @@ from .fast_data_types import ( get_options, ) from .rgb import color_as_sgr, color_from_int, to_color +from .tab_bar import draw_attributed_string, safe_builtins from .types import WindowGeometry, run_once -from .utils import color_as_int, log_error, sgr_sanitizer_pat -from .window_list import WindowList +from .utils import color_as_int, log_error @lru_cache @@ -30,12 +30,6 @@ def _compile_template(template: str) -> Any: _report_template_failure(template, str(e)) -safe_builtins = { - 'max': max, 'min': min, 'str': str, 'repr': repr, 'abs': abs, - 'len': len, 'chr': chr, 'ord': ord, -} - - def _resolve_color(opt_val: Any, fallback_val: Any) -> Any: if opt_val is None: return fallback_val @@ -85,17 +79,6 @@ class WindowTitleFormatter: noitalic = '\x1b[23m' -def _draw_attributed_string(title: str, screen: Screen) -> None: - if '\x1b' in title: - for x in sgr_sanitizer_pat(for_splitting=True).split(title): - if x.startswith('\x1b') and x.endswith('m'): - screen.apply_sgr(x[2:-1]) - else: - screen.draw(x) - else: - screen.draw(title) - - class WindowTitleData(NamedTuple): title: str is_active: bool @@ -199,45 +182,19 @@ class WindowTitleBarScreen: align = opts.window_title_bar_align if align == 'left': - _draw_attributed_string(title_str, s) + draw_attributed_string(title_str, s) else: # Measure the title length by drawing to cursor position 0 # and checking where the cursor ends up - _draw_attributed_string(title_str, s) + draw_attributed_string(title_str, s) title_len = s.cursor.x - s.cursor.x = 0 - s.erase_in_line(2, False) - s.cursor.fg = fg - s.cursor.bg = bg - if align == 'center': pad = max(0, (s.columns - title_len) // 2) else: # right pad = max(0, s.columns - title_len) - - for _ in range(pad): - s.draw(' ') - _draw_attributed_string(title_str, s) - - # Fill remaining cells with background - while s.cursor.x < s.columns: - s.draw(' ') - + if pad: + s.cursor.x = 0 + s.insert_characters(pad) + s.cursor.x = 0 + s.erase_characters(pad) return title_str - - -class WindowTitleBarManager: - - def __init__(self, os_window_id: int, tab_id: int): - self.os_window_id = os_window_id - self.tab_id = tab_id - - def update(self, all_windows: WindowList) -> None: - active_group = all_windows.active_group - for wg in all_windows.iter_all_layoutable_groups(only_visible=True): - is_active = wg is active_group - for w in wg.windows: - w.update_title_bar(is_active=is_active) - - def destroy(self) -> None: - pass