diff --git a/docs/changelog.rst b/docs/changelog.rst index a109a4e21..080e92cdd 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -22,6 +22,9 @@ To update |kitty|, :doc:`follow the instructions `. - Allow the user to supply a custom Python function to draw tab bar. See :opt:`tab_bar_style` +- A new remote control command to :program:`change the tab color ` (:iss:`1287`) + - Add support for reporting mouse events with pixel co-ordinates using the ``SGR_PIXEL_PROTOCOL`` introduced in xterm 359 diff --git a/kitty/rc/set_tab_color.py b/kitty/rc/set_tab_color.py new file mode 100644 index 000000000..7ce432688 --- /dev/null +++ b/kitty/rc/set_tab_color.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPLv3 Copyright: 2020, Kovid Goyal + + +from typing import TYPE_CHECKING, Dict, Optional + +from kitty.rgb import to_color + +from .base import ( + MATCH_TAB_OPTION, ArgsType, Boss, ParsingOfArgsFailed, PayloadGetType, + PayloadType, RCOptions, RemoteCommand, ResponseType, Window +) + +if TYPE_CHECKING: + from kitty.cli_stub import SetTabColorRCOptions as CLIOptions + + +valid_color_names = frozenset('active_fg active_bg inactive_fg inactive_bg'.split()) + + +def parse_colors(args: ArgsType) -> Dict[str, Optional[int]]: + ans: Dict[str, Optional[int]] = {} + for spec in args: + key, val = spec.split('=', 1) + key = key.lower() + if key.lower() not in valid_color_names: + raise KeyError(f'{key} is not a valid color name') + if val.lower() == 'none': + col: Optional[int] = None + else: + q = to_color(val, validate=True) + if q is not None: + col = int(q) + ans[key.lower()] = col + return ans + + +class SetTabColor(RemoteCommand): + + ''' + colors+: An object mapping names to colors as 24-bit RGB integers. A color value of null indicates it should be unset. + match: Which tab to change the color of + self: Boolean indicating whether to use the tab of the window the command is run in + ''' + + short_desc = 'Change the color of the specified tab(s) in the tab bar' + desc = f''' +{short_desc} + +The foreground and background colors when active and inactive can be overridden using this command. \ +The syntax for specifying colors is: active_fg=color active_bg=color inactive_fg=color \ +inactive_bg=color. Where color can be either a color name or a value of the form #rrggbb or \ +the keyword NONE to revert to using the default colors. +''' + options_spec = MATCH_TAB_OPTION + '''\n +--self +type=bool-set +If specified close the tab this command is run in, rather than the active tab. +''' + argspec = 'COLORS' + + def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType: + try: + colors = parse_colors(args) + except Exception as err: + raise ParsingOfArgsFailed(str(err)) from err + if not colors: + raise ParsingOfArgsFailed('No colors specified') + return {'match': opts.match, 'self': opts.self, 'colors': colors} + + def response_from_kitty(self, boss: Boss, window: Optional[Window], payload_get: PayloadGetType) -> ResponseType: + colors = payload_get('colors') + s = {k: None if colors[k] is None else int(colors[k]) for k in valid_color_names if k in colors} + for tab in self.tabs_for_match_payload(boss, window, payload_get): + if tab: + for k, v in s.items(): + setattr(tab, k, v) + tab.mark_tab_bar_dirty() + + +set_tab_color = SetTabColor() diff --git a/kitty/tab_bar.py b/kitty/tab_bar.py index 301027b9b..f5870bf6b 100644 --- a/kitty/tab_bar.py +++ b/kitty/tab_bar.py @@ -30,6 +30,10 @@ class TabBarData(NamedTuple): num_window_groups: int layout_name: str has_activity_since_last_focus: bool + active_fg: Optional[int] + active_bg: Optional[int] + inactive_fg: Optional[int] + inactive_bg: Optional[int] class DrawData(NamedTuple): @@ -50,6 +54,24 @@ class DrawData(NamedTuple): powerline_style: PowerlineStyle tab_bar_edge: EdgeLiteral + def tab_fg(self, tab: TabBarData) -> int: + if tab.is_active: + if tab.active_fg is not None: + return tab.active_fg + return int(self.active_fg) + if tab.inactive_fg is not None: + return tab.inactive_fg + return int(self.inactive_fg) + + def tab_bg(self, tab: TabBarData) -> int: + if tab.is_active: + if tab.active_bg is not None: + return tab.active_bg + return int(self.active_bg) + if tab.inactive_bg is not None: + return tab.inactive_bg + return int(self.inactive_bg) + def as_rgb(x: int) -> int: return (x << 8) | 2 @@ -122,7 +144,8 @@ class SupSub: class ExtraData: - pass + prev_tab: Optional[TabBarData] = None + next_tab: Optional[TabBarData] = None def draw_title(draw_data: DrawData, screen: Screen, tab: TabBarData, index: int) -> None: @@ -183,7 +206,7 @@ def draw_tab_with_slant( ) -> int: orig_fg = screen.cursor.fg left_sep, right_sep = ('', '') if draw_data.tab_bar_edge == 'top' else ('', '') - tab_bg = as_rgb(color_as_int(draw_data.active_bg if tab.is_active else draw_data.inactive_bg)) + tab_bg = screen.cursor.bg slant_fg = as_rgb(color_as_int(draw_data.default_bg)) def draw_sep(which: str) -> None: @@ -224,8 +247,6 @@ def draw_tab_with_separator( before: int, max_title_length: int, index: int, is_last: bool, extra_data: ExtraData ) -> int: - tab_bg = draw_data.active_bg if tab.is_active else draw_data.inactive_bg - screen.cursor.bg = as_rgb(color_as_int(tab_bg)) if draw_data.leading_spaces: screen.draw(' ' * draw_data.leading_spaces) draw_title(draw_data, screen, tab, index) @@ -252,12 +273,13 @@ def draw_tab_with_fade( before: int, max_title_length: int, index: int, is_last: bool, extra_data: ExtraData ) -> int: - tab_bg = draw_data.active_bg if tab.is_active else draw_data.inactive_bg + orig_bg = screen.cursor.bg + tab_bg = color_from_int(orig_bg >> 8) fade_colors = [as_rgb(color_as_int(alpha_blend(tab_bg, draw_data.default_bg, alpha))) for alpha in draw_data.alpha] for bg in fade_colors: screen.cursor.bg = bg screen.draw(' ') - screen.cursor.bg = as_rgb(color_as_int(tab_bg)) + screen.cursor.bg = orig_bg draw_title(draw_data, screen, tab, index) extra = screen.cursor.x - before - max_title_length if extra > 0: @@ -290,29 +312,21 @@ def draw_tab_with_powerline( before: int, max_title_length: int, index: int, is_last: bool, extra_data: ExtraData ) -> int: - tab_bg = as_rgb(color_as_int(draw_data.active_bg if tab.is_active else draw_data.inactive_bg)) - tab_fg = as_rgb(color_as_int(draw_data.active_fg if tab.is_active else draw_data.inactive_fg)) - inactive_bg = as_rgb(color_as_int(draw_data.inactive_bg)) - default_bg = as_rgb(color_as_int(draw_data.default_bg)) + tab_bg = screen.cursor.bg + tab_fg = screen.cursor.fg + default_bg = as_rgb(int(draw_data.default_bg)) + if extra_data.next_tab: + next_tab_bg = as_rgb(draw_data.tab_bg(extra_data.next_tab)) + needs_soft_separator = next_tab_bg == tab_bg + else: + next_tab_bg = default_bg + needs_soft_separator = False - separator_symbol, separator_alt_symbol = powerline_symbols.get(draw_data.powerline_style, ('', '')) + separator_symbol, soft_separator_symbol = powerline_symbols.get(draw_data.powerline_style, ('', '')) min_title_length = 1 + 2 - - if screen.cursor.x + min_title_length >= screen.columns: - screen.cursor.x -= 2 - screen.cursor.bg = default_bg - screen.cursor.fg = inactive_bg - screen.draw(f'{separator_symbol} ') - return screen.cursor.x - start_draw = 2 - if tab.is_active and screen.cursor.x >= 2: - screen.cursor.x -= 2 - screen.cursor.fg = inactive_bg - screen.cursor.bg = tab_bg - screen.draw(f'{separator_symbol} ') - screen.cursor.fg = tab_fg - elif screen.cursor.x == 0: + + if screen.cursor.x == 0: screen.cursor.bg = tab_bg screen.draw(' ') start_draw = 1 @@ -327,13 +341,10 @@ def draw_tab_with_powerline( screen.cursor.x -= extra + 1 screen.draw('…') - if tab.is_active or is_last: + if not needs_soft_separator: screen.draw(' ') screen.cursor.fg = tab_bg - if is_last: - screen.cursor.bg = default_bg - else: - screen.cursor.bg = inactive_bg + screen.cursor.bg = next_tab_bg screen.draw(separator_symbol) else: prev_fg = screen.cursor.fg @@ -344,7 +355,7 @@ def draw_tab_with_powerline( c2 = draw_data.inactive_bg.contrast(draw_data.inactive_fg) if c1 < c2: screen.cursor.fg = default_bg - screen.draw(f' {separator_alt_symbol}') + screen.draw(f' {soft_separator_symbol}') screen.cursor.fg = prev_fg end = screen.cursor.x @@ -516,8 +527,10 @@ class TabBar: ed = ExtraData() for i, t in enumerate(data): - s.cursor.bg = self.active_bg if t.is_active else 0 - s.cursor.fg = self.active_fg if t.is_active else 0 + ed.prev_tab = data[i - 1] if i > 0 else None + ed.next_tab = data[i + 1] if i + 1 < len(data) else None + s.cursor.bg = as_rgb(self.draw_data.tab_bg(t)) + s.cursor.fg = as_rgb(self.draw_data.tab_fg(t)) s.cursor.bold, s.cursor.italic = self.active_font_style if t.is_active else self.inactive_font_style before = s.cursor.x end = self.draw_func(self.draw_data, s, t, before, max_title_length, i + 1, t is last_tab, ed) diff --git a/kitty/tabs.py b/kitty/tabs.py index 6b6fb8fd6..d03b19235 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -77,6 +77,11 @@ def add_active_id_to_history(items: Deque[int], item_id: int, maxlen: int = 64) class Tab: # {{{ + active_fg: Optional[int] = None + active_bg: Optional[int] = None + inactive_fg: Optional[int] = None + inactive_bg: Optional[int] = None + def __init__( self, tab_manager: 'TabManager', @@ -922,7 +927,8 @@ class TabManager: # {{{ ans.append(TabBarData( title, t is at, needs_attention, len(t), t.num_window_groups, t.current_layout.name or '', - has_activity_since_last_focus + has_activity_since_last_focus, t.active_fg, t.active_bg, + t.inactive_fg, t.inactive_bg )) return ans