diff --git a/docs/changelog.rst b/docs/changelog.rst index b7e3a0c26..67b37a707 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -27,6 +27,10 @@ To update |kitty|, :doc:`follow the instructions `. - Allow specifying watchers in session files and via a command line argument (:iss:`2933`) +- Add a setting :opt:`tab_activity_symbol` to show a symbol in the tab title + if one of the windows has some activity after it was last focused + (:iss:`2515`) + - macOS: Switch to using the User Notifications framework for notifications. The current notifications framework has been deprecated in Big Sur. The new framework only allows notifications from signed and notarized applications, diff --git a/kitty/config_data.py b/kitty/config_data.py index b569c7f47..7e58a9111 100644 --- a/kitty/config_data.py +++ b/kitty/config_data.py @@ -883,6 +883,17 @@ o('tab_separator', '"{}"'.format(default_tab_separator), option_type=tab_separat The separator between tabs in the tab bar when using :code:`separator` as the :opt:`tab_bar_style`.''')) +def tab_activity_symbol(x: str) -> Optional[str]: + if x == 'none': + return None + return x or None + + +o('tab_activity_symbol', 'none', option_type=tab_activity_symbol, long_text=_(''' +Some text or a unicode symbol to show on the tab if a window in the tab that does +not have focus has some activity.''')) + + def tab_title_template(x: str) -> str: if x: for q in '\'"': diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 4d47c3d12..1eeb1bd20 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1069,6 +1069,9 @@ class Screen: def has_focus(self) -> bool: pass + def has_activity_since_last_focus(self) -> bool: + pass + def set_tab_bar_render_data( os_window_id: int, xstart: float, ystart: float, dx: float, dy: float, diff --git a/kitty/screen.c b/kitty/screen.c index 28d2b011e..06951230a 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -444,6 +444,9 @@ draw_combining_char(Screen *self, char_type ch) { void screen_draw(Screen *self, uint32_t och) { if (is_ignored_char(och)) return; + if (!self->has_activity_since_last_focus && !self->has_focus) { + self->has_activity_since_last_focus = true; + } uint32_t ch = och < 256 ? self->g_charset[och] : och; bool is_cc = is_combining_char(ch); if (UNLIKELY(is_cc)) { @@ -2531,6 +2534,7 @@ focus_changed(Screen *self, PyObject *has_focus_) { bool has_focus = PyObject_IsTrue(has_focus_) ? true : false; if (has_focus != previous) { self->has_focus = has_focus; + if (has_focus) self->has_activity_since_last_focus = false; if (self->modes.mFOCUS_TRACKING) write_escape_code_to_child(self, CSI, has_focus ? "I" : "O"); Py_RETURN_TRUE; } @@ -2543,6 +2547,11 @@ has_focus(Screen *self, PyObject *args UNUSED) { Py_RETURN_FALSE; } +static PyObject* +has_activity_since_last_focus(Screen *self, PyObject *args UNUSED) { + if (self->has_activity_since_last_focus) Py_RETURN_TRUE; + Py_RETURN_FALSE; +} WRAP2(cursor_position, 1, 1) @@ -2628,6 +2637,7 @@ static PyMethodDef methods[] = { MND(paste_bytes, METH_O) MND(focus_changed, METH_O) MND(has_focus, METH_NOARGS) + MND(has_activity_since_last_focus, METH_NOARGS) MND(copy_colors_from, METH_O) MND(set_marker, METH_VARARGS) MND(marked_cells, METH_NOARGS) diff --git a/kitty/screen.h b/kitty/screen.h index 6ed4fdd9a..8488ada3e 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -122,6 +122,7 @@ typedef struct { DisableLigature disable_ligatures; PyObject *marker; bool has_focus; + bool has_activity_since_last_focus; } Screen; diff --git a/kitty/tab_bar.py b/kitty/tab_bar.py index 6d41798e9..82f36815a 100644 --- a/kitty/tab_bar.py +++ b/kitty/tab_bar.py @@ -24,6 +24,7 @@ class TabBarData(NamedTuple): needs_attention: bool num_windows: int layout_name: str + has_activity_since_last_focus: bool class DrawData(NamedTuple): @@ -40,6 +41,7 @@ class DrawData(NamedTuple): default_bg: Color title_template: str active_title_template: Optional[str] + tab_activity_symbol: Optional[str] def as_rgb(x: int) -> int: @@ -65,6 +67,12 @@ def draw_title(draw_data: DrawData, screen: Screen, tab: TabBarData, index: int) screen.cursor.fg = draw_data.bell_fg screen.draw('🔔 ') screen.cursor.fg = fg + if tab.has_activity_since_last_focus and draw_data.tab_activity_symbol: + fg = screen.cursor.fg + screen.cursor.fg = draw_data.bell_fg + screen.draw(draw_data.tab_activity_symbol) + screen.cursor.fg = fg + template = draw_data.title_template if tab.is_active and draw_data.active_title_template is not None: template = draw_data.active_title_template @@ -226,7 +234,8 @@ class TabBar: self.opts.tab_fade, self.opts.active_tab_foreground, self.opts.active_tab_background, self.opts.inactive_tab_foreground, self.opts.inactive_tab_background, self.opts.tab_bar_background or self.opts.background, self.opts.tab_title_template, - self.opts.active_tab_title_template + self.opts.active_tab_title_template, + self.opts.tab_activity_symbol ) if self.opts.tab_bar_style == 'separator': self.draw_func = draw_tab_with_separator diff --git a/kitty/tabs.py b/kitty/tabs.py index f3d1eb9b1..a05d6af33 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -736,13 +736,16 @@ class TabManager: # {{{ for t in self.tabs: title = (t.name or t.title or appname).strip() needs_attention = False + has_activity_since_last_focus = False for w in t: if w.needs_attention: needs_attention = True - break + if w.has_activity_since_last_focus: + has_activity_since_last_focus = True ans.append(TabBarData( title, t is at, needs_attention, - len(t), t.current_layout.name or '' + len(t), t.current_layout.name or '', + has_activity_since_last_focus )) return ans diff --git a/kitty/window.py b/kitty/window.py index c986801ea..026956a18 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -514,6 +514,10 @@ class Window: def is_active(self) -> bool: return get_boss().active_window is self + @property + def has_activity_since_last_focus(self) -> bool: + return self.screen.has_activity_since_last_focus() + def on_bell(self) -> None: if self.opts.command_on_bell and self.opts.command_on_bell != ['none']: import subprocess