Hi Jim,

> > Or would it be better to separate the pure signal handling and tty cleanup
> > code from the code that looks up the appropriate escape sequences from
> > termlib/termcap, and create a module in gnulib with only the signal handling
> > and tty cleanup code?
> 
> I'm happy to use whichever approach you prefer, especially if you
> provide the patch.

I've taken the approach of adding a module in gnulib that does only the
signal handling and tty cleanup. Code review welcome!


2019-03-24  Bruno Haible  <br...@clisp.org>

        term-style-control: Add tests.
        * tests/test-term-style-control-hello.c: New file.
        * tests/test-term-style-control-yes.c: New file.
        * modules/term-style-control-tests: New file.

        term-style-control: New module.
        * lib/term-style-control.h: New file, based on libtextstyle's
        term-ostream.oo.h and term-ostream.oo.c.
        * lib/term-style-control.c: New file, based on libtextstyle's
        term-ostream.oo.c.
        * modules/term-style-control: New file.


Now you can use this to make the color cleanup in 'grep', 'diff', and 'ls'
reliable.

Test case for 'grep':
$ yes | grep --color y
then interrupt with Ctrl-C. Retry 10 times.

Test case for 'diff':
$ for i in `seq 100000`; do { echo abc; echo x; echo def; }; done > i1
$ for i in `seq 100000`; do { echo abc; echo y; echo def; }; done > i2
$ diff --color -u i1 i2
then interrupt with Ctrl-C. Retry 20 times.

Test case for 'ls':
  - Create a directory with many files in it:
    $ for i in `seq 100000`; do touch xyz$i; done
    $ chmod a+x xyz*
  - Run 'ls' and interrupt is through Ctrl-C after 0.3 seconds. The result
    may end like this, with a stray 'C' on the screen:
xyz18858   xyz30106  xyz41357  xyz52607  xyz63858  xyz75107  xyz86358  xyz97608
xyz18859   xyz30107  C
xyz18914
  - Or run 'ls' and use Ctrl-Z to interrupt it. You may see
xyz19924   xyz31173  xyz42423  xyz53674  xyz64924  xyz76174  xyz87424  xyz98675
xyz19925   xyz31174  xy^Z
xyz19982
    That is, the output continues after it was interrupted through Ctrl-Z.

Bruno
>From 61171aa7682c8429de0455e94bc3d6dd593ab9a6 Mon Sep 17 00:00:00 2001
From: Bruno Haible <br...@clisp.org>
Subject: [PATCH 1/2] term-style-control: New module.

* lib/term-style-control.h: New file, based on libtextstyle's
term-ostream.oo.h and term-ostream.oo.c.
* lib/term-style-control.c: New file, based on libtextstyle's
term-ostream.oo.c.
* modules/term-style-control: New file.
---
 ChangeLog                  |    9 +
 lib/term-style-control.c   | 1013 ++++++++++++++++++++++++++++++++++++++++++++
 lib/term-style-control.h   |  182 ++++++++
 modules/term-style-control |   30 ++
 4 files changed, 1234 insertions(+)
 create mode 100644 lib/term-style-control.c
 create mode 100644 lib/term-style-control.h
 create mode 100644 modules/term-style-control

diff --git a/ChangeLog b/ChangeLog
index 58b47dc..481db9d 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,12 @@
+2019-03-24  Bruno Haible  <br...@clisp.org>
+
+	term-style-control: New module.
+	* lib/term-style-control.h: New file, based on libtextstyle's
+	term-ostream.oo.h and term-ostream.oo.c.
+	* lib/term-style-control.c: New file, based on libtextstyle's
+	term-ostream.oo.c.
+	* modules/term-style-control: New file.
+
 2019-03-22  Akim Demaille  <a...@lrde.epita.fr>
 
 	_Noreturn: beware of C's _Noreturn in C++ pre C++11.
