Skip to content

Commit 1987098

Browse files
committed
Allow specifying text formatting in tab_title_template
Fixes #3146
1 parent 6e83b4c commit 1987098

File tree

7 files changed

+70
-5
lines changed

7 files changed

+70
-5
lines changed

docs/changelog.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
1313
- Add a new mappable `select_tab` action to choose a tab to switch to even
1414
when the tab bar is hidden (:iss:`3115`)
1515

16+
- Allow specifying text formatting in :opt:`tab_title_template` (:iss:`3146`)
17+
1618
- Graphics protocol: Add support for giving individual image placements their
1719
own ids. This is a backwards compatible protocol extension, and also allow
1820
suppressing responses from the terminal to commands (:iss:`3133`)

kitty/config_data.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -939,6 +939,10 @@ def active_tab_title_template(x: str) -> Optional[str]:
939939
in the tab. Note that formatting is done by Python's string formatting
940940
machinery, so you can use, for instance, :code:`{layout_name[:2].upper()}` to
941941
show only the first two letters of the layout name, upper-cased.
942+
If you want to style the text, you can use styling directives, for example:
943+
:code:`{fmt.fg.red}red{fmt.fg.default}normal{fmt.bg._00FF00}green bg{fmt.bg.normal}`.
944+
Similarly, for bold and italic:
945+
:code:`{fmt.bold}bold{fmt.nobold}normal{fmt.italic}italic{fmt.noitalic}`.
942946
'''))
943947
o('active_tab_title_template', 'none', option_type=active_tab_title_template, long_text=_('''
944948
Template to use for active tabs, if not specified falls back

kitty/fast_data_types.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,9 @@ class Screen:
10021002
def draw(self, text: str) -> None:
10031003
pass
10041004

1005+
def apply_sgr(self, text: str) -> None:
1006+
pass
1007+
10051008
def copy_colors_from(self, other: 'Screen') -> None:
10061009
pass
10071010

kitty/parser.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ utf8(char_type codepoint) {
5252
// }}}
5353

5454
// Macros {{{
55-
#define MAX_PARAMS 256
5655
#define IS_DIGIT \
5756
case '0': \
5857
case '1': \
@@ -465,7 +464,10 @@ repr_csi_params(unsigned int *params, unsigned int num_params) {
465464
return buf;
466465
}
467466

468-
static inline void
467+
#ifdef DUMP_COMMANDS
468+
static
469+
#endif
470+
void
469471
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) {
470472
enum State { START, NORMAL, MULTIPLE, COLOR, COLOR1, COLOR3 };
471473
enum State state = START;

kitty/screen.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2137,6 +2137,20 @@ draw(Screen *self, PyObject *src) {
21372137
Py_RETURN_NONE;
21382138
}
21392139

2140+
extern void
2141+
parse_sgr(Screen *screen, uint32_t *buf, unsigned int num, unsigned int *params, PyObject *dump_callback, const char *report_name, Region *region);
2142+
2143+
static PyObject*
2144+
apply_sgr(Screen *self, PyObject *src) {
2145+
if (!PyUnicode_Check(src)) { PyErr_SetString(PyExc_TypeError, "A unicode string is required"); return NULL; }
2146+
if (PyUnicode_READY(src) != 0) { return PyErr_NoMemory(); }
2147+
Py_UCS4 *buf = PyUnicode_AsUCS4Copy(src);
2148+
if (!buf) return NULL;
2149+
unsigned int params[MAX_PARAMS] = {0};
2150+
parse_sgr(self, buf, PyUnicode_GET_LENGTH(src), params, NULL, "parse_sgr", NULL);
2151+
Py_RETURN_NONE;
2152+
}
2153+
21402154
static PyObject*
21412155
reset_mode(Screen *self, PyObject *args) {
21422156
int private = false;
@@ -2783,6 +2797,7 @@ static PyMethodDef methods[] = {
27832797
MND(visual_line, METH_VARARGS)
27842798
MND(current_url_text, METH_NOARGS)
27852799
MND(draw, METH_O)
2800+
MND(apply_sgr, METH_O)
27862801
MND(cursor_position, METH_VARARGS)
27872802
MND(set_mode, METH_VARARGS)
27882803
MND(reset_mode, METH_VARARGS)

kitty/screen.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
#include "graphics.h"
1010
#include "monotonic.h"
11+
#define MAX_PARAMS 256
1112

1213
typedef enum ScrollTypes { SCROLL_LINE = -999999, SCROLL_PAGE, SCROLL_FULL } ScrollType;
1314

kitty/tab_bar.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
)
1414
from .layout.base import Rect
1515
from .options_stub import Options
16-
from .rgb import Color, alpha_blend, color_from_int
16+
from .rgb import Color, alpha_blend, color_as_sgr, color_from_int, to_color
1717
from .utils import color_as_int, log_error
1818
from .window import calculate_gl_geometry
1919

@@ -61,6 +61,35 @@ def compile_template(template: str) -> Any:
6161
report_template_failure(template, str(e))
6262

6363

64+
class ColorFormatter:
65+
66+
def __init__(self, which: str):
67+
self.which = which
68+
69+
def __getattr__(self, name: str) -> str:
70+
q = name
71+
if q == 'default':
72+
ans = '9'
73+
else:
74+
if name.startswith('_'):
75+
q = '#' + name[1:]
76+
c = to_color(q)
77+
if c is None:
78+
raise AttributeError(f'{name} is not a valid color')
79+
ans = '8' + color_as_sgr(c)
80+
return f'\x1b[{self.which}{ans}m'
81+
82+
83+
class Formatter:
84+
reset = '\x1b[0m'
85+
fg = ColorFormatter('3')
86+
bg = ColorFormatter('4')
87+
bold = '\x1b[1m'
88+
nobold = '\x1b[22m'
89+
italic = '\x1b[3m'
90+
noitalic = '\x1b[23m'
91+
92+
6493
def draw_title(draw_data: DrawData, screen: Screen, tab: TabBarData, index: int) -> None:
6594
if tab.needs_attention and draw_data.bell_on_tab:
6695
fg = screen.cursor.fg
@@ -81,13 +110,22 @@ def draw_title(draw_data: DrawData, screen: Screen, tab: TabBarData, index: int)
81110
'index': index,
82111
'layout_name': tab.layout_name,
83112
'num_windows': tab.num_windows,
84-
'title': tab.title
113+
'title': tab.title,
114+
'fmt': Formatter,
85115
}
86116
title = eval(compile_template(template), {'__builtins__': {}}, eval_locals)
87117
except Exception as e:
88118
report_template_failure(template, str(e))
89119
title = tab.title
90-
screen.draw(title)
120+
if '\x1b' in title:
121+
import re
122+
for x in re.split('(\x1b\\[[^m]*m)', title):
123+
if x.startswith('\x1b') and x.endswith('m'):
124+
screen.apply_sgr(x[2:-1])
125+
else:
126+
screen.draw(x)
127+
else:
128+
screen.draw(title)
91129

92130

93131
def draw_tab_with_separator(draw_data: DrawData, screen: Screen, tab: TabBarData, before: int, max_title_length: int, index: int, is_last: bool) -> int:

0 commit comments

Comments
 (0)