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