Merge branch 'pane-title-bar' of https://github.com/mcrmck/kitty

This commit is contained in:
Kovid Goyal 2026-03-05 08:31:50 +05:30
commit b66703ec85
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
14 changed files with 881 additions and 299 deletions

View file

@ -787,7 +787,6 @@ prepare_to_render_os_window(OSWindow *os_window, monotonic_t now, unsigned int *
set_maximum_wait(OPT(cursor_trail) - now + WD.screen->cursor->position_changed_by_client_at);
}
}
} else {
if (WD.screen->cursor_render_info.render_even_when_unfocused) {
if (collect_cursor_info(&WD.screen->cursor_render_info, w, now, os_window)) needs_render = true;
@ -812,6 +811,12 @@ prepare_to_render_os_window(OSWindow *os_window, monotonic_t now, unsigned int *
}
if (send_cell_data_to_gpu(WD.vao_idx, WD.screen, os_window)) needs_render = true;
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) {
trd->screen->cursor_render_info.is_visible = false;
if (send_cell_data_to_gpu(trd->vao_idx, trd->screen, os_window)) needs_render = true;
}
}
}
return needs_render || was_previously_rendered_with_layers != os_window->needs_layers;
@ -872,6 +877,9 @@ render_prepared_os_window(OSWindow *os_window, unsigned int active_window_id, co
if (is_active_window) active_window = w;
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)
draw_cells(trd, os_window, i == tab->active_window, true, false, NULL);
}
}
setup_os_window_for_rendering(os_window, tab, active_window, false);

View file

@ -1404,6 +1404,13 @@ def set_tab_bar_render_data(
pass
def set_window_title_bar_render_data(
os_window_id: int, tab_id: int, window_id: int, screen: Screen,
left: int, top: int, right: int, bottom: int
) -> None:
pass
def set_window_render_data(
os_window_id: int, tab_id: int, window_id: int, screen: Screen,
left: int, top: int, right: int, bottom: int,

View file

@ -368,6 +368,14 @@ def __call__(self, all_windows: WindowList) -> None:
self._set_dimensions(all_windows)
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)
for wg in visible_groups:
for w in wg.windows:
w.show_title_bar = min_windows > 0 and num_visible >= min_windows
self.do_layout(all_windows)
def layout_single_window_group(self, wg: WindowGroup, add_blank_rects: bool = True) -> None:

View file

@ -1466,6 +1466,81 @@
actually resize the window itself, but instead, the layout row/column/slot, which can result
in multiple windows getting resized.
''')
opt('window_title_bar', 'top',
choices=('top', 'bottom'),
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.
'''
)
opt('window_title_bar_min_windows', '0',
option_type='positive_int',
long_text='''
The minimum number of visible windows in a tab before window title bars
are shown. A value of :code:`0` means never show. :code:`1` means always
show. :code:`2` or more means show only when at least that many windows
are visible. Similar to :opt:`tab_bar_min_tabs` for the tab bar.
'''
)
opt('window_title_template', '"{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.window}{progress_percent}{title}"',
option_type='tab_title_template',
long_text='''
A template to render the window title bar text. Uses the same template syntax as
:opt:`tab_title_template`. Available variables include: :code:`{title}`,
:code:`{bell_symbol}`, :code:`{activity_symbol}`, :code:`{progress_percent}`,
:code:`{custom}`, :code:`{fmt}`, :code:`{is_active}`.
You can also provide a custom :code:`draw_window_title(data)` function in
:file:`window_title_bar.py` in the kitty config directory, exposed as :code:`{custom}`.
'''
)
opt('active_window_title_template', 'none',
option_type='tab_title_template',
long_text='''
Template to use for the active window title bar. If not set (the value
:code:`none`), the :opt:`window_title_template` is used.
'''
)
opt('window_title_bar_active_foreground', 'none',
option_type='to_color_or_none', ctype='color_or_none_as_int',
long_text='''
Foreground color for the active window title bar. Defaults to
the corresponding tab bar color (:code:`active_tab_foreground`) when set to :code:`none`.
'''
)
opt('window_title_bar_active_background', 'none',
option_type='to_color_or_none', ctype='color_or_none_as_int',
long_text='''
Background color for the active window title bar. Defaults to
the corresponding tab bar color (:code:`active_tab_background`) when set to :code:`none`.
'''
)
opt('window_title_bar_inactive_foreground', 'none',
option_type='to_color_or_none', ctype='color_or_none_as_int',
long_text='''
Foreground color for inactive window title bars. Defaults to
the corresponding tab bar color (:code:`inactive_tab_foreground`) when set to :code:`none`.
'''
)
opt('window_title_bar_inactive_background', 'none',
option_type='to_color_or_none', ctype='color_or_none_as_int',
long_text='''
Background color for inactive window title bars. Defaults to
the corresponding tab bar color (:code:`inactive_tab_background`) when set to :code:`none`.
'''
)
opt('window_title_bar_align', 'center',
choices=('left', 'center', 'right'),
long_text='Horizontal alignment of the text in window title bars.'
)
egr() # }}}