diff --git a/lib/term-style-control.c b/lib/term-style-control.c
new file mode 100644
index 0000000..4d2b8ee
--- /dev/null
+++ b/lib/term-style-control.c
@@ -0,0 +1,1013 @@
+/* Terminal control for outputting styled text to a terminal.
+   Copyright (C) 2006-2008, 2017, 2019 Free Software Foundation, Inc.
+   Written by Bruno Haible <br...@clisp.org>, 2019.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
+
+#include <config.h>
+
+/* Specification.  */
+#include "term-style-control.h"
+
+/* Set to 1 to get debugging output regarding signals.  */
+#define DEBUG_SIGNALS 0
+
+#include <errno.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#if DEBUG_SIGNALS
+# include <stdio.h>
+#endif
+#if HAVE_TCGETATTR
+# include <termios.h>
+# if !defined NOFLSH            /* QNX */
+#  define NOFLSH 0
+# endif
+#endif
+#if HAVE_TCGETATTR
+# include <sys/stat.h>
+#endif
+
+#include "fatal-signal.h"
+#include "sig-handler.h"
+#include "full-write.h"
+#include "same-inode.h"
+
+#define SIZEOF(a) (sizeof(a) / sizeof(a[0]))
+
+
+/* ============================ EINTR handling ============================ */
+
+/* EINTR handling for tcgetattr(), tcsetattr().
+   These functions can return -1/EINTR even when we don't have any
+   signal handlers set up, namely when we get interrupted via SIGSTOP.  */
+
+#if HAVE_TCGETATTR
+
+static inline int
+nonintr_tcgetattr (int fd, struct termios *tcp)
+{
+  int retval;
+
+  do
+    retval = tcgetattr (fd, tcp);
+  while (retval < 0 && errno == EINTR);
+
+  return retval;
+}
+
+static inline int
+nonintr_tcsetattr (int fd, int flush_mode, const struct termios *tcp)
+{
+  int retval;
+
+  do
+    retval = tcsetattr (fd, flush_mode, tcp);
+  while (retval < 0 && errno == EINTR);
+
+  return retval;
+}
+
+#endif
+
+
+/* ========================== Logging primitives ========================== */
+
+/* We need logging, especially for the signal handling, because
+     - Debugging through gdb is hardly possible, because gdb produces output
+       by itself and interferes with the process states.
+     - strace is buggy when it comes to SIGTSTP handling:  By default, it
+       sends the process a SIGSTOP signal instead of SIGTSTP.  It supports
+       an option '-D -I4' to mitigate this, though.  Also, race conditions
+       appear with different probability with and without strace.
+   fprintf(stderr) is not possible within async-safe code, because fprintf()
+   may invoke malloc().  */
+
+#if DEBUG_SIGNALS
+
+/* Log a simple message.  */
+static _GL_ASYNC_SAFE void
+log_message (const char *message)
+{
+  full_write (STDERR_FILENO, message, strlen (message));
+}
+
+#else
+
+# define log_message(message)
+
+#endif
+
+#if HAVE_TCGETATTR || DEBUG_SIGNALS
+
+/* Async-safe implementation of sprintf (str, "%d", n).  */
+static _GL_ASYNC_SAFE void
+sprintf_integer (char *str, int x)
+{
+  unsigned int y;
+  char buf[20];
+  char *p;
+  size_t n;
+
+  if (x < 0)
+    {
+      *str++ = '-';
+      y = (unsigned int) (-1 - x) + 1;
+    }
+  else
+    y = x;
+
+  p = buf + sizeof (buf);
+  do
+    {
+      *--p = '0' + (y % 10);
+      y = y / 10;
+    }
+  while (y > 0);
+  n = buf + sizeof (buf) - p;
+  memcpy (str, p, n);
+  str[n] = '\0';
+}
+
+#endif
+
+#if HAVE_TCGETATTR
+
+/* Async-safe conversion of errno value to string.  */
+static _GL_ASYNC_SAFE void
+simple_errno_string (char *str, int errnum)
+{
+  switch (errnum)
+    {
+    case EBADF:  strcpy (str, "EBADF"); break;
+    case EINTR:  strcpy (str, "EINTR"); break;
+    case EINVAL: strcpy (str, "EINVAL"); break;
+    case EIO:    strcpy (str, "EIO"); break;
+    case ENOTTY: strcpy (str, "ENOTTY"); break;
+    default: sprintf_integer (str, errnum); break;
+    }
+}
+
+#endif
+
+#if DEBUG_SIGNALS
+
+/* Async-safe conversion of signal number to name.  */
+static _GL_ASYNC_SAFE void
+simple_signal_string (char *str, int sig)
+{
+  switch (sig)
+    {
+    /* Fatal signals (see fatal-signal.c).  */
+    #ifdef SIGINT
+    case SIGINT:   strcpy (str, "SIGINT"); break;
+    #endif
+    #ifdef SIGTERM
+    case SIGTERM:  strcpy (str, "SIGTERM"); break;
+    #endif
+    #ifdef SIGHUP
+    case SIGHUP:   strcpy (str, "SIGHUP"); break;
+    #endif
+    #ifdef SIGPIPE
+    case SIGPIPE:  strcpy (str, "SIGPIPE"); break;
+    #endif
+    #ifdef SIGXCPU
+    case SIGXCPU:  strcpy (str, "SIGXCPU"); break;
+    #endif
+    #ifdef SIGXFSZ
+    case SIGXFSZ:  strcpy (str, "SIGXFSZ"); break;
+    #endif
+    #ifdef SIGBREAK
+    case SIGBREAK: strcpy (str, "SIGBREAK"); break;
+    #endif
+    /* Stopping signals.  */
+    #ifdef SIGTSTP
+    case SIGTSTP:  strcpy (str, "SIGTSTP"); break;
+    #endif
+    #ifdef SIGTTIN
+    case SIGTTIN:  strcpy (str, "SIGTTIN"); break;
+    #endif
+    #ifdef SIGTTOU
+    case SIGTTOU:  strcpy (str, "SIGTTOU"); break;
+    #endif
+    /* Continuing signals.  */
+    #ifdef SIGCONT
+    case SIGCONT:  strcpy (str, "SIGCONT"); break;
+    #endif
+    default: sprintf_integer (str, sig); break;
+    }
+}
+
+/* Emit a message that a given signal handler is being run.  */
+static _GL_ASYNC_SAFE void
+log_signal_handler_called (int sig)
+{
+  char message[100];
+  strcpy (message, "Signal handler for signal ");
+  simple_signal_string (message + strlen (message), sig);
+  strcat (message, " called.\n");
+  log_message (message);
+}
+
+#else
+
+# define log_signal_handler_called(sig)
+
+#endif
+
+
+/* ============================ Signal handling ============================ */
+
+/* There are several situations which can cause garbled output on the terminal's
+   screen:
+   (1) When the program calls exit() after calling flush_to_current_style,
+       the program would terminate and leave the terminal in a non-default
+       state.
+   (2) When the program is interrupted through a fatal signal, the terminal
+       would be left in a non-default state.
+   (3) When the program is stopped through a stopping signal, the terminal
+       would be left (for temporary use by other programs) in a non-default
+       state.
+   (4) When a foreground process receives a SIGINT, the kernel(!) prints '^C'.
+       On Linux, the place where this happens is
+         linux-5.0/drivers/tty/n_tty.c:713..730
+       within a call sequence
+         n_tty_receive_signal_char (n_tty.c:1245..1246)
+         -> commit_echoes (n_tty.c:792)
+         -> __process_echoes (n_tty.c:713..730).
+   (5) When a signal is sent, the output buffer is cleared.
+       On Linux, this output buffer consists of the "echo buffer" in the tty
+       and the "output buffer" in the driver.  The place where this happens is
+         linux-5.0/drivers/tty/n_tty.c:1133..1140
+       within a call
+         isig (n_tty.c:1133..1140).
+
+   How do we mitigate these problems?
+   (1) We install an exit handler that restores the terminal to the default
+       state.
+   (2) If tty_control is TTYCTL_PARTIAL or TTYCTL_FULL:
+       For some of the fatal signals (see gnulib's 'fatal-signal' module for
+       the precise list), we install a handler that attempts to restore the
+       terminal to the default state.  Since the terminal may be in the middle
+       of outputting an escape sequence at this point, the first escape
+       sequence emitted from this handler may have no effect and produce
+       garbled characters instead.  Therefore the handler outputs the cleanup
+       sequence twice.
+       For the other fatal signals, we don't do anything.
+   (3) If tty_control is TTYCTL_PARTIAL or TTYCTL_FULL:
+       For some of the stopping signals (SIGTSTP, SIGTTIN, SIGTTOU), we install
+       a handler that attempts to restore the terminal to the default state.
+       For SIGCONT, we install a handler that does the opposite: it puts the
+       terminal into the desired state again.
+       For SIGSTOP, we cannot do anything.
+   (4) If tty_control is TTYCTL_FULL:
+       The kernel's action depends on L_ECHO(tty) and L_ISIG(tty), that is, on
+       the local modes of the tty (see
+       <https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap11.html>
+       section 11.2.5).  We don't want to change L_ISIG; hence we change L_ECHO.
+       So, we disable the ECHO local flag of the tty; the equivalent command is
+       'stty -echo'.
+   (5) If tty_control is TTYCTL_FULL:
+       The kernel's action depends on !L_NOFLSH(tty), that is, again on the
+       local modes of the tty (see
+       <https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap11.html>
+       section 11.2.5).  So, we enable the NOFLSH local flag of the tty; the
+       equivalent command is 'stty noflsh'.
+       For terminals with a baud rate < 9600 this is suboptimal.  For this case
+       - where the traditional flushing behaviour makes sense - we would use a
+       technique that involves tcdrain(), TIOCOUTQ, and usleep() when it is OK
+       to disable NOFLSH.
+
+   Regarding (4) and (5), there is a complication: Changing the local modes is
+   done through tcsetattr().  However, when the process is put into the
+   background, tcsetattr() does not operate the same way as when the process is
+   running in the foreground.
+   To test this kind of behaviour, use the 'color-filter' example like this:
+     $ yes | ./filter '.*'
+     <Ctrl-Z>
+     $ bg 1
+   We have three possible implementation options:
+     * If we don't ignore the signal SIGTTOU:
+       If the TOSTOP bit in the terminal's local mode is clear (command
+       equivalent: 'stty -tostop') and the process is put into the background,
+       normal output would continue (per POSIX
+       <https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap11.html>
+       section 11.2.5) but tcsetattr() calls would cause it to stop due to
+       a SIGTTOU signal (per POSIX
+       <https://pubs.opengroup.org/onlinepubs/9699919799/functions/tcsetattr.html>).
+       Thus, the program would behave differently with term-style-control than
+       without.
+     * If we ignore the signal SIGTTOU when the TOSTOP bit in the terminal's
+       local mode is clear (i.e. when (tc.c_lflag & TOSTOP) == 0):
+       The tcsetattr() calls do not stop the process, but they don't have the
+       desired effect.
+       On Linux, when I put the process into the background and then kill it with
+       signal SIGINT, I can see that the last operation on the terminal settings
+       (as shown by 'strace') is
+         ioctl(1, TCSETSW, {B38400 opost isig icanon echo ...}) = 0
+       and yet, once the process is terminated, the terminal settings contain
+       '-echo', not 'echo'.
+     * Don't call tcsetattr() if the process is not in the foreground.
+       This approach produces reliable results.
+
+   Blocking some signals while a non-default style is active is *not* useful:
+     - It does not help against (1), since exit() is not a signal.
+     - Signal handlers are the better approach against (2) and (3).
+     - It does not help against (4) and (5), because the kernel's actions happen
+       outside the process.  */
+#define BLOCK_SIGNALS_DURING_NON_DEFAULT_STYLE_OUTPUT 0
+
+/* File descriptor of the currently active 'struct term_style_controller' and
+   'struct term_style_user_data'.  */
+static int volatile term_fd = -1;
+
+#if HAVE_TCGETATTR
+
+/* Status of the process group of term_fd.  */
+typedef enum
+{
+  PGRP_UNKNOWN = 0,     /* term_fd < 0.  Unknown status.  */
+  PGRP_NO_TTY,          /* term_fd >= 0 but is not connected to a tty.  */
+  PGRP_IN_FOREGROUND,   /* term_fd >= 0 is a tty.  This process is running in
+                           the foreground.  */
+  PGRP_IN_BACKGROUND    /* term_fd >= 0 is a tty.  This process is running in
+                           the background.  */
+} pgrp_status_t;
+static pgrp_status_t volatile pgrp_status = PGRP_UNKNOWN;
+
+/* Update pgrp_status, depending on term_fd.  */
+static _GL_ASYNC_SAFE void
+update_pgrp_status (void)
+{
+  int fd = term_fd;
+  if (fd < 0)
+    {
+      pgrp_status = PGRP_UNKNOWN;
+      log_message ("pgrp_status = PGRP_UNKNOWN\n");
+    }
+  else
+    {
+      pid_t p = tcgetpgrp (fd);
+      if (p < 0)
+        {
+          pgrp_status = PGRP_NO_TTY;
+          log_message ("pgrp_status = PGRP_NO_TTY\n");
+        }
+      else
+        {
+          /* getpgrp () changes when the process gets put into the background
+             by a shell that implements job control.  */
+          if (p == getpgrp ())
+            {
+              pgrp_status = PGRP_IN_FOREGROUND;
+              log_message ("pgrp_status = PGRP_IN_FOREGROUND\n");
+            }
+          else
+            {
+              pgrp_status = PGRP_IN_BACKGROUND;
+              log_message ("pgrp_status = PGRP_IN_BACKGROUND\n");
+            }
+        }
+    }
+}
+
+#else
+
+# define update_pgrp_status()
+
+#endif
+
+/* Controller and its user_data that contain information about how to do
+   output.  */
+static const struct term_style_controller * volatile active_controller;
+static struct term_style_user_data * volatile active_user_data;
+
+/* The 'struct term_style_control_data' embedded in active_user_data.
+   Same as
+     (active_controller != NULL
+      ? active_controller->get_control_data (active_user_data)
+      : NULL).  */
+static struct term_style_control_data * volatile active_control_data;
+
+/* The fd contained in active_control_data.
+   Same as
+     (active_controller != NULL
+      ? active_control_data->fd
+      : -1).  */
+static int volatile active_fd = -1;
+
+/* The exit handler.  */
+static void
+atexit_handler (void)
+{
+  /* Only do something while some output was started but not completed.  */
+  if (active_controller != NULL)
+    {
+      active_controller->restore (active_user_data);
+      deactivate_term_non_default_mode (active_controller, active_user_data);
+      #if 0 /* not needed */
+      deactivate_term_style_controller (active_controller, active_user_data);
+      #endif
+    }
+}
+
+#if HAVE_TCGETATTR
+
+/* Return a failure message after tcsetattr() failed.  */
+static _GL_ASYNC_SAFE void
+tcsetattr_failed (char message[100], const char *caller)
+{
+  int errnum = errno;
+  strcpy (message, caller);
+  strcat (message, ": tcsetattr(fd=");
+  sprintf_integer (message + strlen (message), active_fd);
+  strcat (message, ") failed, errno=");
+  simple_errno_string (message + strlen (message), errnum);
+  strcat (message, "\n");
+}
+
+/* True when orig_lflag represents the original tc.c_lflag.  */
+static bool volatile orig_lflag_set;
+static tcflag_t volatile orig_lflag;
+
+/* Modifies the tty's local mode, preparing for non-default terminal state.
+   Used only when the active_control_data's tty_control is TTYCTL_FULL.  */
+static _GL_ASYNC_SAFE void
+clobber_local_mode (void)
+{
+  /* Here, active_fd == term_fd.  */
+  if (pgrp_status == PGRP_IN_FOREGROUND)
+    {
+      struct termios tc;
+      if (nonintr_tcgetattr (active_fd, &tc) >= 0)
+        {
+          if (!orig_lflag_set)
+            orig_lflag = tc.c_lflag;
+          /* Set orig_lflag_set to true before actually modifying the tty's
+             local mode, because restore_local_mode does nothing if
+             orig_lflag_set is false.  */
+          orig_lflag_set = true;
+          tc.c_lflag &= ~ECHO;
+          tc.c_lflag |= NOFLSH;
+          if (nonintr_tcsetattr (active_fd, TCSANOW, &tc) < 0)
+            {
+              /* Since tcsetattr failed, restore_local_mode does not need to
+                 restore anything.  Set orig_lflag_set to false to indicate
+                 this.  */
+              orig_lflag_set = false;
+              {
+                char message[100];
+                tcsetattr_failed (message,
+                                  "term-style-control:clobber_local_mode");
+                full_write (STDERR_FILENO, message, strlen (message));
+              }
+            }
+        }
+    }
+}
+
+/* Modifies the tty's local mode, once the terminal is back to the default state.
+   Returns true if ECHO was turned off.
+   Used only when the active_control_data's tty_control is TTYCTL_FULL.  */
+static _GL_ASYNC_SAFE bool
+restore_local_mode (void)
+{
+  /* Here, active_fd == term_fd.  */
+  bool echo_was_off = false;
+  /* Nothing to do if !orig_lflag_set.  */
+  if (orig_lflag_set)
+    {
+      struct termios tc;
+      if (nonintr_tcgetattr (active_fd, &tc) >= 0)
+        {
+          echo_was_off = (tc.c_lflag & ECHO) == 0;
+          tc.c_lflag = orig_lflag;
+          if (nonintr_tcsetattr (active_fd, TCSADRAIN, &tc) < 0)
+            {
+              char message[100];
+              tcsetattr_failed (message,
+                                "term-style-control:restore_local_mode");
+              full_write (STDERR_FILENO, message, strlen (message));
+            }
+        }
+      orig_lflag_set = false;
+    }
+  return echo_was_off;
+}
+
+#endif
+
+#if defined SIGCONT
+
+/* The list of signals whose default behaviour is to stop or continue the
+   program.  */
+static int const job_control_signals[] =
+  {
+    #ifdef SIGTSTP
+    SIGTSTP,
+    #endif
+    #ifdef SIGTTIN
+    SIGTTIN,
+    #endif
+    #ifdef SIGTTOU
+    SIGTTOU,
+    #endif
+    #ifdef SIGCONT
+    SIGCONT,
+    #endif
+    0
+  };
+
+# define num_job_control_signals (SIZEOF (job_control_signals) - 1)
+
+#endif
+
+/* The following signals are relevant because they output escape sequences to
+   the terminal:
+     - fatal signals,
+     - stopping signals,
+     - continuing signals (SIGCONT).  */
+
+static sigset_t relevant_signal_set;
+static bool relevant_signal_set_initialized = false;
+
+static void
+init_relevant_signal_set ()
+{
+  if (!relevant_signal_set_initialized)
+    {
+      int fatal_signals[64];
+      size_t num_fatal_signals;
+      size_t i;
+
+      num_fatal_signals = get_fatal_signals (fatal_signals);
+
+      sigemptyset (&relevant_signal_set);
+      for (i = 0; i < num_fatal_signals; i++)
+        sigaddset (&relevant_signal_set, fatal_signals[i]);
+      #if defined SIGCONT
+      for (i = 0; i < num_job_control_signals; i++)
+        sigaddset (&relevant_signal_set, job_control_signals[i]);
+      #endif
+
+      relevant_signal_set_initialized = true;
+    }
+}
+
+/* Temporarily delay the relevant signals.  */
+static _GL_ASYNC_SAFE inline void
+block_relevant_signals ()
+{
+  /* The caller must ensure that init_relevant_signal_set () was already
+     called.  */
+  if (!relevant_signal_set_initialized)
+    abort ();
+
+  sigprocmask (SIG_BLOCK, &relevant_signal_set, NULL);
+}
+
+/* Stop delaying the relevant signals.  */
+static _GL_ASYNC_SAFE inline void
+unblock_relevant_signals ()
+{
+  sigprocmask (SIG_UNBLOCK, &relevant_signal_set, NULL);
+}
+
+#if defined SIGCONT
+
+/* Determines whether a signal is ignored.  */
+static _GL_ASYNC_SAFE bool
+is_ignored (int sig)
+{
+  struct sigaction action;
+
+  return (sigaction (sig, NULL, &action) >= 0
+          && get_handler (&action) == SIG_IGN);
+}
+
+#endif
+
+#if HAVE_TCGETATTR
+
+/* Write the same signal marker that the kernel would have printed if ECHO had
+   been turned on.  See (4) above.
+   This is a makeshift and is not perfect:
+     - When stderr refers to a different target than active_control_data->fd,
+       it is too hairy to write the signal marker.
+     - In some cases, when the signal was generated right before and delivered
+       right after a clobber_local_mode invocation, the result is that the
+       marker appears twice, e.g. ^C^C.  This occurs only with a small
+       probability.
+     - In some cases, when the signal was generated right before and delivered
+       right after a restore_local_mode invocation, the result is that the
+       marker does not appear at all.  This occurs only with a small
+       probability.
+   To test this kind of behaviour, use the 'test-term-style-control-yes' example
+   like this:
+     $ ./test-term-style-control-yes
+ */
+static _GL_ASYNC_SAFE void
+show_signal_marker (int sig)
+{
+  /* Write to stderr, not to active_control_data->fd, because
+     active_control_data->fd is often logged or used with 'less -R'.  */
+  if (active_controller != NULL && active_control_data->same_as_stderr)
+    switch (sig)
+      {
+      /* The kernel's action when the user presses the INTR key.  */
+      case SIGINT:
+        full_write (STDERR_FILENO, "^C", 2); break;
+      /* The kernel's action when the user presses the SUSP key.  */
+      case SIGTSTP:
+        full_write (STDERR_FILENO, "^Z", 2); break;
+      /* The kernel's action when the user presses the QUIT key.  */
+      case SIGQUIT:
+        full_write (STDERR_FILENO, "^\\", 2); break;
+      default: break;
+      }
+}
+
+#endif
+
+/* The main code of the signal handler for fatal signals and stopping signals.
+   It is reentrant.  */
+static _GL_ASYNC_SAFE void
+fatal_or_stopping_signal_handler (int sig)
+{
+  #if HAVE_TCGETATTR
+  bool echo_was_off = false;
+  #endif
+  /* Only do something while some output was interrupted.  */
+  if (active_controller != NULL
+      && active_control_data->tty_control != TTYCTL_NONE)
+    {
+      unsigned int i;
+
+      /* Block the relevant signals.  This is needed, because the output
+         of escape sequences below (usually through tputs invocations) is
+         not reentrant.  */
+      block_relevant_signals ();
+
+      /* Restore the terminal to the default state.  */
+      for (i = 0; i < 2; i++)
+        active_controller->async_restore (active_user_data);
+      #if HAVE_TCGETATTR
+      if (active_control_data->tty_control == TTYCTL_FULL)
+        {
+          /* Restore the local mode, once the escape sequences output above
+             have reached their destination.  */
+          echo_was_off = restore_local_mode ();
+        }
+      #endif
+
+      /* Unblock the relevant signals.  */
+      unblock_relevant_signals ();
+    }
+
+  #if HAVE_TCGETATTR
+  if (echo_was_off)
+    show_signal_marker (sig);
+  #endif
+}
+
+/* The signal handler for fatal signals.
+   It is reentrant.  */
+static _GL_ASYNC_SAFE void
+fatal_signal_handler (int sig)
+{
+  log_signal_handler_called (sig);
+  fatal_or_stopping_signal_handler (sig);
+}
+
+#if defined SIGCONT
+
+/* The signal handler for stopping signals.
+   It is reentrant.  */
+static _GL_ASYNC_SAFE void
+stopping_signal_handler (int sig)
+{
+  int saved_errno = errno;
+
+  log_signal_handler_called (sig);
+  fatal_or_stopping_signal_handler (sig);
+
+  /* Now execute the signal's default action.
+     We reinstall the handler later, during the SIGCONT handler.  */
+  {
+    struct sigaction action;
+    action.sa_handler = SIG_DFL;
+    action.sa_flags = SA_NODEFER;
+    sigemptyset (&action.sa_mask);
+    sigaction (sig, &action, NULL);
+  }
+  errno = saved_errno;
+  raise (sig);
+}
+
+/* The signal handler for SIGCONT.
+   It is reentrant.  */
+static _GL_ASYNC_SAFE void
+continuing_signal_handler (int sig)
+{
+  int saved_errno = errno;
+
+  log_signal_handler_called (sig);
+  update_pgrp_status ();
+  /* Only do something while some output was interrupted.  */
+  if (active_controller != NULL
+      && active_control_data->tty_control != TTYCTL_NONE)
+    {
+      /* Reinstall the signals handlers removed in stopping_signal_handler.  */
+      {
+        unsigned int i;
+
+        for (i = 0; i < num_job_control_signals; i++)
+          {
+            int sig = job_control_signals[i];
+
+            if (sig != SIGCONT && !is_ignored (sig))
+              {
+                struct sigaction action;
+                action.sa_handler = &stopping_signal_handler;
+                /* If we get a stopping or continuing signal while executing
+                   stopping_signal_handler or continuing_signal_handler, enter
+                   it recursively, since it is reentrant.
+                   Hence no SA_RESETHAND.  */
+                action.sa_flags = SA_NODEFER;
+                sigemptyset (&action.sa_mask);
+                sigaction (sig, &action, NULL);
+              }
+          }
+      }
+
+      /* Block the relevant signals.  This is needed, because the output of
+         escape sequences done inside the async_set_attributes_from_default
+         call below is not reentrant.  */
+      block_relevant_signals ();
+
+      #if HAVE_TCGETATTR
+      if (active_control_data->tty_control == TTYCTL_FULL)
+        {
+          /* Modify the local mode.  */
+          clobber_local_mode ();
+        }
+      #endif
+      /* Set the terminal attributes.  */
+      active_controller->async_set_attributes_from_default (active_user_data);
+
+      /* Unblock the relevant signals.  */
+      unblock_relevant_signals ();
+    }
+
+  errno = saved_errno;
+}
+
+/* Ensure the signal handlers are installed.
+   Once they are installed, we leave them installed.  It's not worth
+   installing and uninstalling them each time we switch the terminal to a
+   non-default state and back; instead we set active_controller to tell the
+   signal handler whether it has something to do or not.  */
+
+static void
+ensure_continuing_signal_handler (void)
+{
+  static bool signal_handler_installed = false;
+
+  if (!signal_handler_installed)
+    {
+      int sig = SIGCONT;
+      struct sigaction action;
+      action.sa_handler = &continuing_signal_handler;
+      /* If we get a stopping or continuing signal while executing
+         continuing_signal_handler, enter it recursively, since it is
+         reentrant.  Hence no SA_RESETHAND.  */
+      action.sa_flags = SA_NODEFER;
+      sigemptyset (&action.sa_mask);
+      sigaction (sig, &action, NULL);
+
+      signal_handler_installed = true;
+    }
+}
+
+#endif
+
+static void
+ensure_other_signal_handlers (void)
+{
+  static bool signal_handlers_installed = false;
+
+  if (!signal_handlers_installed)
+    {
+      unsigned int i;
+
+      /* Install the handlers for the fatal signals.  */
+      at_fatal_signal (fatal_signal_handler);
+
+      #if defined SIGCONT
+
+      /* Install the handlers for the stopping and continuing signals.  */
+      for (i = 0; i < num_job_control_signals; i++)
+        {
+          int sig = job_control_signals[i];
+
+          if (sig == SIGCONT)
+            /* Already handled in ensure_continuing_signal_handler.  */
+            ;
+          else if (!is_ignored (sig))
+            {
+              struct sigaction action;
+              action.sa_handler = &stopping_signal_handler;
+              /* If we get a stopping or continuing signal while executing
+                 stopping_signal_handler, enter it recursively, since it is
+                 reentrant.  Hence no SA_RESETHAND.  */
+              action.sa_flags = SA_NODEFER;
+              sigemptyset (&action.sa_mask);
+              sigaction (sig, &action, NULL);
+            }
+          #if DEBUG_SIGNALS
+          else
+            {
+              fprintf (stderr, "Signal %d is ignored. Not installing a handler!\n",
+                       sig);
+              fflush (stderr);
+            }
+          #endif
+        }
+
+      #endif
+
+      signal_handlers_installed = true;
+    }
+}
+
+
+/* ============================== Public API ============================== */
+
+void
+activate_term_non_default_mode (const struct term_style_controller *controller,
+                                struct term_style_user_data *user_data)
+{
+  struct term_style_control_data *control_data =
+    controller->get_control_data (user_data);
+
+  if (!control_data->non_default_active)
+    {
+      if (control_data->tty_control != TTYCTL_NONE)
+        ensure_other_signal_handlers ();
+
+      #if BLOCK_SIGNALS_DURING_NON_DEFAULT_STYLE_OUTPUT
+      /* Block fatal signals, so that a SIGINT or similar doesn't interrupt
+         us without the possibility of restoring the terminal's state.
+         Likewise for SIGTSTP etc.  */
+      block_relevant_signals ();
+      #endif
+
+      /* Enable the exit handler for restoring the terminal's state,
+         and make the signal handlers effective.  */
+      if (active_controller != NULL)
+        {
+          /* We can't support two active controllers with non-default
+             attributes at the same time.  */
+          abort ();
+        }
+      /* The uses of 'volatile' (and ISO C 99 section 5.1.2.3.(5)) ensure that
+         we set active_controller to a non-NULL value only after the memory
+         locations active_user_data, active_control_data, active_fd have been
+         filled.  */
+      active_fd = control_data->fd;
+      active_control_data = control_data;
+      active_user_data = user_data;
+      active_controller = controller;
+
+      #if HAVE_TCGETATTR
+      /* Now that the signal handlers are effective, modify the tty.  */
+      if (active_control_data->tty_control == TTYCTL_FULL)
+        {
+          /* Modify the local mode.  */
+          clobber_local_mode ();
+        }
+      #endif
+
+      control_data->non_default_active = true;
+    }
+}
+
+void
+deactivate_term_non_default_mode (const struct term_style_controller *controller,
+                                  struct term_style_user_data *user_data)
+{
+  struct term_style_control_data *control_data =
+    controller->get_control_data (user_data);
+
+  if (control_data->non_default_active)
+    {
+      #if HAVE_TCGETATTR
+      /* Before we make the signal handlers ineffective, modify the tty.  */
+      if (active_control_data->tty_control == TTYCTL_FULL)
+        {
+          /* Restore the local mode, once the tputs calls from out_attr_change
+             have reached their destination.  */
+          restore_local_mode ();
+        }
+      #endif
+
+      /* Disable the exit handler, and make the signal handlers ineffective.  */
+      /* The uses of 'volatile' (and ISO C 99 section 5.1.2.3.(5)) ensure that
+         we reset active_user_data, active_control_data, active_fd only after
+         the memory location active_controller has been cleared.  */
+      active_controller = NULL;
+      active_user_data = NULL;
+      active_control_data = NULL;
+      active_fd = -1;
+
+      #if BLOCK_SIGNALS_DURING_NON_DEFAULT_STYLE_OUTPUT
+      /* Unblock the relevant signals.  */
+      unblock_relevant_signals ();
+      #endif
+
+      control_data->non_default_active = false;
+    }
+}
+
+void
+activate_term_style_controller (const struct term_style_controller *controller,
+                                struct term_style_user_data *user_data,
+                                int fd, ttyctl_t tty_control)
+{
+  struct term_style_control_data *control_data =
+    controller->get_control_data (user_data);
+
+  control_data->fd = fd;
+
+  /* Prepare tty control.  */
+  if (tty_control == TTYCTL_AUTO)
+    tty_control = TTYCTL_FULL;
+  control_data->tty_control = tty_control;
+  if (control_data->tty_control != TTYCTL_NONE)
+    init_relevant_signal_set ();
+  #if HAVE_TCGETATTR
+  if (control_data->tty_control == TTYCTL_FULL)
+    {
+      struct stat statbuf1;
+      struct stat statbuf2;
+      if (fd == STDERR_FILENO
+          || (fstat (fd, &statbuf1) >= 0
+              && fstat (STDERR_FILENO, &statbuf2) >= 0
+              && SAME_INODE (statbuf1, statbuf2)))
+        control_data->same_as_stderr = true;
+      else
+        control_data->same_as_stderr = false;
+    }
+  else
+    /* This value is actually not used.  */
+    control_data->same_as_stderr = false;
+  #endif
+
+  control_data->non_default_active = false;
+
+  /* Start keeping track of the process group status.  */
+  term_fd = fd;
+  #if defined SIGCONT
+  ensure_continuing_signal_handler ();
+  #endif
+  update_pgrp_status ();
+
+  /* Register an exit handler.  */
+  {
+    static bool registered = false;
+    if (!registered)
+      {
+        atexit (atexit_handler);
+        registered = true;
+      }
+  }
+}
+
+void
+deactivate_term_style_controller (const struct term_style_controller *controller,
+                                  struct term_style_user_data *user_data)
+{
+  struct term_style_control_data *control_data =
+    controller->get_control_data (user_data);
+
+  /* Verify that the non-default attributes mode is turned off.  */
+  if (control_data->non_default_active)
+    abort ();
+
+  term_fd = -1;
+  update_pgrp_status ();
+}
diff --git a/lib/term-style-control.h b/lib/term-style-control.h
new file mode 100644
index 0000000..e341931
--- /dev/null
+++ b/lib/term-style-control.h
@@ -0,0 +1,182 @@
+/* Terminal control for outputting styled text to a terminal.
+   Copyright (C) 2019 Free Software Foundation, Inc.
+   Written by Bruno Haible <br...@clisp.org>, 2019.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
+
+#ifndef _TERM_STYLE_CONTROL_H
+#define _TERM_STYLE_CONTROL_H
+
+#include <stdbool.h>
+
+/* The user of this file will define a macro 'term_style_user_data', such that
+   'struct term_style_user_data' is a user-defined struct.  */
+
+
+/* The amount of control to take over the underlying tty in order to avoid
+   garbled output on the screen, due to interleaved output of escape sequences
+   and output from the kernel (such as when the kernel echoes user's input
+   or when the kernel prints '^C' after the user pressed Ctrl-C).  */
+typedef enum
+{
+  TTYCTL_AUTO = 0,  /* Automatic best-possible choice.  */
+  TTYCTL_NONE,      /* No control.
+                       Result: Garbled output can occur, and the terminal can
+                       be left in any state when the program is interrupted.  */
+  TTYCTL_PARTIAL,   /* Signal handling.
+                       Result: Garbled output can occur, but the terminal will
+                       be left in the default state when the program is
+                       interrupted.  */
+  TTYCTL_FULL       /* Signal handling and disabling echo and flush-upon-signal.
+                       Result: No garbled output, and the the terminal will
+                       be left in the default state when the program is
+                       interrupted.  */
+} ttyctl_t;
+
+/* This struct contains data, used by implementation of this module.
+   You should not access the members of this struct; they may be renamed or
+   removed without notice.  */
+struct term_style_control_data
+{
+  int volatile fd;
+  ttyctl_t volatile tty_control;     /* Signal handling and tty control.  */
+  #if HAVE_TCGETATTR
+  bool volatile same_as_stderr;
+  #endif
+  bool non_default_active;           /* True if activate_term_non_default_mode()
+                                        is in effect.  */
+};
+
+/* Forward declaration.  */
+struct term_style_user_data;
+
+/* This struct contains function pointers.  You implement these functions
+   in your application; this module invokes them when it needs to.  */
+struct term_style_controller
+{
+  /* This function returns a pointer to the embedded
+     'struct term_style_control_data' contained in a
+     'struct term_style_user_data'.  */
+  struct term_style_control_data * (*get_control_data) (struct term_style_user_data *);
+
+  /* This function brings the terminal's state back to the default state
+     (no styling attributes set).  It is invoked when the process terminates
+     through exit().  */
+  void (*restore) (struct term_style_user_data *);
+
+  /* This function brings the terminal's state back to the default state
+     (no styling attributes set).  It is async-safe (see gnulib-common.m4 for
+     the precise definition).  It is invoked when the process receives a fatal
+     or stopping signal.  */
+  _GL_ASYNC_SAFE void (*async_restore) (struct term_style_user_data *);
+
+  /* This function brings the terminal's state, from the default state, back
+     to the state where it has the desired attributes set.  It is async-safe
+     (see gnulib-common.m4 for the precise definition).  It is invoked when
+     the process receives a SIGCONT signal.  */
+  _GL_ASYNC_SAFE void (*async_set_attributes_from_default) (struct term_style_user_data *);
+};
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* This module is used as follows:
+   1. You fill a 'struct term_style_controller' with function pointers.
+      You create a 'struct term_style_user_data' that contains, among other
+      members, a 'struct term_style_control_data'.
+      You will pass these two objects to all API functions below.
+   2. You call activate_term_style_controller to activate this controller.
+      Activation of the controller is the prerequisite for activating
+      the non-default mode, which in turn is the prerequisite for changing
+      the terminal's attributes.
+      When you are done with the styled output, you may deactivate the
+      controller.  This is not required before exiting the program, but is
+      required before activating a different controller.
+      You cannot have more than one controller activated at the same time.
+   3. Once the controller is activated, you may turn on the non-default mode.
+      The non-default mode is the prerequisite for changing the terminal's
+      attributes.  Once the terminal's attributes are in the default state
+      again, you may turn off the non-default mode again.
+      In other words:
+        - In the default mode, the terminal's attributes MUST be in the default
+          state; no styled output is possible.
+        - In the non-default mode, the terminal's attributes MAY switch among
+          the default state and other states.
+      This module exercises a certain amount of control over the terminal
+      during the non-default mode phases; see above (ttyctl_t) for details.
+      You may switch between the default and the non-default modes any number
+      of times.
+      The idea is that you switch back to the default mode before doing large
+      amounts of output of unstyled text.  However, this is not a requirement:
+      You may leave the non-default mode turned on all the time until the
+      the program exits.
+   3. Once the non-default mode is activated, you may change the attributes
+      (foreground color, background color, font weight, font posture, underline
+      decoration, etc.) of the terminal.  On Unix, this is typically done by
+      outputting appropriate escape sequences.
+   4. Once attributes are set, text output to the terminal will be rendered
+      with these attributes.
+      Note: You MUST return the terminal to the default state before outputting
+      a newline.
+ */
+
+/* Activates a controller.  The CONTROLLER and its USER_DATA controls the
+   terminal associated with FD.  FD is usually STDOUT_FILENO.
+   TTY_CONTROL specifies the amount of control to take over the underlying tty.
+   The effects of this functions are undone by calling
+   deactivate_term_style_controller.
+   You cannot have more than one controller activated at the same time.
+   You must not close FD while the controller is active.  */
+extern void
+       activate_term_style_controller (const struct term_style_controller *controller,
+                                       struct term_style_user_data *user_data,
+                                       int fd, ttyctl_t tty_control);
+
+/* Activates the non-default mode.
+   CONTROLLER and its USER_DATA must be a currently active controller.
+   This function fiddles with the signals of the current process and with
+   the underlying tty, to an extent described by TTY_CONTROL.
+   This function is idempotent: When you call it twice in a row, the second
+   invocation does nothing.
+   The effects of this function are undone by calling
+   deactivate_term_non_default_mode.  */
+extern void
+       activate_term_non_default_mode (const struct term_style_controller *controller,
+                                       struct term_style_user_data *user_data);
+
+/* Deactivates the non-default mode.
+   CONTROLLER and its USER_DATA must be a currently active controller.
+   This function is idempotent: When you call it twice in a row, the second
+   invocation does nothing.
+   Before invoking this function, you must put the terminal's attributes in
+   the default state.  */
+extern void
+       deactivate_term_non_default_mode (const struct term_style_controller *controller,
+                                         struct term_style_user_data *user_data);
+
+/* Deactivates a controller.
+   CONTROLLER and its USER_DATA must be a currently active controller.
+   Before invoking this function, you must ensure that the non-default mode
+   is deactivated.  */
+extern void
+       deactivate_term_style_controller (const struct term_style_controller *controller,
+                                         struct term_style_user_data *user_data);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _TERM_STYLE_CONTROL_H */
diff --git a/modules/term-style-control b/modules/term-style-control
new file mode 100644
index 0000000..fb2d098
--- /dev/null
+++ b/modules/term-style-control
@@ -0,0 +1,30 @@
+Description:
+Terminal control for outputting styled text to a terminal.
+
+Files:
+lib/term-style-control.h
+lib/term-style-control.c
+
+Depends-on:
+stdbool
+fatal-signal
+sigaction
+sigprocmask
+full-write
+same-inode
+
+configure.ac:
+AC_REQUIRE([AC_C_INLINE])
+AC_CHECK_FUNCS_ONCE([tcgetattr])
+
+Makefile.am:
+lib_SOURCES += term-style-control.c
+
+Include:
+"term-style-control.h"
+
+License:
+GPL
+
+Maintainer:
+Bruno Haible
-- 
2.7.4

