diff --git a/docs/changelog.rst b/docs/changelog.rst index 11d61a785..a96cdb6ae 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -13,6 +13,8 @@ To update |kitty|, :doc:`follow the instructions `. - Add a new mappable `select_tab` action to choose a tab to switch to even when the tab bar is hidden (:iss:`3115`) +- Allow specifying text formatting in :opt:`tab_title_template` (:iss:`3146`) + - Graphics protocol: Add support for giving individual image placements their own ids. This is a backwards compatible protocol extension, and also allow suppressing responses from the terminal to commands (:iss:`3133`) diff --git a/kitty/config_data.py b/kitty/config_data.py index 3d0e1dd58..e42dc4200 100644 --- a/kitty/config_data.py +++ b/kitty/config_data.py @@ -939,6 +939,10 @@ layout name and :code:`{num_windows}` for the number of windows in the tab. Note that formatting is done by Python's string formatting machinery, so you can use, for instance, :code:`{layout_name[:2].upper()}` to show only the first two letters of the layout name, upper-cased. +If you want to style the text, you can use styling directives, for example: +:code:`{fmt.fg.red}red{fmt.fg.default}normal{fmt.bg._00FF00}green bg{fmt.bg.normal}`. +Similarly, for bold and italic: +:code:`{fmt.bold}bold{fmt.nobold}normal{fmt.italic}italic{fmt.noitalic}`. ''')) o('active_tab_title_template', 'none', option_type=active_tab_title_template, long_text=_(''' Template to use for active tabs, if not specified falls back diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index f41f3c42c..0e91c20dd 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1002,6 +1002,9 @@ class Screen: def draw(self, text: str) -> None: pass + def apply_sgr(self, text: str) -> None: + pass + def copy_colors_from(self, other: 'Screen') -> None: pass diff --git a/kitty/parser.c b/kitty/parser.c index c2634f650..5072843e0 100644 --- a/kitty/parser.c +++ b/kitty/parser.c @@ -52,7 +52,6 @@ utf8(char_type codepoint) { // }}} // Macros {{{ -#define MAX_PARAMS 256 #define IS_DIGIT \ case '0': \ case '1': \ @@ -465,7 +464,10 @@ repr_csi_params(unsigned int *params, unsigned int num_params) { return buf; } -static inline void +#ifdef DUMP_COMMANDS +static +#endif +void parse_sgr(Screen *screen, uint32_t *buf, unsigned int num, unsigned int *params, PyObject DUMP_UNUSED *dump_callback, const char *report_name DUMP_UNUSED, Region *region) { enum State { START, NORMAL, MULTIPLE, COLOR, COLOR1, COLOR3 }; enum State state = START; diff --git a/kitty/screen.c b/kitty/screen.c index c0152c323..8d31ca03b 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -2137,6 +2137,20 @@ draw(Screen *self, PyObject *src) { Py_RETURN_NONE; } +extern void +parse_sgr(Screen *screen, uint32_t *buf, unsigned int num, unsigned int *params, PyObject *dump_callback, const char *report_name, Region *region); + +static PyObject* +apply_sgr(Screen *self, PyObject *src) { + if (!PyUnicode_Check(src)) { PyErr_SetString(PyExc_TypeError, "A unicode string is required"); return NULL; } + if (PyUnicode_READY(src) != 0) { return PyErr_NoMemory(); } + Py_UCS4 *buf = PyUnicode_AsUCS4Copy(src); + if (!buf) return NULL; + unsigned int params[MAX_PARAMS] = {0}; + parse_sgr(self, buf, PyUnicode_GET_LENGTH(src), params, NULL, "parse_sgr", NULL); + Py_RETURN_NONE; +} + static PyObject* reset_mode(Screen *self, PyObject *args) { int private = false; @@ -2783,6 +2797,7 @@ static PyMethodDef methods[] = { MND(visual_line, METH_VARARGS) MND(current_url_text, METH_NOARGS) MND(draw, METH_O) + MND(apply_sgr, METH_O) MND(cursor_position, METH_VARARGS) MND(set_mode, METH_VARARGS) MND(reset_mode, METH_VARARGS) diff --git a/kitty/screen.h b/kitty/screen.h index 7affb4441..6d97aeb96 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -8,6 +8,7 @@ #include "graphics.h" #include "monotonic.h" +#define MAX_PARAMS 256 typedef enum ScrollTypes { SCROLL_LINE = -999999, SCROLL_PAGE, SCROLL_FULL } ScrollType; diff --git a/kitty/tab_bar.py b/kitty/tab_bar.py index 82f36815a..0fdab9e46 100644 --- a/kitty/tab_bar.py +++ b/kitty/tab_bar.py @@ -13,7 +13,7 @@ from .fast_data_types import ( ) from .layout.base import Rect from .options_stub import Options -from .rgb import Color, alpha_blend, color_from_int +from .rgb import Color, alpha_blend, color_as_sgr, color_from_int, to_color from .utils import color_as_int, log_error from .window import calculate_gl_geometry @@ -61,6 +61,35 @@ def compile_template(template: str) -> Any: report_template_failure(template, str(e)) +class ColorFormatter: + + def __init__(self, which: str): + self.which = which + + def __getattr__(self, name: str) -> str: + q = name + if q == 'default': + ans = '9' + else: + if name.startswith('_'): + q = '#' + name[1:] + c = to_color(q) + if c is None: + raise AttributeError(f'{name} is not a valid color') + ans = '8' + color_as_sgr(c) + return f'\x1b[{self.which}{ans}m' + + +class Formatter: + reset = '\x1b[0m' + fg = ColorFormatter('3') + bg = ColorFormatter('4') + bold = '\x1b[1m' + nobold = '\x1b[22m' + italic = '\x1b[3m' + noitalic = '\x1b[23m' + + def draw_title(draw_data: DrawData, screen: Screen, tab: TabBarData, index: int) -> None: if tab.needs_attention and draw_data.bell_on_tab: fg = screen.cursor.fg @@ -81,13 +110,22 @@ def draw_title(draw_data: DrawData, screen: Screen, tab: TabBarData, index: int) 'index': index, 'layout_name': tab.layout_name, 'num_windows': tab.num_windows, - 'title': tab.title + 'title': tab.title, + 'fmt': Formatter, } title = eval(compile_template(template), {'__builtins__': {}}, eval_locals) except Exception as e: report_template_failure(template, str(e)) title = tab.title - screen.draw(title) + if '\x1b' in title: + import re + for x in re.split('(\x1b\\[[^m]*m)', title): + if x.startswith('\x1b') and x.endswith('m'): + screen.apply_sgr(x[2:-1]) + else: + screen.draw(x) + else: + screen.draw(title) def draw_tab_with_separator(draw_data: DrawData, screen: Screen, tab: TabBarData, before: int, max_title_length: int, index: int, is_last: bool) -> int: