Allow specifying text formatting in tab_title_template

Fixes #3146
This commit is contained in:
Kovid Goyal
2020-12-08 21:42:40 +05:30
parent 6e83b4c1bc
commit 19870983ca
7 changed files with 70 additions and 5 deletions

View File

@@ -13,6 +13,8 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
- 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`)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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