37
kitty/options/parse.py generated
View file

@ -48,6 +48,9 @@ def active_tab_foreground(self, val: str, ans: dict[str, typing.Any]) -> None:
def active_tab_title_template(self, val: str, ans: dict[str, typing.Any]) -> None:
ans['active_tab_title_template'] = active_tab_title_template(val)
def active_window_title_template(self, val: str, ans: dict[str, typing.Any]) -> None:
ans['active_window_title_template'] = tab_title_template(val)
def allow_cloning(self, val: str, ans: dict[str, typing.Any]) -> None:
val = val.lower()
if val not in self.choices_for_allow_cloning:
@ -1504,6 +1507,40 @@ def window_resize_step_cells(self, val: str, ans: dict[str, typing.Any]) -> None
def window_resize_step_lines(self, val: str, ans: dict[str, typing.Any]) -> None:
ans['window_resize_step_lines'] = positive_int(val)
def window_title_bar(self, val: str, ans: dict[str, typing.Any]) -> None:
val = val.lower()
if val not in self.choices_for_window_title_bar:
raise ValueError(f"The value {val} is not a valid choice for window_title_bar")
ans["window_title_bar"] = val
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)
def window_title_bar_active_foreground(self, val: str, ans: dict[str, typing.Any]) -> None:
ans['window_title_bar_active_foreground'] = to_color_or_none(val)
def window_title_bar_align(self, val: str, ans: dict[str, typing.Any]) -> None:
val = val.lower()
if val not in self.choices_for_window_title_bar_align:
raise ValueError(f"The value {val} is not a valid choice for window_title_bar_align")
ans["window_title_bar_align"] = val
choices_for_window_title_bar_align = choices_for_tab_bar_align
def window_title_bar_inactive_background(self, val: str, ans: dict[str, typing.Any]) -> None:
ans['window_title_bar_inactive_background'] = to_color_or_none(val)
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_template(self, val: str, ans: dict[str, typing.Any]) -> None:
ans['window_title_template'] = tab_title_template(val)
def x11_hide_window_decorations(self, val: str, ans: dict[str, typing.Any]) -> None:
deprecated_hide_window_decorations_aliases('x11_hide_window_decorations', val, ans)

View file

