A new remote control command to change tab color

Fixes #1287
This commit is contained in:
Kovid Goyal 2021-10-12 12:47:52 +05:30
parent 8187ec2cef
commit 5afb16ab8d
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
4 changed files with 139 additions and 35 deletions

View File

@ -22,6 +22,9 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
- 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 <kitty @
set-tab-color>` (:iss:`1287`)
- Add support for reporting mouse events with pixel co-ordinates using the
``SGR_PIXEL_PROTOCOL`` introduced in xterm 359

82
kitty/rc/set_tab_color.py Normal file
View File

@ -0,0 +1,82 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
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()

View File

@ -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)

View File

@ -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