>From a2a1b5b25afcc9d2f5ca0433d42d9b2994820a35 Mon Sep 17 00:00:00 2001
From: Bruno Haible <br...@clisp.org>
Subject: [PATCH 2/2] term-style-control: Add tests.

* tests/test-term-style-control-hello.c: New file.
* tests/test-term-style-control-yes.c: New file.
* modules/term-style-control-tests: New file.
---
 ChangeLog                             |   5 ++
 modules/term-style-control-tests      |  15 ++++
 tests/test-term-style-control-hello.c | 160 ++++++++++++++++++++++++++++++++++
 tests/test-term-style-control-yes.c   | 138 +++++++++++++++++++++++++++++
 4 files changed, 318 insertions(+)
 create mode 100644 modules/term-style-control-tests
 create mode 100644 tests/test-term-style-control-hello.c
 create mode 100644 tests/test-term-style-control-yes.c

diff --git a/ChangeLog b/ChangeLog
index 481db9d..cd2b500 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,10 @@
 2019-03-24  Bruno Haible  <br...@clisp.org>
 
+	term-style-control: Add tests.
+	* tests/test-term-style-control-hello.c: New file.
+	* tests/test-term-style-control-yes.c: New file.
+	* modules/term-style-control-tests: New file.
+
 	term-style-control: New module.
 	* lib/term-style-control.h: New file, based on libtextstyle's
 	term-ostream.oo.h and term-ostream.oo.c.
