To facilitate the use of SGR escape sequences in program output, add an
API to convert printf-like format messages to their equivalent with
select escape sequences.  This allows easier integration into the
codebase without requiring every relevant printf-like call to be
altered.
---
 src/Makefile.am |   2 +
 src/display.c   | 354 ++++++++++++++++++++++++++++++++++++++++++++++++
 src/display.h   |  46 +++++++
 3 files changed, 402 insertions(+)
 create mode 100644 src/display.c
 create mode 100644 src/display.h

diff --git a/src/Makefile.am b/src/Makefile.am
index d6e040798f47..6354452cd8fb 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -4,13 +4,15 @@
 
 mbsync_SOURCES = \
        util.c config.c socket.c \
+       display.c \
        driver.c drv_proxy.c \
        drv_imap.c imap_msgs.c imap_utf7.c \
        drv_maildir.c \
        sync.c sync_state.c sync_msg_cvt.c \
        main.c main_sync.c main_list.c
 noinst_HEADERS = \
        common.h config.h socket.h \
+       display.h \
        driver.h imap_p.h \
        sync.h sync_p.h \
        main_p.h
diff --git a/src/display.c b/src/display.c
new file mode 100644
index 000000000000..9837777e7f0b
--- /dev/null
+++ b/src/display.c
@@ -0,0 +1,354 @@
+// SPDX-FileCopyrightText: 2026 Seth McDonald <[email protected]>
+// SPDX-License-Identifier: GPL-2.0-or-later WITH 
LicenseRef-isync-GPL-exception
+/*
+ * mbsync - mailbox synchronizer
+ */
+
+#define DEBUG_FLAG DEBUG_MAIN
+
+#include "display.h"
+
+#include <errno.h>
+#include <stdint.h>
+
+/*
+ * Parameter values for Select Graphic Rendition (SGR) attributes.  Can be used
+ * to alter the display of text on smart terminal emulators.
+ */
+typedef enum sgr_attr {
+       /* General display */
+       SGR_NONE = 0, /* Remove all applied SGR attributes */
+       SGR_BOLD = 1,
+       SGR_WEAK = 2,
+       SGR_ITALIC = 3,
+       SGR_UNDERLINE = 4,
+       SGR_BLINK = 5,
+       SGR_INVERT = 7,
+       SGR_HIDE = 8,
+       SGR_STRIKE = 9,
+
+       SGR_NO_BOLD = 22,
+       SGR_NO_WEAK = 22, /* Same behaviour as SGR_NO_BOLD */
+       SGR_NO_ITALIC = 23,
+       SGR_NO_UNDERLINE = 24,
+       SGR_NO_BLINK = 25,
+       SGR_NO_INVERT = 27,
+       SGR_NO_HIDE = 28,
+       SGR_NO_STRIKE = 29,
+
+       /* Foreground colour */
+       SGR_FG_BLACK = 30,
+       SGR_FG_RED = 31,
+       SGR_FG_GREEN = 32,
+       SGR_FG_YELLOW = 33,
+       SGR_FG_BLUE = 34,
+       SGR_FG_MAGENTA = 35,
+       SGR_FG_CYAN = 36,
+       SGR_FG_WHITE = 37,
+       SGR_FG_SELECT = 38, /* Custom 8-bit or 24-bit colour selection */
+       SGR_FG_DEFAULT = 39,
+
+       /* Background colour */
+       SGR_BG_BLACK = 40,
+       SGR_BG_RED = 41,
+       SGR_BG_GREEN = 42,
+       SGR_BG_YELLOW = 43,
+       SGR_BG_BLUE = 44,
+       SGR_BG_MAGENTA = 45,
+       SGR_BG_CYAN = 46,
+       SGR_BG_WHITE = 47,
+       SGR_BG_SELECT = 48,
+       SGR_BG_DEFAULT = 49,
+
+       /* Debugging */
+       SGR_INVALID = -1,
+} sgr_attr_t;
+
+/*
+ * Attempts to guess whether the file descriptor 'fd' supports display
+ * attributes via escape sequences.  Returns one if supported, and zero
+ * otherwise.
+ */
+int
+detect_display_attrs( int fd )
+{
+       errno = 0;
+       if (!isatty( fd )) {
+               if (errno != ENOTTY)
+                       sys_error( "Cannot examine file descriptor %d", fd );
+               return 0;
+       }
+
+       const char *term = getenv( "TERM" );
+       if (term && !strcmp( term, "dumb" ))
+               return 0;
+
+       return 1;
+}
+
+/*
+ * Returns the number of set bits in 'x'.
+ */
+static uint
+popcount( uint x )
+{
+#if defined(__has_builtin) && __has_builtin(__builtin_popcount)
+       return __builtin_popcount( x );
+#else
+       uint count = 0;
+       for (uint i = 0; i < CHAR_BIT * sizeof(x); i++)
+               count += (x >> i) & 1U;
+       return count;
+#endif
+}
+
+/*
+ * Takes appropriate action for the situation in which a function is given the
+ * invalid display attribute 'attr'.
+ */
+static void
+handle_invalid_display_attr( uint attr )
+{
+       debug( "Received invalid display attribute 0x%.8x.\n", attr );
+}
+
+/*
+ * If 'attr' is a valid display attribute, returns the SGR attribute that
+ * enables it.  Otherwise, returns SGR_INVALID.
+ */
+static sgr_attr_t
+display_attr_to_sgr_enable( uint attr )
+{
+       switch (attr) {
+       case DS_BOLD:
+               return SGR_BOLD;
+       case DS_WEAK:
+               return SGR_WEAK;
+       case DS_ITALIC:
+               return SGR_ITALIC;
+       case DS_UNDERLINE:
+               return SGR_UNDERLINE;
+       case DS_BLINK:
+               return SGR_BLINK;
+       case DS_INVERT:
+               return SGR_INVERT;
+       case DS_HIDE:
+               return SGR_HIDE;
+       case DS_STRIKE:
+               return SGR_STRIKE;
+       case DS_FG_BLACK:
+               return SGR_FG_BLACK;
+       case DS_FG_RED:
+               return SGR_FG_RED;
+       case DS_FG_GREEN:
+               return SGR_FG_GREEN;
+       case DS_FG_YELLOW:
+               return SGR_FG_YELLOW;
+       case DS_FG_BLUE:
+               return SGR_FG_BLUE;
+       case DS_FG_MAGENTA:
+               return SGR_FG_MAGENTA;
+       case DS_FG_CYAN:
+               return SGR_FG_CYAN;
+       case DS_FG_WHITE:
+               return SGR_FG_WHITE;
+       case DS_BG_BLACK:
+               return SGR_BG_BLACK;
+       case DS_BG_RED:
+               return SGR_BG_RED;
+       case DS_BG_GREEN:
+               return SGR_BG_GREEN;
+       case DS_BG_YELLOW:
+               return SGR_BG_YELLOW;
+       case DS_BG_BLUE:
+               return SGR_BG_BLUE;
+       case DS_BG_MAGENTA:
+               return SGR_BG_MAGENTA;
+       case DS_BG_CYAN:
+               return SGR_BG_CYAN;
+       case DS_BG_WHITE:
+               return SGR_BG_WHITE;
+       default:
+               handle_invalid_display_attr( attr );
+               return SGR_INVALID;
+       }
+}
+
+/*
+ * If 'attr' is a valid display attribute, returns the SGR attribute that
+ * disables it.  Otherwise, returns SGR_INVALID.
+ */
+static sgr_attr_t
+display_attr_to_sgr_disable( uint attr )
+{
+       switch (attr) {
+       case DS_BOLD:
+               return SGR_NO_BOLD;
+       case DS_WEAK:
+               return SGR_NO_WEAK;
+       case DS_ITALIC:
+               return SGR_NO_ITALIC;
+       case DS_UNDERLINE:
+               return SGR_NO_UNDERLINE;
+       case DS_BLINK:
+               return SGR_NO_BLINK;
+       case DS_INVERT:
+               return SGR_NO_INVERT;
+       case DS_HIDE:
+               return SGR_NO_HIDE;
+       case DS_STRIKE:
+               return SGR_NO_STRIKE;
+       case DS_FG_BLACK:
+       case DS_FG_RED:
+       case DS_FG_GREEN:
+       case DS_FG_YELLOW:
+       case DS_FG_BLUE:
+       case DS_FG_MAGENTA:
+       case DS_FG_CYAN:
+       case DS_FG_WHITE:
+               return SGR_FG_DEFAULT;
+       case DS_BG_BLACK:
+       case DS_BG_RED:
+       case DS_BG_GREEN:
+       case DS_BG_YELLOW:
+       case DS_BG_BLUE:
+       case DS_BG_MAGENTA:
+       case DS_BG_CYAN:
+       case DS_BG_WHITE:
+               return SGR_BG_DEFAULT;
+       default:
+               handle_invalid_display_attr( attr );
+               return SGR_INVALID;
+       }
+}
+
+/*
+ * Writes to 'buf' the SGR escape sequence represented by 'attr', including a
+ * terminating NULL character.  At most 'size' bytes are written.  Returns the
+ * number of bytes written, excluding the terminal NULL.
+ */
+static int
+write_sgr_sequence( char *buf, size_t size, sgr_attr_t attr )
+{
+       assert( buf );
+
+       if (size > INT_MAX)
+               size = INT_MAX;
+
+       return nfsnprintf( buf, (int)size, "\x1b[%dm", (int)attr );
+}
+
+/*
+ * Writes to 'buf' the set of escape sequences corresponding to the set of
+ * display attributes given by the bitfield 'attrs', including a terminating
+ * NULL character.  At most 'size' bytes are written.  Returns a pointer to the
+ * written terminal NULL.
+ *
+ * Each display attribute in 'attrs' is converted to an SGR attribute via the
+ * 'ds_to_sgr' function.  This function must return a valid SGR attribute if
+ * given a valid display attribute, and SGR_INVALID otherwise.  The mapping to
+ * valid SGR attributes need not be injective nor surjective.
+ */
+static char *
+write_display_attrs(
+               char *buf, size_t size, uint attrs, sgr_attr_t (*ds_to_sgr)( 
uint ) )
+{
+       assert( buf );
+       assert( ds_to_sgr );
+
+       for (uint i = 0; i < CHAR_BIT * sizeof(attrs); i++) {
+               uint ds_attr = attrs & (1U << i);
+               if (!ds_attr)
+                       continue;
+
+               sgr_attr_t sgr_attr = ds_to_sgr( ds_attr );
+               if (sgr_attr == SGR_INVALID)
+                       continue; // Ignore invalid display attributes
+
+               int seq_len = write_sgr_sequence( buf, size, sgr_attr );
+               buf += seq_len;
+               size -= seq_len;
+       }
+
+       return buf;
+}
+
+/*
+ * Wraps 'msg' in a set of escape sequences representing the display attributes
+ * given in 'attrs' and returns the resultant message.  This message is fit to
+ * be passed to printf() as the format string.  The caller retains ownership of
+ * the returned string.
+ *
+ * 'attrs' should be a valid bitfield of display_attr_enum values.  Each set
+ * display attribute is enabled at the start of the returned message and
+ * disabled at the end.  So after displaying the returned message, the prior
+ * state of attributes not included in 'attrs' are preserved, while attributes
+ * included in 'attrs' are disabled regardless of their prior state.
+ *
+ * It is the caller's responsibility to ensure the returned message will not be
+ * output to an inappropriate source.  Such as a tty without support for escape
+ * sequences, or a regular file whose content has not been explicitly indicated
+ * to include escape sequences.
+ *
+ * EXAMPLE USAGE
+ *
+ * Given the following code:
+ *
+ *     printf( "Hello %s!", name );
+ *
+ * To instead display the message in italic with blue text, you would write:
+ *
+ *     char *msg = wrap_msg( "Hello %s!", DS_ITALIC | DS_FG_BLUE );
+ *     printf( msg, name );
+ *     free( msg );
+ */
+char *
+wrap_msg( const char *msg, uint attrs )
+{
+       assert( msg );
+
+       uint attr_count = popcount( attrs );
+       size_t msg_len = strlen( msg );
+       size_t max_seq_len = strlen( "\x1b[00m" );
+       size_t max_added_len = max_seq_len * attr_count * 2;
+
+       // Check for overflow
+       if (msg_len > SIZE_MAX - max_added_len - 1) {
+               debug(
+                               "Cannot wrap oversized message of length %zu "
+                               "with %u display attributes.\n",
+                               msg_len,
+                               attr_count );
+               return nfstrdup( msg );
+       }
+
+       size_t size = msg_len + max_added_len + 1;
+       char *buf = malloc( size );
+
+       // Don't need to abort() if there's not enough memory for the wrapped
+       // message.  Just don't wrap the message.
+       if (!buf) {
+               debug(
+                               "Not enough memory to wrap message of length 
%zu "
+                               "with %u display attributes.\n",
+                               msg_len,
+                               attr_count );
+               return nfstrdup( msg );
+       }
+
+       size_t offset = 0;
+       char *pos = buf;
+
+       // Sandwich message between escape sequences
+       pos = write_display_attrs(
+                       pos, size - offset, attrs, display_attr_to_sgr_enable );
+       offset += pos - buf;
+
+       memcpy( pos, msg, msg_len + 1 );
+       offset += msg_len;
+       pos += msg_len;
+
+       write_display_attrs(
+                       pos, size - offset, attrs, display_attr_to_sgr_disable 
);
+       return buf;
+}
diff --git a/src/display.h b/src/display.h
new file mode 100644
index 000000000000..91885f9acf1f
--- /dev/null
+++ b/src/display.h
@@ -0,0 +1,46 @@
+// SPDX-FileCopyrightText: 2026 Seth McDonald <[email protected]>
+// SPDX-License-Identifier: GPL-2.0-or-later WITH 
LicenseRef-isync-GPL-exception
+/*
+ * mbsync - mailbox synchronizer
+ */
+
+#ifndef DISPLAY_H
+#define DISPLAY_H
+
+#include "common.h"
+
+#define display_attr_enum(fn) \
+       /* General display; can specify multiple per message */ \
+       fn(DS, BOLD) \
+       fn(DS, WEAK) /* Opposite of bold, I suppose */ \
+       fn(DS, ITALIC) /* Limited tty support */ \
+       fn(DS, UNDERLINE) /* ̲L̲i̲k̲e̲ ̲t̲h̲i̲s̲! */ \
+       fn(DS, BLINK) \
+       fn(DS, INVERT) /* Swap FG and BG colours */ \
+       fn(DS, HIDE) /* Make text invisible */ \
+       fn(DS, STRIKE) /* ̶L̶i̶k̶e̶ ̶t̶h̶i̶s̶! */ \
+       /* Foreground colour; no more than one per message */ \
+       fn(DS, FG_BLACK) \
+       fn(DS, FG_RED) \
+       fn(DS, FG_GREEN) \
+       fn(DS, FG_YELLOW) /* May look orange or brown */ \
+       fn(DS, FG_BLUE) \
+       fn(DS, FG_MAGENTA) \
+       fn(DS, FG_CYAN) \
+       fn(DS, FG_WHITE) /* May look (light) grey */ \
+       /* Background colour; no more than one per message */ \
+       fn(DS, BG_BLACK) \
+       fn(DS, BG_RED) \
+       fn(DS, BG_GREEN) \
+       fn(DS, BG_YELLOW) \
+       fn(DS, BG_BLUE) \
+       fn(DS, BG_MAGENTA) \
+       fn(DS, BG_CYAN) \
+       fn(DS, BG_WHITE)
+DEFINE_PFX_BIT_ENUM(display_attr_enum)
+
+int detect_display_attrs( int fd );
+
+char *wrap_msg( const char *msg, uint attrs );
+
+#endif
-- 
2.47.3



_______________________________________________
isync-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/isync-devel

Reply via email to