From e9cc3a5aa39dd9aebd38432fa77f4463ef565855 Mon Sep 17 00:00:00 2001 From: Mohsin Kaleem Date: Thu, 20 Apr 2023 22:30:12 +0100 Subject: Add support for colored and styled underlines on tty frames CLOSES bug#62994 * src/dispextern.h (face, face_underline_type, syms_of_xfacse) (internal-set-lisp-face-attribute) (gui_supports_face_attributes_p): Add definitions for new underline styles of Double-line, Dots and Dashes. Rename FACE_UNDER_LINE and FACE_UNDER_WAVE to make definitions consistent. Delete tty_underline_p from the face struct and use just underline going forward. Add a flag to check whether styled underlines are available. * lisp/cus-face.el (custom-face-attributes): Add entries for Double-line, Dots and Dashes so they can be set through `customize'. * src/termchar.c (tty_display_info): Add an entry for the escape sequence to set the underline style and color on terminal frames. * src/term.c (init_tty, tty_capable_p, turn_on_face): Read and save the underline style escape sequence from the Smulx termcap (alternatively if the Su flag is set use a default sequence). Allow checking for support of styled underlines in the current terminal frame. Output the necessary escape sequences to activate a styled underline on turn_on_face; this is currently only used for the new special underline styles, a default straight underline will still use the "us" termcap. Output escape sequence to set underline color when set in the face and supported by the tty. Save a default value for this sequence on init_tty when styled underlines are supported. * src/xfaces.c (tty_supports_face_attributes_p, realize_tty_face) (map_tty_color): Assert whether styled underlines are supported by the current terminal on display-supports-face-attributes-p checks. Populate the correct underline style and color in the face spec when realizing a face. Allow map_tty_color to map underline colors alongside foreground and background. The interface of map_tty_color was amended to allow the caller to supply the underline color instead of accessing it through the face attributes. * src/xterm.c (x_draw_glyph_string): Updated to use renamed FACE_UNDERLINE_SINGLE and FACE_UNDERLINE_WAVE face_underline_type enumerations. --- doc/lispref/display.texi | 15 ++-- etc/NEWS | 16 ++++ lisp/cus-face.el | 5 +- src/dispextern.h | 12 ++- src/term.c | 50 +++++++++++- src/termchar.h | 7 ++ src/xfaces.c | 159 ++++++++++++++++++++++++++++++++------- src/xterm.c | 6 +- 8 files changed, 225 insertions(+), 45 deletions(-) diff --git a/doc/lispref/display.texi b/doc/lispref/display.texi index fba15578f4f..8425aa23422 100644 --- a/doc/lispref/display.texi +++ b/doc/lispref/display.texi @@ -2685,12 +2685,15 @@ Face Attributes @var{color} is either a string, or the symbol @code{foreground-color}, meaning the foreground color of the face. Omitting the attribute @code{:color} means to use the foreground color of the face. -@var{style} should be a symbol @code{line} or @code{wave}, meaning to -use a straight or wavy line. Omitting the attribute @code{:style} -means to use a straight line. @var{position}, if non-@code{nil}, means to -display the underline at the descent of the text, instead of at the -baseline level. If it is a number, then it specifies the amount of -pixels above the descent to display the underline. +@var{style} is a symbol which sets the line-style to of the underline. +It should be one of @code{line}, @code{double-line}, @code{wave}, +@code{dots}, or @code{dashes}. GUI frames only support @code{line} and +@code{wave}. Terminal frames can support all aforementioned underline +styles. Omitting the attribute @code{:style} means to use a straight +line. @var{position}, if non-@code{nil}, means to display the underline +at the descent of the text, instead of at the baseline level. If it is +a number, then it specifies the amount of pixels above the descent to +display the underline. @end table @cindex overlined text diff --git a/etc/NEWS b/etc/NEWS index bc8be557711..7926f34f85a 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -424,6 +424,22 @@ Use 'TAB' in the minibuffer to show or hide the password. Likewise, there is an icon on the mode-line, which toggles the visibility of the password when clicking with 'mouse-1'. +** Terminal Emacs + +--- +*** Support for 'styled' and 'colored' underline face attributes on TTY frames. +If your terminals termcap or terminfo database entry has the 'Su' or +'Smulx' capability defined, Emacs will now emit the prescribed escape +sequence necessary to render faces with styled underlines on TTY +frames. + +Styled underlines are any underlines containing a non-default +underline style or a color other than the foreground-color. +The available underline styles for TTY frames are 'single', +'double-line', 'wave', 'dots, and 'dashes'. These are currently +supported by Kitty, libvte, and st (through the undercurl patch) among +other terminals. + * Editing Changes in Emacs 30.1 diff --git a/lisp/cus-face.el b/lisp/cus-face.el index 47afa841f5e..d0a1a66e29f 100644 --- a/lisp/cus-face.el +++ b/lisp/cus-face.el @@ -141,7 +141,10 @@ custom-face-attributes (const :format "" :value :style) (choice :tag "Style" (const :tag "Line" line) - (const :tag "Wave" wave)) + (const :tag "Double line" double-line) + (const :tag "Wave" wave) + (const :tag "Dots" dots) + (const :tag "Dashes" dashes)) (const :format "" :value :position) (choice :tag "Position" (const :tag "At Default Position" nil) diff --git a/src/dispextern.h b/src/dispextern.h index f29377f3596..fc5e53bb055 100644 --- a/src/dispextern.h +++ b/src/dispextern.h @@ -1690,9 +1690,13 @@ #define FONT_TOO_HIGH(ft) \ enum face_underline_type { + /* Note: Order matches the order of the Smulx terminfo extension. */ FACE_NO_UNDERLINE = 0, - FACE_UNDER_LINE, - FACE_UNDER_WAVE + FACE_UNDERLINE_SINGLE, + FACE_UNDERLINE_DOUBLE_LINE, + FACE_UNDERLINE_WAVE, + FACE_UNDERLINE_DOTS, + FACE_UNDERLINE_DASHES, }; /* Structure describing a realized face. @@ -1776,7 +1780,7 @@ #define FONT_TOO_HIGH(ft) \ ENUM_BF (face_box_type) box : 2; /* Style of underlining. */ - ENUM_BF (face_underline_type) underline : 2; + ENUM_BF (face_underline_type) underline : 3; /* If `box' above specifies a 3D type, true means use box_color for drawing shadows. */ @@ -1808,7 +1812,6 @@ #define FONT_TOO_HIGH(ft) \ string meaning the default color of the TTY. */ bool_bf tty_bold_p : 1; bool_bf tty_italic_p : 1; - bool_bf tty_underline_p : 1; bool_bf tty_reverse_p : 1; bool_bf tty_strike_through_p : 1; @@ -3426,6 +3429,7 @@ #define TTY_CAP_BOLD 0x04 #define TTY_CAP_DIM 0x08 #define TTY_CAP_ITALIC 0x10 #define TTY_CAP_STRIKE_THROUGH 0x20 +#define TTY_CAP_UNDERLINE_STYLED (0x32 & TTY_CAP_UNDERLINE) /*********************************************************************** diff --git a/src/term.c b/src/term.c index 3fa244be824..a0baf544897 100644 --- a/src/term.c +++ b/src/term.c @@ -2014,8 +2014,19 @@ turn_on_face (struct frame *f, int face_id) OUTPUT1 (tty, tty->TS_enter_dim_mode); } - if (face->tty_underline_p && MAY_USE_WITH_COLORS_P (tty, NC_UNDERLINE)) - OUTPUT1_IF (tty, tty->TS_enter_underline_mode); + if (face->underline && MAY_USE_WITH_COLORS_P (tty, NC_UNDERLINE)) + { + if (face->underline == FACE_UNDERLINE_SINGLE + || !tty->TF_set_underline_style) + OUTPUT1_IF (tty, tty->TS_enter_underline_mode); + else if (tty->TF_set_underline_style) + { + char *p; + p = tparam (tty->TF_set_underline_style, NULL, 0, face->underline, 0, 0, 0); + OUTPUT (tty, p); + xfree (p); + } + } if (face->tty_strike_through_p && MAY_USE_WITH_COLORS_P (tty, NC_STRIKE_THROUGH)) @@ -2041,6 +2052,14 @@ turn_on_face (struct frame *f, int face_id) OUTPUT (tty, p); xfree (p); } + + ts = tty->TF_set_underline_color; + if (ts && face->underline_color) + { + p = tparam (ts, NULL, 0, face->underline_color, 0, 0, 0); + OUTPUT (tty, p); + xfree (p); + } } } @@ -2061,7 +2080,7 @@ turn_off_face (struct frame *f, int face_id) if (face->tty_bold_p || face->tty_italic_p || face->tty_reverse_p - || face->tty_underline_p + || face->underline || face->tty_strike_through_p) { OUTPUT1_IF (tty, tty->TS_exit_attribute_mode); @@ -2073,7 +2092,7 @@ turn_off_face (struct frame *f, int face_id) { /* If we don't have "me" we can only have those appearances that have exit sequences defined. */ - if (face->tty_underline_p) + if (face->underline) OUTPUT_IF (tty, tty->TS_exit_underline_mode); } @@ -2104,6 +2123,9 @@ #define TTY_CAPABLE_P_TRY(tty, cap, TS, NC_bit) \ TTY_CAPABLE_P_TRY (tty, TTY_CAP_UNDERLINE, tty->TS_enter_underline_mode, NC_UNDERLINE); + TTY_CAPABLE_P_TRY (tty, + TTY_CAP_UNDERLINE_STYLED, tty->TF_set_underline_style, + NC_UNDERLINE); TTY_CAPABLE_P_TRY (tty, TTY_CAP_BOLD, tty->TS_enter_bold_mode, NC_BOLD); TTY_CAPABLE_P_TRY (tty, @@ -4360,6 +4382,26 @@ init_tty (const char *name, const char *terminal_type, bool must_succeed) tty->TF_underscore = tgetflag ("ul"); tty->TF_teleray = tgetflag ("xt"); + /* Styled underlines. Support for this is provided either by the + escape sequence in Smulx or the Su flag. The latter results in a + common default escape sequence and is not recommended. */ +#ifdef TERMINFO + tty->TF_set_underline_style = tigetstr ("Smulx"); + if (tty->TF_set_underline_style == (char *) (intptr_t) -1) + tty->TF_set_underline_style = NULL; +#else + tty->TF_set_underline_style = tgetstr ("Smulx", address); +#endif + if (!tty->TF_set_underline_style && tgetflag ("Su")) + /* Default to the kitty escape sequence. See + https://sw.kovidgoyal.net/kitty/underlines/. */ + tty->TF_set_underline_style = "\x1b[4:%p1%dm"; + + if (tty->TF_set_underline_style) + /* Standard escape sequence to set the underline color. + Requires a single parameter, the color index. */ + tty->TF_set_underline_color = "\x1b[58:2::%p1%{65536}%/%d:%p1%{256}%/%{255}%&%d:%p1%{255}%&%dm"; + #else /* DOS_NT */ #ifdef WINDOWSNT { diff --git a/src/termchar.h b/src/termchar.h index 2d845107e11..a1df5a19518 100644 --- a/src/termchar.h +++ b/src/termchar.h @@ -171,6 +171,13 @@ #define EMACS_TERMCHAR_H non-blank position. Must clear before writing _. */ int TF_teleray; /* termcap xt flag: many weird consequences. For t1061. */ + const char *TF_set_underline_style; /* termcap Smulx entry: Switches the underline + style based on the parameter. Param should + be one of: 0 (none), 1 (straight), 2 (double-line), + 3 (wave), 4 (dots), or 5 (dashes). */ + const char *TF_set_underline_color; /* Enabled when TF_set_underline_style is set: + Sets the color of the underline. Accepts a + single parameter, the color index. */ int RPov; /* # chars to start a TS_repeat */ diff --git a/src/xfaces.c b/src/xfaces.c index d4583e1a78f..649c65bef8f 100644 --- a/src/xfaces.c +++ b/src/xfaces.c @@ -3311,7 +3311,11 @@ DEFUN ("internal-set-lisp-face-attribute", Finternal_set_lisp_face_attribute, } else if (EQ (key, QCstyle) - && !(EQ (val, Qline) || EQ (val, Qwave))) + && !(EQ (val, Qline) + || EQ (val, Qdouble_line) + || EQ (val, Qwave) + || EQ (val, Qdots) + || EQ (val, Qdashes))) { valid_p = false; break; @@ -5275,6 +5279,7 @@ gui_supports_face_attributes_p (struct frame *f, Lisp_Object attrs[LFACE_VECTOR_SIZE], struct face *def_face) { + Lisp_Object val; Lisp_Object *def_attrs = def_face->lface; Lisp_Object lattrs[LFACE_VECTOR_SIZE]; @@ -5369,6 +5374,14 @@ gui_supports_face_attributes_p (struct frame *f, return false; } + /* Check supported underline styles. */ + val = attrs[LFACE_UNDERLINE_INDEX]; + if (!UNSPECIFIEDP (val) + && EQ (CAR_SAFE (val), QCstyle) + && !(EQ (CAR_SAFE (CDR_SAFE (val)), Qline) + || EQ (CAR_SAFE (CDR_SAFE (val)), Qwave))) + return false; /* Unsupported underline style. */ + /* Everything checks out, this face is supported. */ return true; } @@ -5462,9 +5475,18 @@ tty_supports_face_attributes_p (struct frame *f, if (!UNSPECIFIEDP (val)) { if (STRINGP (val)) - return false; /* ttys can't use colored underlines */ - else if (EQ (CAR_SAFE (val), QCstyle) && EQ (CAR_SAFE (CDR_SAFE (val)), Qwave)) - return false; /* ttys can't use wave underlines */ + test_caps |= TTY_CAP_UNDERLINE_STYLED; + else if (EQ (CAR_SAFE (val), QCstyle)) + { + if (!(EQ (CAR_SAFE (CDR_SAFE (val)), Qline) + || EQ (CAR_SAFE (CDR_SAFE (val)), Qdouble_line) + || EQ (CAR_SAFE (CDR_SAFE (val)), Qwave) + || EQ (CAR_SAFE (CDR_SAFE (val)), Qdots) + || EQ (CAR_SAFE (CDR_SAFE (val)), Qdashes))) + return false; /* Face uses an unsupported underline style. */ + + test_caps |= TTY_CAP_UNDERLINE_STYLED; + } else if (face_attr_equal_p (val, def_attrs[LFACE_UNDERLINE_INDEX])) return false; /* same as default */ else @@ -6321,7 +6343,7 @@ realize_gui_face (struct face_cache *cache, Lisp_Object attrs[LFACE_VECTOR_SIZE] if (EQ (underline, Qt)) { /* Use default color (same as foreground color). */ - face->underline = FACE_UNDER_LINE; + face->underline = FACE_UNDERLINE_SINGLE; face->underline_defaulted_p = true; face->underline_color = 0; face->underline_at_descent_line_p = false; @@ -6330,7 +6352,7 @@ realize_gui_face (struct face_cache *cache, Lisp_Object attrs[LFACE_VECTOR_SIZE] else if (STRINGP (underline)) { /* Use specified color. */ - face->underline = FACE_UNDER_LINE; + face->underline = FACE_UNDERLINE_SINGLE; face->underline_defaulted_p = false; face->underline_color = load_color (f, face, underline, @@ -6350,7 +6372,7 @@ realize_gui_face (struct face_cache *cache, Lisp_Object attrs[LFACE_VECTOR_SIZE] { /* `(:color COLOR :style STYLE)'. STYLE being one of `line' or `wave'. */ - face->underline = FACE_UNDER_LINE; + face->underline = FACE_UNDERLINE_SINGLE; face->underline_color = 0; face->underline_defaulted_p = true; face->underline_at_descent_line_p = false; @@ -6387,9 +6409,11 @@ realize_gui_face (struct face_cache *cache, Lisp_Object attrs[LFACE_VECTOR_SIZE] else if (EQ (keyword, QCstyle)) { if (EQ (value, Qline)) - face->underline = FACE_UNDER_LINE; + face->underline = FACE_UNDERLINE_SINGLE; else if (EQ (value, Qwave)) - face->underline = FACE_UNDER_WAVE; + face->underline = FACE_UNDERLINE_WAVE; + else + face->underline = FACE_UNDERLINE_SINGLE; } else if (EQ (keyword, QCposition)) { @@ -6440,17 +6464,18 @@ realize_gui_face (struct face_cache *cache, Lisp_Object attrs[LFACE_VECTOR_SIZE] } -/* Map a specified color of face FACE on frame F to a tty color index. - IDX is either LFACE_FOREGROUND_INDEX or LFACE_BACKGROUND_INDEX, and - specifies which color to map. Set *DEFAULTED to true if mapping to the +/* Map the specified color COLOR of face FACE on frame F to a tty + color index. IDX is one of LFACE_FOREGROUND_INDEX, + LFACE_BACKGROUND_INDEX or LFACE_UNDERLINE_INDEX, and specifies + which color to map. Set *DEFAULTED to true if mapping to the default foreground/background colors. */ static void -map_tty_color (struct frame *f, struct face *face, - enum lface_attribute_index idx, bool *defaulted) +map_tty_color (struct frame *f, struct face *face, Lisp_Object color, + enum lface_attribute_index idx, bool *defaulted) { - Lisp_Object frame, color, def; - bool foreground_p = idx == LFACE_FOREGROUND_INDEX; + Lisp_Object frame, def; + bool foreground_p = idx != LFACE_BACKGROUND_INDEX; unsigned long default_pixel = foreground_p ? FACE_TTY_DEFAULT_FG_COLOR : FACE_TTY_DEFAULT_BG_COLOR; unsigned long pixel = default_pixel; @@ -6459,10 +6484,11 @@ map_tty_color (struct frame *f, struct face *face, foreground_p ? FACE_TTY_DEFAULT_BG_COLOR : FACE_TTY_DEFAULT_FG_COLOR; #endif - eassert (idx == LFACE_FOREGROUND_INDEX || idx == LFACE_BACKGROUND_INDEX); + eassert (idx == LFACE_FOREGROUND_INDEX + || idx == LFACE_BACKGROUND_INDEX + || idx == LFACE_UNDERLINE_INDEX); XSETFRAME (frame, f); - color = face->lface[idx]; if (STRINGP (color) && SCHARS (color) @@ -6507,13 +6533,21 @@ map_tty_color (struct frame *f, struct face *face, #endif /* MSDOS */ } - if (foreground_p) - face->foreground = pixel; - else - face->background = pixel; + switch (idx) + { + case LFACE_FOREGROUND_INDEX: + face->foreground = pixel; + break; + case LFACE_UNDERLINE_INDEX: + face->underline_color = pixel; + break; + case LFACE_BACKGROUND_INDEX: + default: + face->background = pixel; + break; + } } - /* Realize the fully-specified face with attributes ATTRS in face cache CACHE for ASCII characters. Do it for TTY frame CACHE->f. Value is a pointer to the newly created realized face. */ @@ -6524,6 +6558,7 @@ realize_tty_face (struct face_cache *cache, { struct face *face; int weight, slant; + Lisp_Object underline; bool face_colors_defaulted = false; struct frame *f = cache->f; @@ -6543,16 +6578,83 @@ realize_tty_face (struct face_cache *cache, face->tty_bold_p = true; if (slant != 100) face->tty_italic_p = true; - if (!NILP (attrs[LFACE_UNDERLINE_INDEX])) - face->tty_underline_p = true; if (!NILP (attrs[LFACE_INVERSE_INDEX])) face->tty_reverse_p = true; if (!NILP (attrs[LFACE_STRIKE_THROUGH_INDEX])) face->tty_strike_through_p = true; + /* Text underline. */ + underline = attrs[LFACE_UNDERLINE_INDEX]; + if (NILP (underline)) + { + face->underline = FACE_NO_UNDERLINE; + face->underline_color = 0; + } + else if (EQ (underline, Qt)) + { + face->underline = FACE_UNDERLINE_SINGLE; + face->underline_color = 0; + } + else if (STRINGP (underline)) + { + face->underline = FACE_UNDERLINE_SINGLE; + bool underline_color_defaulted; + map_tty_color (f, face, underline, LFACE_UNDERLINE_INDEX, + &underline_color_defaulted); + } + else if (CONSP (underline)) + { + /* `(:color COLOR :style STYLE)'. + STYLE being one of `line', `double-line', `wave', `dots' or `dashes'. */ + face->underline = FACE_UNDERLINE_SINGLE; + face->underline_color = 0; + + while (CONSP (underline)) + { + Lisp_Object keyword, value; + + keyword = XCAR (underline); + underline = XCDR (underline); + + if (!CONSP (underline)) + break; + value = XCAR (underline); + underline = XCDR (underline); + + if (EQ (keyword, QCcolor)) + { + if (EQ (value, Qforeground_color)) + face->underline_color = 0; + else if (STRINGP (value)) + { + bool underline_color_defaulted; + map_tty_color (f, face, value, LFACE_UNDERLINE_INDEX, + &underline_color_defaulted); + } + } + else if (EQ (keyword, QCstyle)) + { + if (EQ (value, Qline)) + face->underline = FACE_UNDERLINE_SINGLE; + else if (EQ (value, Qdouble_line)) + face->underline = FACE_UNDERLINE_DOUBLE_LINE; + else if (EQ (value, Qwave)) + face->underline = FACE_UNDERLINE_WAVE; + else if (EQ (value, Qdots)) + face->underline = FACE_UNDERLINE_DOTS; + else if (EQ (value, Qdashes)) + face->underline = FACE_UNDERLINE_DASHES; + else + face->underline = FACE_UNDERLINE_SINGLE; + } + } + } + /* Map color names to color indices. */ - map_tty_color (f, face, LFACE_FOREGROUND_INDEX, &face_colors_defaulted); - map_tty_color (f, face, LFACE_BACKGROUND_INDEX, &face_colors_defaulted); + map_tty_color (f, face, face->lface[LFACE_FOREGROUND_INDEX], + LFACE_FOREGROUND_INDEX, &face_colors_defaulted); + map_tty_color (f, face, face->lface[LFACE_BACKGROUND_INDEX], + LFACE_BACKGROUND_INDEX, &face_colors_defaulted); /* Swap colors if face is inverse-video. If the colors are taken from the frame colors, they are already inverted, since the @@ -7238,6 +7340,9 @@ syms_of_xfaces (void) DEFSYM (QCposition, ":position"); DEFSYM (Qline, "line"); DEFSYM (Qwave, "wave"); + DEFSYM (Qdouble_line, "double-line"); + DEFSYM (Qdots, "dots"); + DEFSYM (Qdashes, "dashes"); DEFSYM (Qreleased_button, "released-button"); DEFSYM (Qpressed_button, "pressed-button"); DEFSYM (Qflat_button, "flat-button"); diff --git a/src/xterm.c b/src/xterm.c index e08ffd15b18..360541ac0b9 100644 --- a/src/xterm.c +++ b/src/xterm.c @@ -10957,7 +10957,7 @@ x_draw_glyph_string (struct glyph_string *s) /* Draw underline. */ if (s->face->underline) { - if (s->face->underline == FACE_UNDER_WAVE) + if (s->face->underline == FACE_UNDERLINE_WAVE) { if (s->face->underline_defaulted_p) x_draw_underwave (s, decoration_width); @@ -10971,13 +10971,13 @@ x_draw_glyph_string (struct glyph_string *s) XSetForeground (display, s->gc, xgcv.foreground); } } - else if (s->face->underline == FACE_UNDER_LINE) + else if (s->face->underline == FACE_UNDERLINE_SINGLE) { unsigned long thickness, position; int y; if (s->prev - && s->prev->face->underline == FACE_UNDER_LINE + && s->prev->face->underline == FACE_UNDERLINE_SINGLE && (s->prev->face->underline_at_descent_line_p == s->face->underline_at_descent_line_p) && (s->prev->face->underline_pixels_above_descent_line -- 2.44.0