diff --git a/modules/term-style-control-tests b/modules/term-style-control-tests
new file mode 100644
index 0000000..e07f394
--- /dev/null
+++ b/modules/term-style-control-tests
@@ -0,0 +1,15 @@
+Files:
+tests/test-term-style-control-hello.c
+tests/test-term-style-control-yes.c
+
+Depends-on:
+stdbool
+unistd
+full-write
+
+configure.ac:
+
+Makefile.am:
+TESTS += test-term-style-control-hello
+check_PROGRAMS += test-term-style-control-hello
+noinst_PROGRAMS += test-term-style-control-yes
diff --git a/tests/test-term-style-control-hello.c b/tests/test-term-style-control-hello.c
new file mode 100644
index 0000000..32deb58
--- /dev/null
+++ b/tests/test-term-style-control-hello.c
@@ -0,0 +1,160 @@
+/* Simple test program for the term-style-control module.
+   Copyright (C) 2019 Free Software Foundation, Inc.
+   Written by Bruno Haible <br...@clisp.org>, 2019.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
+
+#include <config.h>
+
+/* Specification.  */
+#include "term-style-control.h"
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "full-write.h"
+
+/* This program outputs the line:     Hello Dr. Linus Pauling!
+   with underlining here:                   _________________
+   and a cyan background color here:            _____
+ */
+
+/* ECMA-48 / ISO 6429 escape sequences.  See
+   https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters
+ */
+static const char set_underline_on[] = "\033[4m";
+static const char set_underline_off[] = "\033[24m";
+static const char set_background_color_cyan[] = "\033[46m";
+static const char set_background_color_default[] = "\033[49m";
+
+struct term_style_user_data
+{
+  /* These fields are marked volatile, because they are accessed from the
+     async-safe function async_set_attributes_from_default.  */
+  bool volatile underline;
+  bool volatile background_color_cyan;
+
+  struct term_style_control_data ctrl_data;
+};
+
+static struct term_style_control_data *
+get_control_data (struct term_style_user_data *user_data)
+{
+  return &user_data->ctrl_data;
+}
+
+static void
+restore (struct term_style_user_data *user_data)
+{
+  fputs (set_underline_off, stdout);
+  fputs (set_background_color_default, stdout);
+  fflush (stdout);
+}
+
+static _GL_ASYNC_SAFE void
+async_restore (struct term_style_user_data *user_data)
+{
+  /* No <stdio.h> calls here!  */
+  full_write (STDOUT_FILENO, set_underline_off,
+              strlen (set_underline_off));
+  full_write (STDOUT_FILENO, set_background_color_default,
+              strlen (set_background_color_default));
+}
+
+static _GL_ASYNC_SAFE void
+async_set_attributes_from_default (struct term_style_user_data *user_data)
+{
+  /* No <stdio.h> calls here!  */
+  if (user_data->underline)
+    full_write (STDOUT_FILENO, set_underline_on,
+                strlen (set_underline_on));
+  if (user_data->background_color_cyan)
+    full_write (STDOUT_FILENO, set_background_color_cyan,
+                strlen (set_background_color_cyan));
+}
+
+static const struct term_style_controller controller =
+{
+  get_control_data,
+  restore,
+  async_restore,
+  async_set_attributes_from_default
+};
+
+int
+main (int argc, char *argv[])
+{
+  struct term_style_user_data user_data;
+
+  /* Initialization.  */
+  user_data.underline = false;
+  user_data.background_color_cyan = false;
+
+  activate_term_style_controller (&controller, &user_data, STDOUT_FILENO,
+                                  TTYCTL_AUTO);
+
+  /* As long as no styling is needed, we can stay in the default mode.  */
+  fputs ("Hello ", stdout);
+  fflush (stdout);
+
+  /* Before any styling, enable the non-default mode.  */
+  activate_term_non_default_mode (&controller, &user_data);
+
+  /* Set user_data.underline *before* emitting the appropriate
+     escape sequences, otherwise async_set_attributes_from_default will not
+     do its job correctly.  */
+  user_data.underline = true;
+  fputs (set_underline_on, stdout);
+  fflush (stdout);
+
+  fputs ("Dr. ", stdout);
+  fflush (stdout);
+
+  /* Set user_data.background_color_cyan *before* emitting the appropriate
+     escape sequences, otherwise async_set_attributes_from_default will not
+     do its job correctly.  */
+  user_data.background_color_cyan = true;
+  fputs (set_background_color_cyan, stdout);
+  fflush (stdout);
+
+  fputs ("Linus", stdout);
+  fflush (stdout);
+
+  user_data.background_color_cyan = false;
+  fputs (set_background_color_default, stdout);
+  fflush (stdout);
+
+  fputs (" Pauling", stdout);
+  fflush (stdout);
+
+  user_data.underline = false;
+  fputs (set_underline_off, stdout);
+  fflush (stdout);
+
+  /* Needed as a prerequisite of the deactivate_term_style_controller call
+     below.  */
+  deactivate_term_non_default_mode (&controller, &user_data);
+
+  fputs ("!\n", stdout);
+
+  /* If the user_data was allocated in heap memory, with indefinite extent,
+     this call would be optional.  But since we have allocated it on the
+     stack, we must deactivate it before it goes out of scope.  Otherwise
+     we get undefined behaviour in an atexit() handler.  */
+  deactivate_term_style_controller (&controller, &user_data);
+
+  return 0;
+}
diff --git a/tests/test-term-style-control-yes.c b/tests/test-term-style-control-yes.c
new file mode 100644
index 0000000..83432fd
--- /dev/null
+++ b/tests/test-term-style-control-yes.c
@@ -0,0 +1,138 @@
+/* Interactive test program for the term-style-control module.
+   Copyright (C) 2019 Free Software Foundation, Inc.
+   Written by Bruno Haible <br...@clisp.org>, 2019.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
+
+#include <config.h>
+
+/* Specification.  */
+#include "term-style-control.h"
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "full-write.h"
+
+/* This program outputs an endless amount of lines, each consisting of a
+   single 'y', in red color and underlined.
+   It can be used to exercise race conditions caused by
+     - simultaneous keyboard input on the terminal,
+     - pressing Ctrl-C,
+     - pressing Ctrl-Z and then "fg".  */
+
+/* ECMA-48 / ISO 6429 escape sequences.  See
+   https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters
+ */
+static const char set_underline_on[] = "\033[4m";
+static const char set_underline_off[] = "\033[24m";
+static const char set_foreground_color_red[] = "\033[31m";
+static const char set_foreground_color_default[] = "\033[39m";
+
+struct term_style_user_data
+{
+  /* This field is marked volatile, because it is accessed from the
+     async-safe function async_set_attributes_from_default.  */
+  bool volatile red_and_underline;
+
+  struct term_style_control_data ctrl_data;
+};
+
+static struct term_style_control_data *
+get_control_data (struct term_style_user_data *user_data)
+{
+  return &user_data->ctrl_data;
+}
+
+static void
+restore (struct term_style_user_data *user_data)
+{
+  fputs (set_underline_off, stdout);
+  fputs (set_foreground_color_default, stdout);
+  fflush (stdout);
+}
+
+static _GL_ASYNC_SAFE void
+async_restore (struct term_style_user_data *user_data)
+{
+  /* No <stdio.h> calls here!  */
+  full_write (STDOUT_FILENO, set_underline_off,
+              strlen (set_underline_off));
+  full_write (STDOUT_FILENO, set_foreground_color_default,
+              strlen (set_foreground_color_default));
+}
+
+static _GL_ASYNC_SAFE void
+async_set_attributes_from_default (struct term_style_user_data *user_data)
+{
+  /* No <stdio.h> calls here!  */
+  if (user_data->red_and_underline)
+    {
+      full_write (STDOUT_FILENO, set_underline_on,
+                  strlen (set_underline_on));
+      full_write (STDOUT_FILENO, set_foreground_color_red,
+                  strlen (set_foreground_color_red));
+    }
+}
+
+static const struct term_style_controller controller =
+{
+  get_control_data,
+  restore,
+  async_restore,
+  async_set_attributes_from_default
+};
+
+int
+main (int argc, char *argv[])
+{
+  struct term_style_user_data user_data;
+
+  /* Initialization.  */
+  user_data.red_and_underline = false;
+
+  activate_term_style_controller (&controller, &user_data, STDOUT_FILENO,
+                                  TTYCTL_AUTO);
+
+  for (;;)
+    {
+      /* Before any styling, enable the non-default mode.  */
+      activate_term_non_default_mode (&controller, &user_data);
+
+      /* Set user_data.red_and_underline *before* emitting the appropriate
+         escape sequences, otherwise async_set_attributes_from_default will not
+         do its job correctly.  */
+      user_data.red_and_underline = true;
+      fputs (set_underline_on, stdout);
+      fputs (set_foreground_color_red, stdout);
+      fflush (stdout);
+
+      fputs ("y", stdout);
+      fflush (stdout);
+
+      /* Revert to the default style before emitting a newline.  */
+      user_data.red_and_underline = false;
+      fputs (set_underline_off, stdout);
+      fputs (set_foreground_color_default, stdout);
+      fflush (stdout);
+
+      /* Optional.  */
+      deactivate_term_non_default_mode (&controller, &user_data);
+
+      fputs ("\n", stdout);
+      fflush (stdout);
+    }
+}
-- 
2.7.4

Reply via email to