@ -993,6 +993,58 @@ convert_from_opts_window_drag_tolerance(PyObject *py_opts, Options *opts) {
Py_DECREF(ret);
}
static void
convert_from_python_window_title_bar_active_foreground(PyObject *val, Options *opts) {
opts->window_title_bar_active_foreground = color_or_none_as_int(val);
}
static void
convert_from_opts_window_title_bar_active_foreground(PyObject *py_opts, Options *opts) {
PyObject *ret = PyObject_GetAttrString(py_opts, "window_title_bar_active_foreground");
if (ret == NULL) return;
convert_from_python_window_title_bar_active_foreground(ret, opts);
Py_DECREF(ret);
}
static void
convert_from_python_window_title_bar_active_background(PyObject *val, Options *opts) {
opts->window_title_bar_active_background = color_or_none_as_int(val);
}
static void
convert_from_opts_window_title_bar_active_background(PyObject *py_opts, Options *opts) {
PyObject *ret = PyObject_GetAttrString(py_opts, "window_title_bar_active_background");
if (ret == NULL) return;
convert_from_python_window_title_bar_active_background(ret, opts);
Py_DECREF(ret);
}
static void
convert_from_python_window_title_bar_inactive_foreground(PyObject *val, Options *opts) {
opts->window_title_bar_inactive_foreground = color_or_none_as_int(val);
}
static void
convert_from_opts_window_title_bar_inactive_foreground(PyObject *py_opts, Options *opts) {
PyObject *ret = PyObject_GetAttrString(py_opts, "window_title_bar_inactive_foreground");
if (ret == NULL) return;
convert_from_python_window_title_bar_inactive_foreground(ret, opts);
Py_DECREF(ret);
}
static void
convert_from_python_window_title_bar_inactive_background(PyObject *val, Options *opts) {
opts->window_title_bar_inactive_background = color_or_none_as_int(val);
}
static void
convert_from_opts_window_title_bar_inactive_background(PyObject *py_opts, Options *opts) {
PyObject *ret = PyObject_GetAttrString(py_opts, "window_title_bar_inactive_background");
if (ret == NULL) return;
convert_from_python_window_title_bar_inactive_background(ret, opts);
Py_DECREF(ret);
}
static void
convert_from_python_tab_bar_edge(PyObject *val, Options *opts) {
opts->tab_bar_edge = PyLong_AsLong(val);
@ -1550,6 +1602,14 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) {
if (PyErr_Occurred()) return false;
convert_from_opts_window_drag_tolerance(py_opts, opts);
if (PyErr_Occurred()) return false;
convert_from_opts_window_title_bar_active_foreground(py_opts, opts);
if (PyErr_Occurred()) return false;
convert_from_opts_window_title_bar_active_background(py_opts, opts);
if (PyErr_Occurred()) return false;
convert_from_opts_window_title_bar_inactive_foreground(py_opts, opts);
if (PyErr_Occurred()) return false;
convert_from_opts_window_title_bar_inactive_background(py_opts, opts);
if (PyErr_Occurred()) return false;
convert_from_opts_tab_bar_edge(py_opts, opts);
if (PyErr_Occurred()) return false;
convert_from_opts_tab_bar_margin_height(py_opts, opts);

24
kitty/options/types.py generated
View file

@ -37,6 +37,8 @@
choices_for_undercurl_style = typing.Literal['thin-sparse', 'thin-dense', 'thick-sparse', 'thick-dense']
choices_for_underline_hyperlinks = typing.Literal['hover', 'always', 'never']
choices_for_window_logo_position = choices_for_placement_strategy
choices_for_window_title_bar = typing.Literal['top', 'bottom']
choices_for_window_title_bar_align = choices_for_tab_bar_align
option_names = (
'action_alias',
@ -45,6 +47,7 @@
'active_tab_font_style',
'active_tab_foreground',
'active_tab_title_template',
'active_window_title_template',
'allow_cloning',
'allow_hyperlinks',
'allow_remote_control',
@ -497,6 +500,14 @@
'window_padding_width',
'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_template',
)
@ -506,6 +517,7 @@ class Options:
active_tab_font_style: tuple[bool, bool] = (True, True)
active_tab_foreground: Color = Color(0, 0, 0)
active_tab_title_template: str | None = None
active_window_title_template: str = 'none'
allow_cloning: choices_for_allow_cloning = 'ask'
allow_hyperlinks: int = 1
allow_remote_control: choices_for_allow_remote_control = 'no'
@ -689,6 +701,14 @@ class Options:
window_padding_width: FloatEdges = FloatEdges(left=0, top=0, right=0, bottom=0)
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_template: str = '{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.window}{progress_percent}{title}'
action_alias: dict[str, str] = {}
env: dict[str, str] = {}
exe_search_path: dict[str, str] = {}
@ -1111,6 +1131,10 @@ def __setattr__(self, key: str, val: typing.Any) -> typing.Any:
'cursor_trail_color',
'visual_bell_color',
'active_border_color',
'window_title_bar_active_foreground',
'window_title_bar_active_background',
'window_title_bar_inactive_foreground',
'window_title_bar_inactive_background',
'tab_bar_background',
'tab_bar_margin_color',
'selection_foreground',

View file

@ -255,12 +255,16 @@ add_tab(id_type os_window_id) {
static void
create_gpu_resources_for_window(Window *w) {
w->render_data.vao_idx = create_cell_vao();
w->window_title_render_data.vao_idx = create_cell_vao();
}
static void
release_gpu_resources_for_window(Window *w) {
if (w->render_data.vao_idx > -1) remove_vao(w->render_data.vao_idx);
w->render_data.vao_idx = -1;
if (w->window_title_render_data.vao_idx > -1) remove_vao(w->window_title_render_data.vao_idx);
w->window_title_render_data.vao_idx = -1;
Py_CLEAR(w->window_title_render_data.screen);
}
static bool
@ -860,6 +864,17 @@ PYWRAP1(set_tab_bar_render_data) {
Py_RETURN_NONE;
}
PYWRAP1(set_window_title_bar_render_data) {
WindowGeometry g;
id_type os_window_id, tab_id, window_id;
Screen *screen;
PA("KKKOIIII", &os_window_id, &tab_id, &window_id, &screen, &g.left, &g.top, &g.right, &g.bottom);
WITH_WINDOW(os_window_id, tab_id, window_id)
init_window_render_data(&window->window_title_render_data, g, screen);
END_WITH_WINDOW
Py_RETURN_NONE;
}
static PyTypeObject RegionType;
static PyStructSequence_Field region_fields[] = {
{"left", ""}, {"top", ""}, {"right", ""}, {"bottom", ""}, {"width", ""}, {"height", ""}, {NULL, NULL}
@ -1597,6 +1612,7 @@ static PyMethodDef module_methods[] = {
MW(reorder_tabs, METH_VARARGS),
MW(set_borders_rects, METH_VARARGS),
MW(set_tab_bar_render_data, METH_VARARGS),
MW(set_window_title_bar_render_data, METH_VARARGS),
MW(set_window_render_data, METH_VARARGS),
MW(set_window_padding, METH_VARARGS),
MW(viewport_for_window, METH_VARARGS),

View file

@ -62,7 +62,8 @@ typedef struct Options {
bool scrollback_fill_enlarged_window;
char_type *select_by_word_characters;
char_type *select_by_word_characters_forward;
color_type url_color, background, foreground, active_border_color, inactive_border_color, bell_border_color, tab_bar_background, tab_bar_margin_color;
color_type url_color, background, foreground, active_border_color, inactive_border_color, bell_border_color, tab_bar_background, tab_bar_margin_color,
window_title_bar_active_foreground, window_title_bar_active_background, window_title_bar_inactive_foreground, window_title_bar_inactive_background;
monotonic_t repaint_delay, input_delay;
bool focus_follows_mouse;
unsigned int hide_window_decorations;
@ -208,6 +209,7 @@ typedef struct Window {
bool visible;
PyObject *title;
WindowRenderData render_data;
WindowRenderData window_title_render_data;
WindowLogoRenderData window_logo;
MousePosition mouse_pos;
struct {

View file

@ -61,6 +61,7 @@
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')
@ -170,6 +171,7 @@ def __init__(
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
@ -440,6 +442,7 @@ def set_title(self, title: str) -> None:
self.mark_tab_bar_dirty()
def title_changed(self, window: Window) -> None:
self.window_title_bar_manager.update(self.windows)
if window is self.active_window:
tm = self.tab_manager_ref()
if tm is not None:
@ -468,6 +471,7 @@ def relayout_borders(self) -> None:
current_layout=ly, tab_bar_rects=tm.tab_bar_rects,
draw_window_borders=draw_borders
)
self.window_title_bar_manager.update(self.windows)
def create_layout_object(self, name: str) -> Layout:
return create_layout_object_for(name, self.os_window_id, self.id)

View file

@ -84,6 +84,7 @@
set_window_logo,
set_window_padding,
set_window_render_data,
set_window_title_bar_render_data,
update_ime_position_for_window,
update_pointer_shape,
update_window_title,
@ -732,6 +733,8 @@ def __init__(
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
self.child = child
@ -977,13 +980,36 @@ def resize_child(self, current_pty_size: tuple[int, int, int, int]) -> bool:
def set_geometry(self, new_geometry: WindowGeometry) -> None:
if self.destroyed:
return
if self.needs_layout or new_geometry.xnum != self.screen.columns or new_geometry.ynum != self.screen.lines:
self.screen.resize(max(0, new_geometry.ynum), max(0, new_geometry.xnum))
# Determine if we need a title bar and compute adjusted dimensions
opts = get_options()
position = opts.window_title_bar
show_tb = self.show_title_bar and new_geometry.ynum > 1
if show_tb:
render_ynum = new_geometry.ynum - 1
cell_width, cell_height = cell_size_for_window(self.os_window_id)
if position == 'top':
render_top = new_geometry.top + cell_height
render_bottom = new_geometry.bottom
tb_top = new_geometry.top
tb_bottom = new_geometry.top + cell_height
else:
render_top = new_geometry.top
render_bottom = new_geometry.bottom - cell_height
tb_top = new_geometry.bottom - cell_height
tb_bottom = new_geometry.bottom
else:
render_ynum = new_geometry.ynum
render_top = new_geometry.top
render_bottom = new_geometry.bottom
if self.needs_layout or new_geometry.xnum != self.screen.columns or render_ynum != self.screen.lines:
self.screen.resize(max(0, render_ynum), max(0, new_geometry.xnum))
self.needs_layout = False
call_watchers(weakref.ref(self), 'on_resize', {'old_geometry': self.geometry, 'new_geometry': new_geometry})
current_pty_size = (
self.screen.lines, self.screen.columns,
max(0, new_geometry.right - new_geometry.left), max(0, new_geometry.bottom - new_geometry.top))
max(0, new_geometry.right - new_geometry.left), max(0, render_bottom - render_top))
update_ime_position = False
if current_pty_size != self.last_reported_pty_size:
if self._pause_resize_notifications_to_child is None:
@ -993,14 +1019,78 @@ def set_geometry(self, new_geometry: WindowGeometry) -> None:
else:
mark_os_window_dirty(self.os_window_id)
# Store original geometry for borders/padding calculations
self.geometry = g = new_geometry
# Set C-side render data with adjusted top/bottom for content area
set_window_render_data(self.os_window_id, self.tab_id, self.id, self.screen,
g.left, g.top, g.right, g.bottom,
g.left, render_top, g.right, render_bottom,
g.spaces.left, g.spaces.top, g.spaces.right, g.spaces.bottom)
self.update_effective_padding()
# Handle title bar screen
if show_tb:
from .window_title_bar import WindowTitleBarScreen
if self._title_bar_screen is None:
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,
xnum=0, ynum=1,
)
self._title_bar_screen.layout(tb_geom)
set_window_title_bar_render_data(
self.os_window_id, self.tab_id, self.id, self._title_bar_screen.screen,
tb_geom.left, tb_geom.top, tb_geom.right, tb_geom.bottom,
)
elif self._title_bar_screen is not None:
# Clear title bar render data
set_window_title_bar_render_data(
self.os_window_id, self.tab_id, self.id, self._title_bar_screen.screen,
0, 0, 0, 0,
)
self._title_bar_screen = None
if update_ime_position:
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:
return
from .progress import ProgressState
from .window_title_bar import WindowTitleData
progress_percent = ''
if self.progress.state is not ProgressState.unset:
if self.progress.state is ProgressState.indeterminate:
progress_percent = '[…] '
elif self.progress.percent > 0:
progress_percent = f'[{self.progress.percent}%] '
has_activity = self.has_activity_since_last_focus
data = WindowTitleData(
title=self.title or '',
is_active=is_active,
window_id=self.id,
tab_id=self.tab_id,
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:
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,
)
def close(self) -> None:
get_boss().mark_window_for_close(self)

243
kitty/window_title_bar.py Normal file
View file

@ -0,0 +1,243 @@
#!/usr/bin/env python
# License: GPL v3 Copyright: 2024, kitty contributors
import os
from functools import lru_cache
from typing import Any, NamedTuple
from .constants import config_dir
from .fast_data_types import (
DECAWM,
Screen,
get_options,
)
from .rgb import color_as_sgr, color_from_int, to_color
from .types import WindowGeometry, run_once
from .utils import color_as_int, log_error, sgr_sanitizer_pat
from .window_list import WindowList
@lru_cache
def _report_template_failure(template: str, e: str) -> None:
log_error(f'Invalid window title template: "{template}" with error: {e}')
@lru_cache
def _compile_template(template: str) -> Any:
try:
return compile('f"""' + template + '"""', '<window_title_template>', 'eval')
except Exception as e:
_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
return opt_val
class WindowTitleColorFormatter:
is_active: bool = False
def __init__(self, which: str):
self.which = which
def __getattr__(self, name: str) -> str:
q = name
if q == 'default':
ans = '9'
elif q == 'window':
opts = get_options()
if self.is_active:
fg_color = _resolve_color(opts.window_title_bar_active_foreground, opts.active_tab_foreground)
bg_color = _resolve_color(opts.window_title_bar_active_background, opts.active_tab_background)
col = color_from_int(color_as_int(fg_color if self.which == '3' else bg_color))
else:
fg_color = _resolve_color(opts.window_title_bar_inactive_foreground, opts.inactive_tab_foreground)
bg_color = _resolve_color(opts.window_title_bar_inactive_background, opts.inactive_tab_background)
col = color_from_int(color_as_int(fg_color if self.which == '3' else bg_color))
ans = f'8{color_as_sgr(col)}'
elif q.startswith('color'):
ans = f'8:5:{int(q[5:])}'
else:
if name.startswith('_'):
q = f'#{name[1:]}'
c = to_color(q)
if c is None:
raise AttributeError(f'{name} is not a valid color')
ans = f'8{color_as_sgr(c)}'
return f'\x1b[{self.which}{ans}m'
class WindowTitleFormatter:
reset = '\x1b[0m'
fg = WindowTitleColorFormatter('3')
bg = WindowTitleColorFormatter('4')
bold = '\x1b[1m'
nobold = '\x1b[22m'
italic = '\x1b[3m'
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
window_id: int
tab_id: int
needs_attention: bool = False
has_activity_since_last_focus: bool = False
@run_once
def load_custom_window_title_bar_module() -> dict[str, Any]:
import runpy
import traceback
try:
return runpy.run_path(os.path.join(config_dir, 'window_title_bar.py'))
except FileNotFoundError:
return {}
except Exception as e:
traceback.print_exc()
log_error(f'Failed to load custom window_title_bar.py module with error: {e}')
return {}
def _get_custom_draw_result(data: WindowTitleData) -> str | None:
m = load_custom_window_title_bar_module()
func = m.get('draw_window_title')
if func is None:
return None
try:
return str(func(data))
except Exception as e:
log_error(f'Custom draw_window_title function failed with error: {e}')
return None
def clear_caches() -> None:
load_custom_window_title_bar_module.clear_cached()
class WindowTitleBarScreen:
def __init__(self, os_window_id: int, cell_width: int, cell_height: int):
self.os_window_id = os_window_id
self.cell_width = cell_width
self.screen = Screen(None, 1, 10, 0, cell_width, cell_height)
self.screen.reset_mode(DECAWM)
def layout(self, geometry: WindowGeometry) -> None:
ncells = max(4, (geometry.right - geometry.left) // self.cell_width)
self.screen.resize(1, ncells)
self.geometry = geometry
def render(self, data: WindowTitleData, progress_percent: str) -> str:
opts = get_options()
s = self.screen
s.cursor.x = 0
s.erase_in_line(2, False)
is_active = data.is_active
if is_active:
fg_color = _resolve_color(opts.window_title_bar_active_foreground, opts.active_tab_foreground)
bg_color = _resolve_color(opts.window_title_bar_active_background, opts.active_tab_background)
else:
fg_color = _resolve_color(opts.window_title_bar_inactive_foreground, opts.inactive_tab_foreground)
bg_color = _resolve_color(opts.window_title_bar_inactive_background, opts.inactive_tab_background)
s.color_profile.default_fg = fg_color
s.color_profile.default_bg = bg_color
fg = (color_as_int(fg_color) << 8) | 2
bg = (color_as_int(bg_color) << 8) | 2
s.cursor.fg = fg
s.cursor.bg = bg
template = opts.window_title_template
if is_active and opts.active_window_title_template and opts.active_window_title_template != 'none':
template = opts.active_window_title_template
WindowTitleColorFormatter.is_active = is_active
bell_symbol = opts.bell_on_tab if data.needs_attention else ''
activity_symbol = opts.tab_activity_symbol if data.has_activity_since_last_focus else ''
custom_result = _get_custom_draw_result(data)
eval_locals = {
'title': data.title,
'is_active': is_active,
'fmt': WindowTitleFormatter,
'bell_symbol': bell_symbol,
'activity_symbol': activity_symbol,
'progress_percent': progress_percent,
'custom': custom_result or '',
}
try:
title = eval(_compile_template(template), {'__builtins__': safe_builtins}, eval_locals)
except Exception as e:
_report_template_failure(template, str(e))
title = data.title
title_str = str(title)
align = opts.window_title_bar_align
if align == 'left':
_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)
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(' ')
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

View file

@ -15,15 +15,19 @@ import (
var nullable_colors = map[string]bool{
// generated by gen-config.py do not edit
// NULLABLE_COLORS_START
"active_border_color": true,
"cursor": true,
"cursor_text_color": true,
"cursor_trail_color": true,
"selection_background": true,
"selection_foreground": true,
"tab_bar_background": true,
"tab_bar_margin_color": true,
"visual_bell_color": true,
"active_border_color": true,
"cursor": true,
"cursor_text_color": true,
"cursor_trail_color": true,
"selection_background": true,
"selection_foreground": true,
"tab_bar_background": true,
"tab_bar_margin_color": true,
"visual_bell_color": true,
"window_title_bar_active_background": true,
"window_title_bar_active_foreground": true,
"window_title_bar_inactive_background": true,
"window_title_bar_inactive_foreground": true,
// NULLABLE_COLORS_END
}

View file

@ -35,290 +35,294 @@ var _ = fmt.Print
var AllColorSettingNames = map[string]bool{ // {{{
// generated by gen-config.py do not edit
// ALL_COLORS_START
"active_border_color": true,
"active_tab_background": true,
"active_tab_foreground": true,
"background": true,
"bell_border_color": true,
"color0": true,
"color1": true,
"color10": true,
"color100": true,
"color101": true,
"color102": true,
"color103": true,
"color104": true,
"color105": true,
"color106": true,
"color107": true,
"color108": true,
"color109": true,
"color11": true,
"color110": true,
"color111": true,
"color112": true,
"color113": true,
"color114": true,
"color115": true,
"color116": true,
"color117": true,
"color118": true,
"color119": true,
"color12": true,
"color120": true,
"color121": true,
"color122": true,
"color123": true,
"color124": true,
"color125": true,
"color126": true,
"color127": true,
"color128": true,
"color129": true,
"color13": true,
"color130": true,
"color131": true,
"color132": true,
"color133": true,
"color134": true,
"color135": true,
"color136": true,
"color137": true,
"color138": true,
"color139": true,
"color14": true,
"color140": true,
"color141": true,
"color142": true,
"color143": true,
"color144": true,
"color145": true,
"color146": true,
"color147": true,
"color148": true,
"color149": true,
"color15": true,
"color150": true,
"color151": true,
"color152": true,
"color153": true,
"color154": true,
"color155": true,
"color156": true,
"color157": true,
"color158": true,
"color159": true,
"color16": true,
"color160": true,
"color161": true,
"color162": true,
"color163": true,
"color164": true,
"color165": true,
"color166": true,
"color167": true,
"color168": true,
"color169": true,
"color17": true,
"color170": true,
"color171": true,
"color172": true,
"color173": true,
"color174": true,
"color175": true,
"color176": true,
"color177": true,
"color178": true,
"color179": true,
"color18": true,
"color180": true,
"color181": true,
"color182": true,
"color183": true,
"color184": true,
"color185": true,
"color186": true,
"color187": true,
"color188": true,
"color189": true,
"color19": true,
"color190": true,
"color191": true,
"color192": true,
"color193": true,
"color194": true,
"color195": true,
"color196": true,
"color197": true,
"color198": true,
"color199": true,
"color2": true,
"color20": true,
"color200": true,
"color201": true,
"color202": true,
"color203": true,
"color204": true,
"color205": true,
"color206": true,
"color207": true,
"color208": true,
"color209": true,
"color21": true,
"color210": true,
"color211": true,
"color212": true,
"color213": true,
"color214": true,
"color215": true,
"color216": true,
"color217": true,
"color218": true,
"color219": true,
"color22": true,
"color220": true,
"color221": true,
"color222": true,
"color223": true,
"color224": true,
"color225": true,
"color226": true,
"color227": true,
"color228": true,
"color229": true,
"color23": true,
"color230": true,
"color231": true,
"color232": true,
"color233": true,
"color234": true,
"color235": true,
"color236": true,
"color237": true,
"color238": true,
"color239": true,
"color24": true,
"color240": true,
"color241": true,
"color242": true,
"color243": true,
"color244": true,
"color245": true,
"color246": true,
"color247": true,
"color248": true,
"color249": true,
"color25": true,
"color250": true,
"color251": true,
"color252": true,
"color253": true,
"color254": true,
"color255": true,
"color26": true,
"color27": true,
"color28": true,
"color29": true,
"color3": true,
"color30": true,
"color31": true,
"color32": true,
"color33": true,
"color34": true,
"color35": true,
"color36": true,
"color37": true,
"color38": true,
"color39": true,
"color4": true,
"color40": true,
"color41": true,
"color42": true,
"color43": true,
"color44": true,
"color45": true,
"color46": true,
"color47": true,
"color48": true,
"color49": true,
"color5": true,
"color50": true,
"color51": true,
"color52": true,
"color53": true,
"color54": true,
"color55": true,
"color56": true,
"color57": true,
"color58": true,
"color59": true,
"color6": true,
"color60": true,
"color61": true,
"color62": true,
"color63": true,
"color64": true,
"color65": true,
"color66": true,
"color67": true,
"color68": true,
"color69": true,
"color7": true,
"color70": true,
"color71": true,
"color72": true,
"color73": true,
"color74": true,
"color75": true,
"color76": true,
"color77": true,
"color78": true,
"color79": true,
"color8": true,
"color80": true,
"color81": true,
"color82": true,
"color83": true,
"color84": true,
"color85": true,
"color86": true,
"color87": true,
"color88": true,
"color89": true,
"color9": true,
"color90": true,
"color91": true,
"color92": true,
"color93": true,
"color94": true,
"color95": true,
"color96": true,
"color97": true,
"color98": true,
"color99": true,
"cursor": true,
"cursor_text_color": true,
"cursor_trail_color": true,
"foreground": true,
"inactive_border_color": true,
"inactive_tab_background": true,
"inactive_tab_foreground": true,
"macos_titlebar_color": true,
"mark1_background": true,
"mark1_foreground": true,
"mark2_background": true,
"mark2_foreground": true,
"mark3_background": true,
"mark3_foreground": true,
"scrollbar_handle_color": true,
"scrollbar_track_color": true,
"selection_background": true,
"selection_foreground": true,
"tab_bar_background": true,
"tab_bar_margin_color": true,
"url_color": true,
"visual_bell_color": true,
"wayland_titlebar_color": true, // ALL_COLORS_END
"active_border_color": true,
"active_tab_background": true,
"active_tab_foreground": true,
"background": true,
"bell_border_color": true,
"color0": true,
"color1": true,
"color10": true,
"color100": true,
"color101": true,
"color102": true,
"color103": true,
"color104": true,
"color105": true,
"color106": true,
"color107": true,
"color108": true,
"color109": true,
"color11": true,
"color110": true,
"color111": true,
"color112": true,
"color113": true,
"color114": true,
"color115": true,
"color116": true,
"color117": true,
"color118": true,
"color119": true,
"color12": true,
"color120": true,
"color121": true,
"color122": true,
"color123": true,
"color124": true,
"color125": true,
"color126": true,
"color127": true,
"color128": true,
"color129": true,
"color13": true,
"color130": true,
"color131": true,
"color132": true,
"color133": true,
"color134": true,
"color135": true,
"color136": true,
"color137": true,
"color138": true,
"color139": true,
"color14": true,
"color140": true,
"color141": true,
"color142": true,
"color143": true,
"color144": true,
"color145": true,
"color146": true,
"color147": true,
"color148": true,
"color149": true,
"color15": true,
"color150": true,
"color151": true,
"color152": true,
"color153": true,
"color154": true,
"color155": true,
"color156": true,
"color157": true,
"color158": true,
"color159": true,
"color16": true,
"color160": true,
"color161": true,
"color162": true,
"color163": true,
"color164": true,
"color165": true,
"color166": true,
"color167": true,
"color168": true,
"color169": true,
"color17": true,
"color170": true,
"color171": true,
"color172": true,
"color173": true,
"color174": true,
"color175": true,
"color176": true,
"color177": true,
"color178": true,
"color179": true,
"color18": true,
"color180": true,
"color181": true,
"color182": true,
"color183": true,
"color184": true,
"color185": true,
"color186": true,
"color187": true,
"color188": true,
"color189": true,
"color19": true,
"color190": true,
"color191": true,
"color192": true,
"color193": true,
"color194": true,
"color195": true,
"color196": true,
"color197": true,
"color198": true,
"color199": true,
"color2": true,
"color20": true,
"color200": true,
"color201": true,
"color202": true,
"color203": true,
"color204": true,
"color205": true,
"color206": true,
"color207": true,
"color208": true,
"color209": true,
"color21": true,
"color210": true,
"color211": true,
"color212": true,
"color213": true,
"color214": true,
"color215": true,
"color216": true,
"color217": true,
"color218": true,
"color219": true,
"color22": true,
"color220": true,
"color221": true,
"color222": true,
"color223": true,
"color224": true,
"color225": true,
"color226": true,
"color227": true,
"color228": true,
"color229": true,
"color23": true,
"color230": true,
"color231": true,
"color232": true,
"color233": true,
"color234": true,
"color235": true,
"color236": true,
"color237": true,
"color238": true,
"color239": true,
"color24": true,
"color240": true,
"color241": true,
"color242": true,
"color243": true,
"color244": true,
"color245": true,
"color246": true,
"color247": true,
"color248": true,
"color249": true,
"color25": true,
"color250": true,
"color251": true,
"color252": true,
"color253": true,
"color254": true,
"color255": true,
"color26": true,
"color27": true,
"color28": true,
"color29": true,
"color3": true,
"color30": true,
"color31": true,
"color32": true,
"color33": true,
"color34": true,
"color35": true,
"color36": true,
"color37": true,
"color38": true,
"color39": true,
"color4": true,
"color40": true,
"color41": true,
"color42": true,
"color43": true,
"color44": true,
"color45": true,
"color46": true,
"color47": true,
"color48": true,
"color49": true,
"color5": true,
"color50": true,
"color51": true,
"color52": true,
"color53": true,
"color54": true,
"color55": true,
"color56": true,
"color57": true,
"color58": true,
"color59": true,
"color6": true,
"color60": true,
"color61": true,
"color62": true,
"color63": true,
"color64": true,
"color65": true,
"color66": true,
"color67": true,
"color68": true,
"color69": true,
"color7": true,
"color70": true,
"color71": true,
"color72": true,
"color73": true,
"color74": true,
"color75": true,
"color76": true,
"color77": true,
"color78": true,
"color79": true,
"color8": true,
"color80": true,
"color81": true,
"color82": true,
"color83": true,
"color84": true,
"color85": true,
"color86": true,
"color87": true,
"color88": true,
"color89": true,
"color9": true,
"color90": true,
"color91": true,
"color92": true,
"color93": true,
"color94": true,
"color95": true,
"color96": true,
"color97": true,
"color98": true,
"color99": true,
"cursor": true,
"cursor_text_color": true,
"cursor_trail_color": true,
"foreground": true,
"inactive_border_color": true,
"inactive_tab_background": true,
"inactive_tab_foreground": true,
"macos_titlebar_color": true,
"mark1_background": true,
"mark1_foreground": true,
"mark2_background": true,
"mark2_foreground": true,
"mark3_background": true,
"mark3_foreground": true,
"scrollbar_handle_color": true,
"scrollbar_track_color": true,
"selection_background": true,
"selection_foreground": true,
"tab_bar_background": true,
"tab_bar_margin_color": true,
"url_color": true,
"visual_bell_color": true,
"wayland_titlebar_color": true,
"window_title_bar_active_background": true,
"window_title_bar_active_foreground": true,
"window_title_bar_inactive_background": true,
"window_title_bar_inactive_foreground": true, // ALL_COLORS_END
} // }}}
type JSONMetadata struct {