//********************************************************************************
//* File       : AcEditFile.cpp                                                  *
//* Author     : Mahlon R. Smith                                                 *
//*              Copyright (c) 2023-2026 Mahlon R. Smith, The Software Samurai   *
//*                 GNU GPL copyright notice in AcEdit.cpp.                      *
//* Date       : 03-Apr-2026                                                     *
//* Version    : (see AppVersion string)                                         *
//*                                                                              *
//* Description: Support file for AcEdit class. Perform file management.         *
//*              a) Validate source files to be edited.                          *
//*              b) Manage temporary files.                                      *
//*              c) Manage clipboard access.                                     *
//*              d) Sandbox                                                      *
//*                                                                              *
//********************************************************************************

//*****************
//* Include Files *
//*****************
#include "AcEdit.hpp"         //* Application-class definition
#include <sys/stat.h>         //* file statistics

//***************
//* Definitions *
//***************
#if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0
extern ofstream ofsdbg ;
extern gString  gsdbg ;
#if DEBUG_ACE != 0
//* Names of controls within the application *
const char* fldId[fiFIELDS+1] = 
{
   "fiLINEvl", "fiLINEvr", "fiLINEhu", "fiLINEhl", // valid LineNumber controls
   "fiTEXTfw", "fiTEXTvl", "fiTEXTvr", "fiTEXThu", // valid TextEdit controls
   "fiTEXThl",
   "--      ",                                     // fiHDIV (IDLE_BUFF, unitialized)
   "??", "??", "??", "??", "??", "??", "??", // placeholders
   "--"                                  // unassigned
} ;
#endif   // DEBUG_ACE
#endif   // DEBUG_ANSICMD && DEBUG_LOG

//**************
//* Local data *
//**************

//********************
//* Local prototypes *
//********************
static fmFType DecodeFileType ( uint32_t mode ) ;
static void DecodeEpochTime ( int64_t eTime, localTime& ftTime ) ;
static short SetFilePermissions ( const char* pathBuff, mode_t newPermissions ) ;
static void rcfSort ( gString& gsSrc, int recs, int fldWid ) ;
const char* decodePermissions ( uint16_t mode, bool compress = false ) ;
const char* decodeFiletype ( fmFType ftype ) ;
static bool getEmulator ( gString& gsEmu, const char* tmpSpec = NULL ) ;


//*************************
//*     GetFileStats      *
//*************************
//********************************************************************************
//* Perform a 'stax' on the file specified by the full path string.              *
//* By default the stats for the specified file are returned, but if target is   *
//* a symbolic link, the symbolic link is followed i.e. stats for link target    *
//* are returned.                                                                *
//*                                                                              *
//*                                                                              *
//* Input  : tnFile : tnFName class object (by reference) to hold 'stat' data    *
//*          fPath  : full or partial path/filename specification                *
//*          getTarg: (optional, false by default) if true, symbolic links are   *
//*                   followed. Else, do not follow links                        *
//*                                                                              *
//* Returns: ZERO if successful, all data members of tnFile will be initialized  *
//*          Else returns system 'errno' value                                   *
//*            From 'realpath':                                                  *
//*            If errno == EACCES, at least one path component is not readable   *
//*            If errno == ENOENT, at least one path component does not exist    *
//*            From 'stat' i.e. 'stat64':                                        *
//*            If errno == ENOENT, file does not exist                           *
//*                                (or link target does not exist)               *
//*            If errno == EACCES, the process lacks permission for a container  *
//*                                directory                                     *
//*            If errno == ENOTDIR, an element of the path s/b a directory,      *
//*                                 but is not (unlikely)                        *
//*                                                                              *
//*           If error during system call, on return:                            *
//*            - tnFile.readAcc == false and all other members are undefined     *
//*             Exception:                                                       *
//*             If error is a broken symbolic link:                              *
//*             - tnFile.fName == name of link target if known, else "unknown"   *
//*             - tnFile.fSpec == full filespec if known, else "unknown"         *
//*             - tnFile.readAcc == false                                        *
//*             - tnFile.fType == fmTYPES                                        *
//*             - All other members of 'tnFile' are undefined                    *
//********************************************************************************

short AcEdit::GetFileStats ( tnFName& tnFile, const gString& fPath )
{
   gString gsfp, gsfn ;             // text management
   short status = ZERO ;            // return value

   //* Provides an orderly crash in case of failure *
   tnFile.fBytes = ZERO ;
   tnFile.fType = fmUNKNOWN_TYPE ;
   tnFile.readAcc = tnFile.writeAcc = false ;

   //* Convert the provided filename string to a full filespec. *
   //* Symbolic links _are_ followed. If error, status receives *
   //* 'errno' value.                                           *
   if ( (this->GetRealpath ( fPath.ustr(), gsfp, &status )) != false )
   {
      gsfp.copy( tnFile.fSpec, MAX_PATH ) ;     // save the full filespec
      this->ExtractFilename ( gsfn, gsfp ) ;
      gsfn.copy( tnFile.fName, MAX_FNAME ) ;    // isolate the target filename

      //* If successful 'stat' of the target, decode the raw stats. *
      if ( (status = statx ( sx_dirfd, tnFile.fSpec, sx_flags, sx_mask, &tnFile.rawStats )) == ZERO )
      {
         tnFile.fType = DecodeFileType ( tnFile.rawStats.stx_mode ) ; // file type
         tnFile.fBytes = tnFile.rawStats.stx_size ;    // number of bytes in file
         DecodeEpochTime ( (int64_t)tnFile.rawStats.stx_mtime.tv_sec,  // mod time
                           tnFile.modTime ) ;
         //* The fact that we can 'stat' the file does not necessarily mean    *
         //* that user has read access, so do the test to initialize 'readAcc'.*
         tnFile.readAcc = this->ReadPermission ( tnFile, gsfp ) ;
         //* Test for write access in case caller wants to modify/delete file. *
         tnFile.writeAcc = this->WritePermission ( tnFile, gsfp ) ;
      }
      else     // stat failed. return the 'errno' value (see note above)
         status = errno ;

   }     // realpath()

   //* 'realpath' failed. This is likely a broken symbolic link.*
   else
   {
      //* A broken link causes 'stat' to fail, but that is not actually  *
      //* an error. It is a a successful determination that the link     *
      //* target is missing or inaccessible. Find the path and file name *
      //* of the missing target if possible. (see notes above)           *
      this->BrokenLinkTargetPath ( fPath.ustr(), tnFile.fSpec, tnFile.fName ) ;
      tnFile.fType = fmTYPES ;   // indicate broken link
      tnFile.readAcc = false ;
      if ( (tnFile.fSpec[0] != NULLCHAR) && (tnFile.fName[0] != NULLCHAR) )
      {  // Re-constitute the full path
         gsfp.compose( "%s/%s", tnFile.fSpec, tnFile.fName ) ;
         gsfp.copy( tnFile.fSpec, MAX_PATH ) ;
      }
   }

   return status ;

}  //* End GetFileStats() *

//* Overload for const char* *
short AcEdit::GetFileStats ( tnFName& tnFile, const char* fPath )
{
   gString gs( fPath ) ;
   return ( (this->GetFileStats ( tnFile, gs )) ) ;

}  //* End GetFileStats() *

//*************************
//*      GetRealpath      *
//*************************
//********************************************************************************
//* Convert relative path to full filespec, resolving all symlinks, '.' and '..'.*
//*                                                                              *
//* Input  : rawPath : relative file path to be resolved                         *
//*          fSpec   : (by reference) receives resolved filespec                 *
//*          errnoPtr: (optional, default: null pointer) if call to realpath()   *
//*                    fails pointer target receives 'errno' value               *
//*                    value if                                                  *
//* Returns: 'true'  if filespec fully resolved,                                 *
//*          'false' if filespec incomplete/unknown ('fSpec' contents undefined) *
//********************************************************************************
//* Technical Note:                                                              *
//* The C library 'realpath' function is unable to convert the '~' (tilde)       *
//* character to the equivalent "home" directory filespec.                       *
//* If the tilde is the leading character in the 'rawPath' parameter, this       *
//* method manually replaces the tilde with the value of the "$HOME" environment *
//* variable.                                                                    *
//* NOTE: In the unlikely event that "$HOME" is not defined in the environment,  *
//* it is likely that realpath() will return an invalid filespec.                *
//* Errors Returned by realpath():                                               *
//* ‘EACCES’ (13) The process does not have search permission for a directory    *
//*               component of the file name.                                    *
//* 'ENOENT' ( 2) This error is reported when a file referenced as a directory   *
//*               component in the file name doesn't exist, or when a component  *
//*               is a symbolic link whose target file does not exist.           *
//********************************************************************************

bool AcEdit::GetRealpath ( const char* rawPath, gString& fSpec, short* errnoPtr )
{
   static char homeDir[MAX_FNAME] = { "\0" } ;
   char fullPath[MAX_PATH] ;        // full path/filename
   gString gs( rawPath ) ;          // work buffer
   bool status = false ;            // return value

   if ( (gs.find( L'~' )) == ZERO )
   {
      if ( *homeDir == '\0' )
      {
         gString gstmp( (secure_getenv("HOME")) ) ;
         if ( (gstmp.gschars()) > 1 )
            gstmp.copy( homeDir, MAX_FNAME ) ;
      }
      if ( *homeDir != NULLCHAR )
         gs.replace( L"~", homeDir ) ;
   }
   if ( (realpath ( gs.ustr(), fullPath )) != NULL )
   {
      fSpec = fullPath ;
      if ( errnoPtr != NULL )
         *errnoPtr = ZERO ;
      status = true ;
   }
   else if ( errnoPtr != NULL )
      *errnoPtr = errno ;
   return status ;

}  //* End GetRealpath() *

//*************************
//*    DecodeFileType     *
//*************************
//********************************************************************************
//* Extract the file type from the "st_mode" element of the stat{} structure     *
//* and return the equivalent member of enum fmFType.                            *
//*                                                                              *
//* PRIVATE METHOD.                                                              *
//*                                                                              *
//* The POSIX standard defines certain macros to access the file "st_mode".      *
//* There are several layers to the macros, but for Linux EXT2, EXT3 we          *
//* finally get down to the actual bits in /usr/include/bits/stat.h:             *
//* See also page 135 of "Linux Programming by Example."                         *
//* The actual macros that use these bitmasks are used in this routine.          *
//*                                                                              *
//* Encoding of the file mode.                                                   *
//* #define	__S_IFMT	0170000	/* These bits determine file type.                 *
//* File types.                                                                  *
//* #define	__S_IFDIR	0040000	 Directory.                                     *
//* #define	__S_IFCHR	0020000	 Character device.                              *
//* #define	__S_IFBLK	0060000	 Block device.                                  *
//* #define	__S_IFREG	0100000	 Regular file.                                  *
//* #define	__S_IFIFO	0010000	 FIFO.                                          *
//* #define	__S_IFLNK	0120000	 Symbolic link.                                 *
//* #define	__S_IFSOCK	0140000	 Socket.                                        *
//*                                                                              *
//* Input Values : mode: st_mode field of file's stat{} strucutre                *
//*                                                                              *
//* Return Value : member of enum fmFType                                        *
//********************************************************************************

static fmFType DecodeFileType ( uint32_t mode )
{
   fmFType  ftype = fmUNKNOWN_TYPE ;

   if ( (S_ISREG(mode)) != false )        ftype = fmREG_TYPE ;
   else if ( (S_ISDIR(mode)) != false )   ftype = fmDIR_TYPE ;
   else if ( (S_ISLNK(mode)) != false )   ftype = fmLINK_TYPE ;
   else if ( (S_ISCHR(mode)) != false )   ftype = fmCHDEV_TYPE ;
   else if ( (S_ISBLK(mode)) != false )   ftype = fmBKDEV_TYPE ;
   else if ( (S_ISFIFO(mode)) != false )  ftype = fmFIFO_TYPE ;
   else if ( (S_ISSOCK(mode)) != false )  ftype = fmSOCK_TYPE ;
   
   return ftype ;
   
}  //* End DecodeFileType() *

//*************************
//*    ExtractFilename    *
//*************************
//********************************************************************************
//* Given a string that may contain one of the following:                        *
//*  - a filename                                                                *
//*  - a relative path/filename                                                  *
//*  - an absolute path/filename                                                 *
//* extract the filename string from the input string and return it to caller.   *
//*                                                                              *
//* Input  : nameBuff: buffer to hold filename                                   *
//*          pfSource: source filename or path/filename                          *
//*                                                                              *
//* Returns: 'true' if filename successfully isolated                            *
//*          'false' in special case of pfSource=="/" (at root directory)        *
//*                  OR if invalid input                                         *
//********************************************************************************
//* Programmer's Note: It is assumed that caller has sent us a valid filespec    *
//* in pfSource. If not, the source will be returned in the target.              *
//********************************************************************************

bool AcEdit::ExtractFilename ( gString& nameBuff, const gString& pfSource )
{
   bool status = false ;      // return value

   short fIndx = (pfSource.findlast( fSLASH )) + 1 ;
   nameBuff = &pfSource.gstr()[fIndx] ;
   if ( ((nameBuff.gschars()) > 2) ||
        (((nameBuff.gschars()) == 2) && (*nameBuff.gstr() != fSLASH)) )
   {
      status = true ;
   }
   return status ;

}  //* End ExtractFilename() *
bool AcEdit::ExtractFilename ( char nameBuff[MAX_FNAME], const gString& pfSource )
{
   bool status = false ;      // return value

   gString gs ;
   status = this->ExtractFilename ( gs, pfSource ) ;
   gs.copy( nameBuff, MAX_FNAME ) ;

   return status ;

}  //* End ExtractFilename() *

//*************************
//*    ExtractPathname    *
//*************************
//********************************************************************************
//* Given a string that contains a full path/filename specification, extract     *
//* the pathname, i.e. truncate the string to remove the filename.               *
//*                                                                              *
//*                                                                              *
//* Input  : pathBuff: buffer to contain path string                             *
//*          pfSource: full path/filename specification                          *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* Programmer's Note: It is assumed that caller has sent us a valid filespec    *
//* in pfSource. If not, the source will be returned in the target.              *
//********************************************************************************

void AcEdit::ExtractPathname ( gString& pathBuff, const gString& pfSource )
{
   pathBuff = pfSource ;
   short indx = pathBuff.findlast( fSLASH ) ;
   if ( indx >= ZERO )
   {
      if ( indx == ZERO )     // if pathname == root directory
         ++indx ;
      pathBuff.limitChars( indx ) ;
   }

}  //* End of ExtractPathname() *
void AcEdit::ExtractPathname ( char pathBuff[MAX_PATH], const gString& pfSource )
{
   gString gsp ;
   this->ExtractPathname ( gsp, pfSource ) ;
   gsp.copy( pathBuff, MAX_PATH ) ;

}  //* End of ExtractPathname() *

//*************************
//*    DecodeEpochTime    *
//*************************
//********************************************************************************
//* Convert GNU/UNIX epoch time code (seconds since the epoch: Jan. 01, 1970)    *
//* to local time in a structured format (class localTime).                      *
//*                                                                              *
//* See the info page for the 'stat' command for more information.               *
//* See also the notes in DirEntryDisplayFormat() above.                         *
//*                                                                              *
//* Input  : eTime  : epoch time (this is a signed, 64-bit value)                *
//*                   (See note in definition of localTime class.)               *
//*          ftTime : (by reference, initial values ignored)                     *
//*                                                                              *
//* Returns: nothing, but all members of ftTime will be initialized              *
//********************************************************************************
//* System conversion from time_t epoch to local time is returned in this        *
//* structure.                                                                   *
//*                                                                              *
//*  struct tm                                                                   *
//*  {                                                                           *
//*     int  tm_sec ;          // 0-59  (leap seconds to 60)                     *
//*     int  tm_min ;          // 0-59                                           *
//*     int  tm_hour ;         // 0-23                                           *
//*     int  tm_mday ;         // 1-31                                           *
//*     int  tm_mon ;          // 0-11                                           *
//*     int  tm_year ;         // since 1900                                     *
//*     int  tm_wday ;         // 0-6                                            *
//*     int  tm_yday ;         // 0-365                                          *
//*     int  tm_isdst ;        // >0 == is DST, 0 == not DST, <0 == unknown      *
//*                                                                              *
//*   # ifdef __USE_MISC       // defs from 4.3BSD and System V Unix (features.h)*
//*     long int tm_gmtoff;    // Seconds east of UTC. (negative values, West)   *
//*     const char *tm_zone;   // Timezone abbreviation.                         *
//*   # else  // (not used)                                                      *
//*     long int __tm_gmtoff;  // Seconds east of UTC.                           *
//*     const char *__tm_zone; // Timezone abbreviation.                         *
//*   # endif                                                                    *
//*  } ;                                                                         *
//*                                                                              *
//* -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - *
//* Note that we do a bit of defensive programming in anticipation of the        *
//* dreaded year 2038 overflow of the 32-bit time_t type.                        *
//* -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - *
//* Daylight Savings Time:                                                       *
//* ----------------------                                                       *
//* Because we use this method to convert file timestamps, the DST calculation   *
//* done by 'localtime_r' throws the time off by an hour during the DST          *
//* part of the year. Unfortunately it does not do this uniformly. Files on      *
//* Linux filesystems drop back an hour (3,600 seconds), while files on          *
//* non-Linux filesystem such as VFAT do not drop back. What's up with that?     *
//* Presumably because VFAT does not include TZ data?                            *
//*                                                                              *
//* It's actually much worse than that. The system dynamically MODIFIES the      *
//* reported mod and stat timestamps for all files based on DST.                 *
//* This means that localtime_r is doing its job properly, but is working with   *
//* bad data sent from the kernel for filesystems that have DST offsets.         *
//* We're pretty sure this isn't the whole story but we don't have enough        *
//* information at this time to solve it. See the 'zdump' utility which lists    *
//* the offsets in a zoneinfo file:                                              *
//*         zdump -v /etc/localtime                                              *
//*         zdump -v /usr/share/zoneinfo/America/New_York | grep '2025 E.T'      *
//* This yields:                                                                 *
//* Sun Mar  9 06:59:59 2025 UT =                                                *
//* Sun Mar  9 01:59:59 2025 EST isdst=0 gmtoff=-18000                           *
//* Sun Mar  9 07:00:00 2025 UT =                                                *
//* Sun Mar  9 03:00:00 2025 EDT isdst=1 gmtoff=-14400                           *
//* Sun Nov  2 05:59:59 2025 UT =                                                *
//* Sun Nov  2 01:59:59 2025 EDT isdst=1 gmtoff=-14400                           *
//* Sun Nov  2 06:00:00 2025 UT =                                                *
//* Sun Nov  2 01:00:00 2025 EST isdst=0 gmtoff=-18000                           *
//*                                                                              *
//* It seems that file timestamps are in UTC, and the system applies the offset  *
//* listed in the specified zoneinfo file (/etc/localtime). If this is true,     *
//* then why is one offset applied to ext4, and a different offset applied       *
//* to VFAT filesystem (SD card)?                                                *
//* What EXACTLY is the difference that causes the DST offset to be applied in   *
//* one case and the standard time offset to be applied in the other?            *
//*  -- the 'tm_isdst' value seems to be > ZERO for both                         *
//*  -- 'tm_gmtoff' value ?                                                      *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//* "To disable DST changes, link your /etc/localtime file to one of zoneinfo    *
//* files placed under the folder /usr/share/zoneinfo/Etc/"                      *
//* "Zones like Etc/GMT+6 are intentionally reversed for backwards               *
//* compatibility with POSIX standards"                                          *
//*   Example command:                                                           *
//*         ln -s /usr/share/zoneinfo/Etc/GMT+3 /etc/localtime                   *
//* Note that the 'GMT' files do not contain DST offsets. Instead, they contain  *
//* two epoch values with the second value exactly 24 hours (24*60*60 seconds)   *
//* ahead of the first. What's up with that?                                     *
//*                                                                              *
//* Because this development machine is on New York time (-5:00 i.e. 5 hours     *
//* west of UTC, -4:00DST), and because the GMT files are "intentionally         *
//* reversed," our setting would be:                                             *
//*         ln -s /usr/share/zoneinfo/Etc/GMT+5 /etc/localtime                   *
//* Be aware, however, that this doesn't actually resolve the issue. It simply   *
//* sets BOTH vfat and ext4 timestamps back an _additional_ hour.                *
//*                                                                              *
//* If the kernel would tell us the truth, we would consider using ctime_r       *
//* which converts the time_t value to a string, which we would then scan for    *
//* the values, but since the kernel is a lying SOS, this won't help us.         *
//*                                                                              *
//********************************************************************************

static void DecodeEpochTime ( int64_t eTime, localTime& ftTime )
{
   ftTime.reset() ;           // initialize caller's data
   if ( eTime >= ZERO )       // defend against stupidity
   {  //* Width of time_t is system dependent *
      ftTime.epoch = eTime ;
      ftTime.sysepoch = (time_t)eTime ;   // (possible narrowing)

      //* Receives the broken-down time.              *
      Tm bdt ;       // receives broken-down time
      if ( (localtime_r ( &ftTime.sysepoch, &bdt )) != NULL )
      {
         //* Translate to localTime format *
         ftTime.date    = bdt.tm_mday ;         // today's date
         ftTime.month   = bdt.tm_mon + 1 ;      // month
         ftTime.year    = bdt.tm_year + 1900 ;  // year
         ftTime.hours   = bdt.tm_hour ;         // hour
         ftTime.minutes = bdt.tm_min ;          // minutes
         ftTime.seconds = bdt.tm_sec ;          // seconds
         ftTime.day     = bdt.tm_wday ;         // day-of-week (0 == Sunday)
         ftTime.julian  = bdt.tm_yday ;         // Julian date (0 == Jan.01)

         #if DECODE_TZ != 0   // time-zone data
         ftTime.gmtoffset = bdt.tm_gmtoff ;     // offset as number of seconds
         ftTime.dst = (bdt.tm_isdst > ZERO ) ? true : false ; // DST in effect

         #if 1    // additional time zone data (not very useful) *
         gString gs( bdt.tm_zone ) ;
         gs.copy( ftTime.timezone, ltFMT_LEN ) ;
         int absOffset = abs(ftTime.gmtoffset) ;
         int hOffset = absOffset / (60*60),     // convert offset seconds to 
             mOffset = absOffset % (60*60) ;    // hours and minutes
         if ( ftTime.gmtoffset < ZERO )
            hOffset = -(hOffset) ;
         gs.compose( L"UTC%+d:%02d", &hOffset, &mOffset ) ;
         gs.copy( ftTime.utc_zone, ltFMT_LEN ) ;
         #endif   // additional time zone data
         #endif   // DECODE_TZ
      }
   }

}  //* End DecodeEpochTime() *

//*************************
//*    ReadPermission     *
//*************************
//********************************************************************************
//* Determine whether user has 'read' permission for the specified file,         *
//* and optionally enable owner/group read permission.                           *
//*                                                                              *
//* IMPORTANT NOTE:                                                              *
//* True read permission on a directory name actually requires BOTH 'Read' and   *
//* 'Execute' permission.                                                        *
//* a) 'Execute' only, will allow us to set the directory to CWD, but            *
//*    will not allow us to read its contents.                                   *
//* b) 'Read' only, will allow us to read the NAMES of the files contained in    *
//*    the directory (but NOT the other stats), but will not allow us to set     *
//*    the directory as the CWD.                                                 *
//* Therefore, if caller wants a report of read access on a directory name,      *
//* we indicate read access ONLY if user has both 'Read' and 'Execute'           *
//* permission. If caller wants to enable read access on a directory name, we    *
//* enable BOTH 'Read' and 'Execute' permissions.                                *
//*                                                                              *
//* Input : sStats: (by reference) stats of file to be tested                    *
//*                 sStats.readAcc will be set/reset according to result         *
//*                 and if 'enable', then st_mode bits will be updated,          *
//*                 other members remain unchanged                               *
//*         sPath : path/filename of file to be tested                           *
//*         enable: (optional, false by default)                                 *
//*                 if 'true', enable 'owner' and 'group' read access            *
//*                 if 'false', simply report user's current read access         *
//*                                                                              *
//* Returns: 'true' if user has read permission, else 'false'                    *
//********************************************************************************
//* Programmer's Note: The 'access' library function is very good in most        *
//* respects in that it accurately mimics true admin tests of access; however,   *
//* it follows symbolic links, rather than reporting the permission on the       *
//* link file itself. For this reason, IF !'enable' we do a direct check on      *
//* the permission bits of symbolic-link source files. This is necessary,        *
//* since the caller wants to know if it's ok to copy the symlink, NOT the       *
//* link target.                                                                 *
//*                                                                              *
//* However, if caller wants to enable read access on a protected file, the      *
//* enable will be done on the link target, since the permission bits for the    *
//* link file are meaningless in that case.                                      *
//*                                                                              *
//********************************************************************************

bool AcEdit::ReadPermission ( tnFName& sStats, const gString& sPath, bool enable )
{

   return ( (this->ReadPermission ( sStats, sPath.ustr(), enable )) ) ;

}  //* End ReadPermission() *

bool AcEdit::ReadPermission ( tnFName& sStats, const char* sPath, bool enable )
{
   //* Determine user's read access *
   if ( (sStats.fType != fmLINK_TYPE) || (sStats.fType == fmLINK_TYPE && enable) )
   {
      sStats.readAcc = bool((access ( sPath, R_OK )) == ZERO) ;
      if ( sStats.readAcc != false && sStats.fType == fmDIR_TYPE )
         sStats.readAcc = bool((access ( sPath, X_OK )) == ZERO) ;

      if ( !sStats.readAcc && enable != false )
      {
         mode_t sMode = sStats.rawStats.stx_mode ;
         if ( sStats.fType == fmLINK_TYPE )
         {  //* Get link target info *
            tnFName ltrgStats ;
            gString ltrgPath ;
            if ( (GetLinkTargetStats ( sPath, ltrgStats, ltrgPath )) == ZERO )
               sMode = ltrgStats.rawStats.stx_mode ;
            if ( ltrgStats.fType == fmDIR_TYPE )
               sMode |= (S_IXUSR | S_IXGRP) ;
         }
         else if ( sStats.fType == fmDIR_TYPE )
            sMode |= (S_IXUSR | S_IXGRP) ;
         sMode |= (S_IRUSR | S_IRGRP) ;
         if ( (SetFilePermissions ( sPath, sMode )) == ZERO )
         {
            sStats.rawStats.stx_mode = sMode ;
            sStats.readAcc = true ;
         }
      }
   }
   else
   {  //* Source file is a symbolic link - report only *
      mode_t sMode = sStats.rawStats.stx_mode ;
      bool   usrRE = bool((sMode & S_IRUSR) != ZERO), 
             grpRE = bool((sMode & S_IRGRP) != ZERO), 
             othRE = bool((sMode & S_IROTH) != ZERO) ;
      sStats.readAcc = false ;      // assume no read access

      //* If universal 'read' permission OR if user is Super-User *
      if ( othRE || this->userInfo.userID == superUID )
         sStats.readAcc = true ;

      //* If user is file's owner AND owner 'read' permission *
      else if ( usrRE && sStats.rawStats.stx_uid == userInfo.userID )
         sStats.readAcc = true ;

      //* If group 'read' permission, determine  *
      //* whether user is a member of that group *
      else if ( grpRE )
      {
         gid_t grpID = sStats.rawStats.stx_gid ;
         //* If file group is user's group AND group 'read' permission *
         if ( grpID == userInfo.grpID )
            sStats.readAcc = true ;
         //* If user is a member of the file's group * 
         else
         {
            for ( short i = ZERO ; i < userInfo.suppGroups ; i++ )
            {
               if ( userInfo.suppGID[i] == grpID )
               {
                  sStats.readAcc = true ;
                  break ;
               }
            }
         }
      }
   }
   return sStats.readAcc ;

}  //* End ReadPermission() *

//*************************
//*    WritePermission    *
//*************************
//********************************************************************************
//* Determine whether user has 'write' permission for the specified file,        *
//* and optionally enable owner/group write permission.                          *
//*                                                                              *
//* Input : sStats: (by reference) stats of file to be tested                    *
//*                 sStats.writeAcc will be set/reset according to result        *
//*                 and if 'enable', then st_mode bits will be updated,          *
//*                 other members remain unchanged                               *
//*         sPath : path/filename of file to be tested                           *
//*         enable: (optional, false by default)                                 *
//*                 if 'true', enable 'owner' and 'group' write access           *
//*                 if 'false', simply report user's current write access        *
//*                                                                              *
//* Returns: 'true' if user has write permission, else 'false'                   *
//*******************************************************************************
//* Programmer's Note: The 'access' library function is very good in most        *
//* respects in that it accurately mimics true admin tests of access; however,   *
//* it follows symbolic links, rather than reporting the permission on the       *
//* link file itself. For this reason, IF !'enable' we do a direct check on      *
//* the permission bits of symbolic-link source files. This is necessary,        *
//* since the caller wants to know if it's ok to modify/delete the symlink,      *
//* NOT the link target.                                                         *
//*                                                                              *
//* However, if caller wants to enable write access on a protected file, the     *
//* enable will be done on the link target, since the permission bits for the    *
//* link file are meaningless in that case.                                      *
//********************************************************************************

bool AcEdit::WritePermission ( tnFName& sStats, const gString& sPath, bool enable )
{

   return WritePermission ( sStats, sPath.ustr(), enable ) ;

}  //* End WritePermission() *

bool AcEdit::WritePermission ( tnFName& sStats, const char* sPath, bool enable )
{
   //* Determine user's write access *
   if ( (sStats.fType != fmLINK_TYPE) || (sStats.fType == fmLINK_TYPE && enable) )
   {
      sStats.writeAcc = bool((access ( sPath, W_OK )) == ZERO) ;

      if ( !sStats.writeAcc && enable != false )
      {
         mode_t sMode = sStats.rawStats.stx_mode ;
         if ( sStats.fType == fmLINK_TYPE )
         {  //* Get link target info *
            tnFName ltrgStats ;
            gString ltrgPath ;
            if ( (GetLinkTargetStats ( sPath, ltrgStats, ltrgPath )) == ZERO )
               sMode = ltrgStats.rawStats.stx_mode ;
         }
         sMode |= (S_IWUSR | S_IWGRP) ;
         if ( (SetFilePermissions ( sPath, sMode )) == ZERO )
         {
            sStats.rawStats.stx_mode = sMode ;
            sStats.writeAcc = true ;
         }
      }
   }
   else
   {  //* Source file is a symbolic link - report only *
      mode_t sMode = sStats.rawStats.stx_mode ;
      bool   usrWE = bool((sMode & S_IWUSR) != ZERO), 
             grpWE = bool((sMode & S_IWGRP) != ZERO), 
             othWE = bool((sMode & S_IWOTH) != ZERO) ; 
      sStats.writeAcc = false ;     // assume no write accees

      //* If universal 'write' permission OR if user is Super-User *
      if ( othWE || this->userInfo.userID == superUID )
         sStats.writeAcc = true ;

      //* If user is file's owner AND owner 'write' permission *
      else if ( usrWE && sStats.rawStats.stx_uid == userInfo.userID )
         sStats.writeAcc = true ;

      //* If group 'write' permission, determine *
      //* whether user is a member of that group *
      else if ( grpWE )
      {
         gid_t grpID = sStats.rawStats.stx_gid ;
         //* If file group is user's group AND group 'write' permission *
         if ( grpID == userInfo.grpID )
            sStats.writeAcc = true ;
         //* If user is a member of the file's group * 
         else
         {
            for ( short i = ZERO ; i < userInfo.suppGroups ; i++ )
            {
               if ( userInfo.suppGID[i] == grpID )
               {
                  sStats.writeAcc = true ;
                  break ;
               }
            }
         }
      }
   }
   return sStats.writeAcc ;

}  //* End WritePermission() *

#if 0    // CURRENTLY UNUSED
//*************************
//*  ExecutePermission    *
//*************************
//********************************************************************************
//* Determine whether user has 'execute' permission for the specified file,      *
//* and optionally set file as executable for owner/group/others.                *
//* This method follows symbolic links and reports or modifies the link-target   *
//* file (if it exists).                                                         *
//*                                                                              *
//* Input : sPath : path/filename of file to be modified                         *
//*         enable: if 'true', enable 'execute' permission                       *
//*                 if 'false', simply report current 'executable' status        *
//*                                                                              *
//* Returns: 'true' if user has execute permission                               *
//*          'false' if no execute permission OR if file does not exist          *
//********************************************************************************
//* Programmer's Note: Symbolic links are executable by nature. It is the        *
//* link target's permissions that are of interest.                              *
//*                                                                              *
//********************************************************************************

static bool ExecutePermission ( const gString& sPath, bool enable = false ) ;
static bool ExecutePermission ( const gString& sPath, bool enable )
{
   bool hasPermission = false ;
   if ( (access ( sPath.ustr(), F_OK )) == ZERO)   // if file exists
   {
      hasPermission = bool((access ( sPath.ustr(), X_OK )) == ZERO) ;

      //* If we were asked to enable exec permission,*
      //* AND user doesn't already have it           *
      if ( enable && ! hasPermission )
      {
         FileStats rawStats ;
         if ( (stat64 ( sPath.ustr(), &rawStats )) == ZERO )
         {
            mode_t newMode = rawStats.st_mode | (S_IXUSR | S_IXGRP | S_IXOTH) ;
            if ( (SetFilePermissions ( sPath.ustr(), newMode )) == ZERO )
               hasPermission = true ;
         }
      }
   }
   return hasPermission ;

}  //* End ExecutePermission() *
#endif   // CURRENTLY UNUSED

//*************************
//*  SetFilePermissions   *
//*************************
//********************************************************************************
//* Set file permissions for the specified file: USR rwx, GRP rwx, OTHER rwx,    *
//* sUID, sGID, and sticky bit.                                                  *
//* (requires either superuser access or ownership of the file.)                 *
//* See <sys/stat.h> and <bits/stat.h> for mask definitions.                     *
//* NOTE: For a symbolic link source file, setting or resetting permissions      *
//*       always occurs for the link target, not the link itself.                *
//*                                                                              *
//* Input  : pathBuff: full path/filename specification                          *
//*          newMode : encoded bit word (unsigned int)                           *
//*                                                                              *
//* Returns: ZERO if successful, else code from 'errno'.                         *
//********************************************************************************

static short SetFilePermissions ( const char* pathBuff, mode_t newPermissions )
{
   short status = chmod ( pathBuff, newPermissions ) ;

   if ( status != ZERO )
      status = errno ;

   return status ;

}  //* End SetFilePermissions() *

//*************************
//*  GetLinkTargetStats   *
//*************************
//********************************************************************************
//* For a symbolic link source file, return the stat information for the         *
//* file the sym-link points to i.e. the sym-link target.                        *
//*                                                                              *
//*                                                                              *
//* Input  : lnkPath  : full path/filename specification of symbolic link file   *
//*          ltrgStats: (by reference, initial contents ignored)                 *
//*                     tnFName class object (by reference) to hold 'stat' data  *
//*          ltrgPath : (by reference, initial contents ignored)                 *
//*                     receives absolute path/filename of link-target file      *
//*                                                                              *
//* Returns: OK if successful,                                                   *
//*            - ltrgPath gets the absolute path/filename of link target file.   *
//*            - all data members of ltrgStats will be initialized               *
//*          Else if specified source file is not a symbolic link, the errno     *
//*            code EINVAL (invalid argument) is returned.                       *
//*            - ltrgPath is initialized to "unknown"                            *
//*            - ltrgStats.readAcc == false and all other members are undefined  *
//*          Else system 'errno' value is returned                               *
//*            If error during system call:                                      *
//*            - ltrgPath is initialized to "unknown"                            *
//*            - ltrgStats.readAcc == false and all other members are undefined  *
//*               Exception:                                                     *
//*               If the cause of the error is a broken symbolic link i.e.       *
//*               (link target file not found), then:                            *
//*               - ltrgPath == path/filename of missing link target if known,   *
//*                 else "unknown"                                               *
//*               - ltrgStats.fName == name of missing link target if known,     *
//*                 else "unknown"                                               *
//*               - ltrgStats.readAcc == false                                   *
//*               - ltrgStats.fType == fmTYPES                                   *
//*               - All other members of ltrgStats are undefined                 *
//********************************************************************************

short AcEdit::GetLinkTargetStats ( const gString& lnkPath, 
                                   tnFName& ltrgStats, gString& ltrgPath )
{
   FileStats   lnkStats ;
   short       success ;

   //* Provides an orderly crash in case of failure *
   ltrgPath = "unknown" ;
   ltrgPath.copy( ltrgStats.fName, MAX_FNAME ) ;
   ltrgStats.fBytes = ZERO ;
   ltrgStats.fType = fmUNKNOWN_TYPE ;
   ltrgStats.readAcc = ltrgStats.writeAcc = false ;

   if ( (success = statx ( sx_dirfd, lnkPath.ustr(), sx_lflags, sx_mask, 
                           &lnkStats )) == ZERO )
   {
      //* Get the file type. If a symbolic link, stat the target file *
      if ( (DecodeFileType ( lnkStats.stx_mode )) == fmLINK_TYPE )
      {
         char tmpPath[MAX_PATH] ;   // full path/filename of link target
         //* Follow symbolic link to stats for target file *
         if ( (success = statx ( sx_dirfd, lnkPath.ustr(), sx_flags, sx_mask, 
                                 &ltrgStats.rawStats )) == ZERO )
         {
            //* Get the decoded path/filename of the link-target file *
            if ( (realpath ( lnkPath.ustr(), tmpPath )) != NULL )
            {
               ltrgPath = tmpPath ;          // store target file's path/filename
               //* Extract filename from target file's path/filename
               this->ExtractFilename ( ltrgStats.fName, ltrgPath ) ;
               //* Format the stats *
               ltrgStats.fBytes = ltrgStats.rawStats.stx_size ;
               ltrgStats.fType = DecodeFileType ( ltrgStats.rawStats.stx_mode ) ;
               DecodeEpochTime ( (int64_t)ltrgStats.rawStats.stx_mtime.tv_sec, ltrgStats.modTime ) ;
               ltrgStats.readAcc = this->ReadPermission ( ltrgStats, ltrgPath.ustr() ) ;
               ltrgStats.writeAcc = this->WritePermission ( ltrgStats, ltrgPath ) ;
            }
            else
            {
               BrokenLinkTargetPath ( lnkPath.ustr(), tmpPath, ltrgStats.fName ) ;
               ltrgStats.fType = fmTYPES ;// indicate broken link
               ltrgStats.readAcc = false ;
            }
         } 
         else  // stat on link target failed
         {
            //* A broken link causes 'stat' to fail, but that is not an error  *
            //* that should be reported. It is actually a successful           *
            //* determination that the link target is missing or inaccessible. *
            //* Find the path and file name of the missing target if possible. *
            BrokenLinkTargetPath ( lnkPath.ustr(), tmpPath, ltrgStats.fName ) ;
            ltrgStats.fType = fmTYPES ;// indicate broken link
            ltrgStats.readAcc = false ;
         }
      }
      else  // source file is not a symbolic link file
         success = EINVAL ;
   }

   return success ;

}  //* End GetLinkTargetStats() *

//*************************
//* BrokenLinkTargetPath  *
//*************************
//********************************************************************************
//* Retrieve the path/filename of missing target for a broken symbolic link.     *
//*                                                                              *
//* Input  : linkPath: full path/filename specification of symbolic link file    *
//*          trgPath : receives target path (not filename) if available          *
//*          trgName : receives target filename if available                     *
//*                                                                              *
//* Returns: none                                                                *
//*          (if unable to retrieve the data, then both buffers get "unknown")   *
//********************************************************************************
//* Programmer's Note: Contents of link file may be relative path OR absolute    *
//* path OR upgefuched. We return the absolute path if possible. If 'realpath'   *
//* returns NULL, we may still have the path IF errno == EACCES ||               *
//* errno == ENOENT. If contents of link file are corrupted, then we will        *
//* unfortunately return garbage.                                                *
//********************************************************************************

void AcEdit::BrokenLinkTargetPath ( const char* linkPath, 
                                    char trgPath[MAX_PATH], char trgName[MAX_FNAME] )
{
   static const char* cantDoIt = "unknown" ;
   char tmpPath[MAX_PATH] ;
   short status = -1 ;

   //* Read the contents of the symbolic link *
   short bytesIn = readlink ( linkPath, tmpPath, (MAX_PATH-1) ) ;
   if ( bytesIn > ZERO )
   {
      tmpPath[bytesIn] = NULLCHAR ;    // be sure string is terminated
      const char* rpRet = realpath ( tmpPath, trgPath ) ;
      short rpErrno = errno ;
      if ( rpRet != NULL || (rpErrno == EACCES || rpErrno == ENOENT) )
      {
         gString gs( trgPath ) ;
         this->ExtractPathname ( trgPath, gs ) ;
         this->ExtractFilename (trgName, gs ) ;
         if ( *trgPath != NULLCHAR && *trgName != NULLCHAR )
            status = ZERO ;
      }
   }
   if ( status != ZERO )
   {  //* Path retrieval unsuccessful, use default strings *
      gString gs( cantDoIt ) ;
      gs.copy( trgPath, MAX_PATH ) ;
      gs.copy( trgName, MAX_FNAME ) ;
   }

}  //* End BrokenLinkTargetPath() *

//*************************
//*      isTextfile       *
//*************************
//********************************************************************************
//* Determine whether the specified file is a text file or a binary file.        *
//* Calls the 'file' utility which scans for filesystem markers, magic numbers,  *
//* and various text tests (UTF-8-16-32, etc.)                                   *
//*                                                                              *
//* Note the redirection syntax: 'stderr' (2) is redirected to wherever          *
//* 'stdout' (1) is currently pointing, which in this case is a temporary file.  *
//*                                                                              *
//* Input  : gsTrg : (by reference) target filespec                              *
//*                                                                              *
//* Returns: 'true' if target file contains only text,                           *
//*           else 'false' (file not plain text data, OR system call failed)     *
//*                 Note that if user does not have read access, test will fail. *
//********************************************************************************
//* Programmer's Note: Because this method is invoked both before and after the  *
//* ACWin-class object has been allocated, a test is performed to determine      *
//* which temp-file manager is active.                                           *
//********************************************************************************

bool AcEdit::isTextfile ( const gString& gsTrg )
{
   //* 'file' switches:                                               *
   //*      --brief         brief output (report only file type info) *
   //*      --mime          report canonical MIME-type string         *
   //*      --dereference   follow symbolic links                     *
   const char* cmdTemplate = 
               "file --brief --mime --dereference \"%S\" 1>\"%S\" 2>&1" ;

   gString gsTmp, gsCmd ;
   bool isText = false ;

   //* Create a temporary file *
   if ( ((this->win != NULL) && (this->win->acCreateTempfile ( gsTmp ))) ||
        ((this->tfm != NULL) && (this->tfm->createTempFile ( gsTmp ))) )
   {
      //* Create a command which invokes 'file' utility *
      gsCmd.compose( cmdTemplate, gsTrg.gstr(), gsTmp.gstr() ) ;

      //* Execute the command *
      system ( gsCmd.ustr() ) ;

      //* Scan the temp file. If target is a text file, *
      //* the first word in the string should be "text".*
      ifstream ifs( gsTmp.ustr(), ifstream::in ) ;
      if ( ifs.is_open() )
      {
         this->GetLine ( ifs, gsCmd ) ;
         if ( (gsCmd.find( "text" )) == ZERO )
            isText = true ;
         ifs.close() ;      // close the temp file
      }
      if ( this->win != NULL )
         this->win->acDeleteTempfile ( gsTmp ) ;
      else
         this->tfm->deleteTempFile ( gsTmp ) ;
   }
   return isText ;

}  //* End isTextfile() *

//*************************
//*        GetLine        *
//*************************
//********************************************************************************
//* Generic ifs.getline() method.                                                *
//* 1) Read the raw data line to and including the newline character (if any).   *
//*    a) The library getline() function reads and counts the newline character  *
//*       but does not copy it to the target buffer.                             *
//*    b) Optionally, the newline character may be included in the captured      *
//*       text by resetting the 'stripnl' flag.                                  *
//* 2) If a Windoze carriage-return/linefeed pair ('\r\n') is received, the      *
//*    the carriage-return character is always discarded, and the newline        *
//*    is optionally retained as described above.                                *
//*                                                                              *
//* Input  :  ifs     : (by reference) input data stream                         *
//*           gsIn    : (by reference) receives the stream data                  *
//*           stripnl : (optional, default: true)                                *
//*                     if 'true',  discard newline characters ('\n')            *
//*                     if 'false', retain newline characters                    *
//*                                 This is used when reading in a file for edit *
//*                                 in order to report an exact copy of the data.*
//*                                                                              *
//* Returns: number of bytes read from the stream                                *
//*          (incl. CR and LF characters if received)                            *
//********************************************************************************

short AcEdit::GetLine ( std::ifstream& ifs, gString& gsIn, bool stripnl )
{
   char  inBuff[MAX_PATH] ;         // input buffer
   short indx,                      // byte index
         inBytes ;                  // return value

   ifs.getline( inBuff, MAX_PATH, NEWLINE ) ;
   if ( (inBytes = ifs.gcount()) > ZERO )
   {
      //* Protect against Windoze CR/LF combo *
      if ( ((indx = (inBytes - 2)) >= ZERO) && 
           (inBuff[indx] == '\r') )
      { inBuff[indx] = NULLCHAR ; }
      ++indx ;

      if ( !stripnl && !(ifs.eof()) )  // reinstate the discarded newline character
      { inBuff[indx++] = '\n' ; inBuff[indx] = NULLCHAR ; }

      gsIn = inBuff ;                  // copy to caller's buffer
   }
   return ( inBytes ) ;

}  //* End GetLine() *

//*************************
//*     DeleteFile        *
//*************************
//********************************************************************************
//* Delete (unlink) the specified source file.                                   *
//*                                                                              *
//* Input  : srcPath : full path/filename specification of source                *
//*                                                                              *
//* Returns: ZERO if successful, else returns errno value                        *
//********************************************************************************
//* Programmer's Note: The C-language version of 'unlink' INSISTS on doing its   *
//* own escaping of special characters. If it receives escaped characters, it    *
//* escapes the '\' characters, negating the previous escape sequence.           *
//********************************************************************************

short AcEdit::DeleteFile ( const char* srcPath )
{
   short    status ;

   if ( (status = unlink ( srcPath )) != ZERO )
   {
      status = errno ;
   }
   return status ;

}  //* End DeleteFile() *

//************************
//*    DeleteEmptyDir    *
//************************
//********************************************************************************
//* Delete the specified directory. (directory must be empty)                    *
//*                                                                              *
//*                                                                              *
//* Input  : dirPath : full path specification of source                         *
//*                                                                              *
//* Returns: ZERO if successful, else returns errno value                        *
//********************************************************************************
//* Programmer's Note: The C-language version of 'rmdir' INSISTS on doing its    *
//* own escaping of special characters. If it receives escaped characters, it    *
//* escapes the '\' characters, negating the previous escape sequence.           *
//********************************************************************************

short AcEdit::DeleteEmptyDir ( const char* dirPath )
{
   short    status ;

   if ( (status = rmdir ( dirPath )) != ZERO )
   {
      status = errno ;
   }
   return status ;

}  //* End DeleteEmptyDir() *

//*************************
//*      CopyFile         *
//*************************
//********************************************************************************
//* Create a copy of the specified source file.                                  *
//* Preserves all permission bits and date/timestamps.                           *
//* Note that symbolic links _will be_ followed, and a symlink source will       *
//* cause the target of the source link will be copied, not the link itself.     *
//*                                                                              *
//* WARNING: Do not call this method to copy a 'special' file.                   *
//*                                                                              *
//* Input  : srcPath    : full path/filename specification of source             *
//*          trgPath    : full path/filename specification of target             *
//*          no_clobber : (optional, default: false)                             *
//*                       if set, a newer existing target will not be overwritten*
//*                       if reset, an existing target will always be overwritten*
//*                                                                              *
//* Returns: ZERO if successful, else returns errno value                        *
//********************************************************************************
//* Programmer's Note: This is a simplified version of the method found in       *
//* the FileMangler FMgr class.                                                  *
//*                                                                              *
//* Valid filename characters:                                                   *
//* --------------------------                                                   *
//* 1) The list of valid filename characters is OS or filesystem specific.       *
//*    Linux/UNIX allow anything except forward slash (/) and NULLCHAR (\0)      *
//*    Ambiguous characters may be used as filename characters _IF_ the are      *
//*    escaped. This application ensures that calls to external programs and     *
//*    and anything else sent to the shell are escaped by enclosing the          *
//*    filespecs in double quotes, but the double-quote delimiters will not      *
//*    escape the following: ( '$' '\' '`' '"' ).                                *
//*    See the EscapeSpecialChars() method for more information.                 *
//*                                                                              *
//* 2) Some filesystems e.g. vfat will not accept all special characters, in     *
//*    which case the copy will fail.                                            *
//*                                                                              *
//* 3) The tilde shortcut representation of the user's home directory IS NOT     *
//*    recognized by the call to the 'cp' shelll program. Caller should          *
//*    substitute the full home directory:                                       *
//*           "~/Documents/abc.txt" --> "/home/sam/Documents/abc.txt"            *
//*                                                                              *
//* Overwrite of Existing Target:                                                *
//* -----------------------------                                                *
//* The 'cp' command includes options to specify whether an existing target      *
//* should be overridden.                                                        *
//* 1) --no_clobber                                                              *
//*      This is the best known option, but it is now deprecated.                *
//* 2) --update[=all | none | none-fail | older]                                 *
//*      This is now the preferred option for controlling overwrite.             *
//* 3) --preserve=all                                                            *
//*      Testing indicates that using this option, an existing target is         *
//*      overriden regardless of timestamp. This is important because it means   *
//*      that the --update option is not needed UNLESS caller wishes to avoid    *
//*      overwrite of newer files.                                               *
//*      -- Note that the ORDER of parameters in the command may be important.   *
//*      -- In a bit of nostalgia, we have named the 'preserve-newer' flag as:   *
//*         'no_clobber'. When set, newer existing targets are protected.        *
//*                                                                              *
//* Verifying the Operation:                                                     *
//* ------------------------                                                     *
//* Note that the return value from system() is not reliable; however, it is     *
//* important to verify that the copy operation is successful.                   *
//* Possible solutions:                                                          *
//*  - Compare old/new stats? Slow but effective.                                *
//*  - Redirect stderr to a RAM array? see /dev/shm (shared memory)              *
//*  - https://man7.org/linux/man-pages/man7/shm_overview.7.html                 *
//*  - See man 7 shm_overview.                                                   *
//********************************************************************************

short AcEdit::CopyFile ( const char* srcPath, const char* trgPath, bool no_clobber )
{
   const char* cmdTemplate = 
      "cp --preserve=all \"%s\" \"%s\" 1>/dev/null 2>&1" ;
   const char* protectNewer = "--update=older " ;

   gString gsSrc( srcPath ), gsTrg( trgPath ) ; // adjusted filespecs
   gString gsCmd ;                              // format system command
   short    status ;                            // return value

   //* Escape any special characters not handled by double-quote delimiters.*
   this->EscapeSpecialChars ( gsSrc, escLINUX ) ;
   this->EscapeSpecialChars ( gsTrg, escLINUX ) ;

   gsCmd.compose( cmdTemplate, gsSrc.ustr(), gsTrg.ustr() ) ;

   if ( no_clobber )                      // see note above
   {
      int indx = gsCmd.find( L'"' ) ;
      gsCmd.insert( protectNewer, indx ) ;
   }

   //* Note that the return value from system() *
   //* is not reliable. (see notes above)       *
   if ( (status = system ( gsCmd.ustr() )) != ZERO )
      status = errno ;

   return status ;

}  //* End CopyFile() *

//*************************
//*  EscapeSpecialChars   *
//*************************
//********************************************************************************
//* Scan the source string (typically a filespec). If the string contains any    *
//* characters of the specified character group (enum escGroup) 'escape' them    *
//* with a backslash character.                                                  *
//*                                                                              *
//* Do not use this method for URL (Universal Resource Locator) specs.           *
//* URL specifications require a specific protocol which is not handled here.    *
//*                                                                              *
//* Input  : pathSpec : (by reference) source text data                          *
//*                     on return the data has been 'escaped' accoring to the    *
//*                     specified criteria                                       *
//*          group    : (member of enum escGroup) specifies the group of         *
//*                     characters to be 'escaped'                               *
//*          wch      : (optional, NULLCHAR by default)                          *
//*                     if 'group' parameter == escWCHAR, then 'wch' specifies   *
//*                     a single character (NOT nullchar) to be 'escaped'        *
//*                                                                              *
//* Returns: number of characters 'escaped'                                      *
//********************************************************************************
//* Special Case Testing for '\' as the special character:                       *
//* 1) If the incomming data contains a '\', we must test whether:               *
//*    a) The '\' itself has already been escaped,                               *
//*    b) The '\' is escaping some other special character,                      *
//*    c) The '\' is a special character which needs to be escaped.              *
//* 2) If data contains an escaped special character, but caller intended        *
//*    for it to be two SEPERATE special characters, caller will be              *
//*    disappointed that we could not read his/her/its mind. (sorry about that)  *
//*                                                                              *
//* -   -   -   -   -   -   -   -   -   -   -   -   -   -   -   -   -   -   -  - *
//* For more information, see "Quoting Special Characters in BASH.odt".          *
//********************************************************************************

short AcEdit::EscapeSpecialChars ( gString& pathSpec, escGroup group, wchar_t wch )
{
   //* List of critical "special" characters.*
   const short   minSpecialCount = 4 ;
   const wchar_t minSpecialChars[minSpecialCount] = 
   { L'\\', L'"', L'`', L'$' } ;

   //* List of Linux/UNIX shell "special" characters.*
   const short   linuxSpecialCount = 13 ;
   const wchar_t linuxSpecialChars[linuxSpecialCount] = 
   { L'\\', L'"', L'\'', L'?', L':', L';', L'&', L'>', L'<', L'|', L'*', L'`', L'$' } ;

   //* List of all known "special" characters which might be interpreted *
   //* by the shell or shell programs as commands rather than plain text.*
   const short   maxSpecialCount = 27 ;
   const wchar_t maxSpecialChars[maxSpecialCount] = 
   { L'\\', L'"', L'\'', L'?', L':', L';', L'&', L'>', L'<', L'|', L'*', L'`', L'$',
     L'#', L'+', L'=', L'%', L'!', L'~', L'{', L'}', L'@', L'(', L')', L'[', L']', L' ' } ;

   const wchar_t wDQ = L'"',
                 wBS = L'\\' ;

   const wchar_t *wPtr ;            // pointer to wchar_t array
   short wCount,                    // number of elements in wchar_t array
         indx,                      // text index
         specChars = ZERO ;         // return value

   //* Point to the reference array and establish number of array elements.*
   if ( group == escMIN )
   { wPtr = minSpecialChars ;   wCount = minSpecialCount ; }
   else if ( group == escLINUX )
   { wPtr = linuxSpecialChars ; wCount = linuxSpecialCount ; }
   else if ( group == escMAX )
   { wPtr = maxSpecialChars ;   wCount = maxSpecialCount ; }
   else if ( group == escDQ )
   { wPtr = &wDQ ;              wCount = 1 ; }
   else if ( group == escWCHAR )
   {  //* Do not allow escaping of null terminator.*
      wPtr = &wch ;
      wCount = (wch != NULLCHAR) ? 1 : 0 ;
   }

   for ( short specIndx = ZERO ; specIndx < wCount ; ++specIndx )
   {
      indx = ZERO ;
      do
      {
         if ( (indx = pathSpec.find( wPtr[specIndx], indx )) >= ZERO )
         {
            //* Special character == '\'. (see note above) *
            if ( (pathSpec.gstr()[indx]) == wBS )
            {
               bool insert = true ;
               //* If escape character is itself escaped *
               if ( (indx > ZERO) && ((pathSpec.gstr()[indx - 1]) == wBS) )
               { ++indx ; insert = false ; }
               //* If '\' is being used as an escape character *
               if ( insert )
               {
                  for ( short i = ZERO ; i < maxSpecialCount && insert ; ++i )
                  {
                     if ( (pathSpec.gstr()[indx + 1]) == maxSpecialChars[i] )
                     { indx += 2 ; insert = false ; }
                  }
               }

               if ( insert )  // escape the escape character
               {
                  pathSpec.insert( wBS, indx ) ;
                  ++specChars ;
                  indx += 2 ;
               }
               else
                  ++indx ;
            }
            else
            {
               if ( indx == ZERO )       // first character in array
               {
                  pathSpec.insert( wBS, indx ) ;
                  ++specChars ;
                  indx += 2 ;
               }
               else // (indx > ZERO), second or later character in array
               {
                  if ( (pathSpec.gstr()[indx - 1]) != wBS )
                  {
                     pathSpec.insert( wBS, indx ) ;
                     ++specChars ;
                     indx += 2 ;
                  }
                  else
                     ++indx ;
               }
            }
         }
      }
      while ( (indx >= ZERO) && (pathSpec.gstr()[indx] != NULLCHAR) ) ;
   }

   return specChars ;

}  //* End EscapeSpecialChars() *

//*************************
//*      RenameFile       *
//*************************
//********************************************************************************
//* Rename the specified source file to specified destination file.              *
//*                                                                              *
//* For the rename() primitive: If srcPath refers to a symbolic link the         *
//* link is renamed; if trgPath refers to a symbolic link the link will be       *
//* overwritten.                                                                 *
//*                                                                              *
//* Input  : srcPath : full path/filename specification of source                *
//*          trgPath : full path/filename specification of target                *
//*                                                                              *
//* Returns: zero(0) if successful, else returns errno value                     *
//********************************************************************************
//* Programmer's Note: The C-language version of 'rename' INSISTS on doing its   *
//* own escaping of special characters. If it receives escaped characters, it    *
//* escapes the '\' characters, negating the previous escape sequence.           *
//********************************************************************************

short AcEdit::RenameFile ( const char* srcPath, const char* trgPath )
{
   short    status ;

   if ( (status = rename ( srcPath, trgPath )) != ZERO )
   {
      status = errno ;
   }
   return status ;

}  //* End RenameFile() *

//*************************
//*       getCWD          *
//*************************
//********************************************************************************
//* Returns the path of user's current working directory.                        *
//*                                                                              *
//* Input  : cdir : (by reference) receives filespec of CWD                      *
//*                                                                              *
//* Returns: 'true'  if successful                                               *
//*          Note: If system call fails the "." (this) spec will be returned.    *
//********************************************************************************

bool AcEdit::getCWD ( gString& cdir )
{
   const char* THIS_DIR = "." ;
   char  buff[MAX_PATH] ;
   bool  status = true ;

   if ( (getcwd ( buff, MAX_PATH )) != NULL )
   {
      cdir = buff ;
      status = true ;
   }
   else
      cdir = THIS_DIR ;
//      cdir.clear() ;    // caller's buffer set to empty string

   return status ;

}  //* End getCWD() *

//*************************
//*        setCWD         *
//*************************
//********************************************************************************
//* Set the current working directory (CWD).                                     *
//* Note that the CWD will be active only until the application exits, at which  *
//* time the CWD will revert to the directory from which the application was     *
//* invoked.                                                                     *
//*                                                                              *
//* Input  : newCwd : (by reference) filespec of target directory.               *
//*          oldCwd : (optional, NULL pointer by default)                        *
//*                   if specified, receives the old (previous) CWD              *
//*                                                                              *
//* Returns: 'true'  if new CWD set                                              *
//*          'false' if invalid filespec or system access error                  *
//********************************************************************************

bool AcEdit::setCWD ( const gString& newCwd, gString* oldCwd )
{
   bool status = false ;

   //* If specified get the current CWD *
   if ( oldCwd != NULL )
      this->getCWD ( *oldCwd ) ;

   //* Set the new CWD *
   if ( (chdir ( newCwd.ustr() )) == ZERO )
      status = true ;

   return status ;

}  //* End setCWD() *

//*************************
//*        readCwd        *
//*************************
//********************************************************************************
//* Scan and save all filenames and sub-directory names in the specified         *
//* directory.                                                                   *
//*                                                                              *
//* Each record is formatted according to 'specTemplate' formatting string.      *
//* This template formats each record with three(3) leading character which      *
//* identify the record as directory or non-directory records.                   *
//* This is done to avoid having to identify the records according to color      *
//* attribute. This design decision may be revisited at a later date.            *
//*                                                                              *
//* - Subdirectory names are stored in gsDNames, and dnCnt receives subdir count.*
//* - Filenames are stored in gsFNames, and fnCnt receives the filename count.   *
//* - Both lists are sorted alphabetically, case insensitive and low-to-high.    *
//* - The entries for parent directory (..) and current directory (.) are        *
//*   read and discarded.                                                        *
//* - Hidden files are reported ( ".Wxyz,txt" ).                                 *
//*   Exception: Files beginning with ".#" are assumed to be temporary files     *
//*              assocociated with the application itself, and are therefore     *
//*              excluded from the list by default; but see the 'incltmp'        *
//*              parameter.                                                      *
//*                                                                              *
//* --- --- --- ---                                                              *
//* Technical Note: The 'readdir64' function is used instead of the standard     *
//* 'readdir' function. This ensures support for larger filesystems.             *
//*                                                                              *
//*                                                                              *
//* Input  : cwdPath  : filespec of target directory                             *
//*          gsDNames : (by reference) receives the list of directory names      *
//*                     seperated by newline characters ('\n')                   *
//*          dnCnt    : (by reference) receives number of directory records      *
//*                     captured                                                 *
//*          gsFNames : (by reference) receives the list of non-directory names  *
//*                     seperated by newline characters ('\n')                   *
//*          fnCnt    : (by reference) receives number of non-directory records  *
//*                     captured                                                 *
//*          trgWid   : width of target field in columns (max record width)      *
//*          incltmp  : (optional, default: false)                               *
//*                     'false' discard filenames of temp files created by       *
//*                             the application                                  *
//*                     'true'  retain filenames of application temp files       *
//*                                                                              *
//* Returns: 'true'  if directory scanned successfully                           *
//*          'false' if target does not exist or user does not have read access  *
//********************************************************************************

bool AcEdit::readCwd ( const char* cwdPath, gString& gsDNames, short& dnCnt, 
                       gString& gsFNames, short& fnCnt, int trgWid, bool incltmp )
{
   const char* specTemplate = " %C %s\n" ; // format each filespec record

   DIR* dirPtr = NULL ;                   // pointer to directory object
   deStats* destat ;                      // directory-entry record
   bool     trgFile,                      // set if potential target file
            status = false ;              // return value

   gsDNames.clear() ;                     // initialize buffers
   gsFNames.clear() ;
   dnCnt = fnCnt = ZERO ;                 // initialize counters

   if ( (dirPtr = opendir ( cwdPath )) != NULL )
   {
      //* Read each item in the directory *
      while ( (destat = readdir64 ( dirPtr )) != NULL )
      {
         //* Do not include 'current dir' and 'parent dir' names. *
         trgFile = true ;
         if ( destat->d_name[0] == PCHAR )
         {
            if ( (destat->d_name[1] == '\0') ||    // ('this' directory)
                 ((destat->d_name[1] == PCHAR) &&  // ('parent' directory)
                  (destat->d_name[2] == '\0')) ||
                 // a hidden file which (probably) belongs to the application
                 // (see note above)
                 ((destat->d_name[1] == '#') && !incltmp)
               )
            { trgFile = false ; }
         }

         if ( trgFile != false )
         {
            if ( destat->d_type == DT_DIR )
            { ++dnCnt ; gsDNames.append( specTemplate, &DCHAR, destat->d_name ) ; }
            else
            { ++fnCnt ; gsFNames.append( specTemplate, &FCHAR, destat->d_name ) ; }
         }
      }
      closedir ( dirPtr ) ;               // Close the directory

      //* Sort the filenames and directory name records *
      rcfSort ( gsFNames, fnCnt, trgWid ) ;
      rcfSort ( gsDNames, dnCnt, trgWid ) ;

      status = true ;                     // return success
   }

   return status ;

}  //* End readCwd() *

//*************************
//*        readCwd        *
//*************************
//********************************************************************************
//* Scan and save all filenames and sub-directory names in the specified         *
//* directory. Create an array of color attributes, one for each filename        *
//* scanned.                                                                     *
//*                                                                              *
//* Directory names are rendered in an alternate color to distinguish between    *
//* directory and non-directory names.                                           *
//*                                                                              *
//* Input  : gsList : (by reference) receives the concatenated list of items,    *
//*                   Each item include the three-character prefix and is        *
//*                   padded to the specified field width.                       *
//*          acArray: (by reference) pointer to the dynamically allocated        *
//*                   color attribute array.                                     *
//*                   IMPORTANT NOTE: Caller is responsible for releasing the    *
//*                                   dynamic allocation.                        *
//*          fldWid : width of target control in columns                         *
//*                   (Target field is assumed to be type ftSB.)                 *
//*                   Each record will be padded on the right to specified width.*
//*          dAttr  : (optional, default: acaUSEDFLT)                            *
//*                   color attribute for directory names (and parent item)      *
//*          fAttr  : (optional, default: acaUSEDFLT)                            *
//*                   color attribute for non-directory names                    *
//*          incltmp  : (optional, default: false)                               *
//*                     'false' discard filenames of temp files created by       *
//*                             the application                                  *
//*                     'true'  retain filenames of application temp files       *
//*                                                                              *
//* Returns: total number of entries:                                            *
//*          subdirectory names + filenames + parent directory record            *
//*          -- Note that for safety, the size of color array is padded to       *
//*             two(2) larger than this value.                                   *
//********************************************************************************
//* Notes:                                                                       *
//* ------                                                                       *
//* 1) Directory names should be rendered in an alternate color to distinguish   *
//*    between directory and non-directory names.                                *
//* 2) Currently, caller's color attributes are ignored; instead the attributes  *
//*    are hard-coded.                                                           *
//*    a) Directory names are hard-coded as Cyan on 'auxAttr' background.        *
//*    b) Non-directory names are set to the placeholder value, acaUSEDFLT,      *
//*       which in this context is interpreted as the attributes for the         *
//*       target control (which is currently ALSO 'auxAttr').                    *
//*    c) Caller's attributes will be implemented if we get requests from        *
//*       the user community.                                                    *
//********************************************************************************

short AcEdit::readCwd ( gString& gsList, acAttr*& acArray, short fldWid, 
                        acAttr dAttr, acAttr fAttr, bool incltmp  )
{
   static const char* parentMsg[uilENV] = 
   {
      "parent directory",        // English
      "父目录",                     // Zhongwen
   } ;

   gString gsCwd ;                        // filespec of CWD
   gString gsFNames, gsDNames ;           // accumulator buffers
   short dCnt    = ZERO,                  // number of subdirectory names scanned
         fCnt    = ZERO,                  // number of non-directory names scanned
         itemCnt = ZERO,                  // total number of records scanned
         ci      = ZERO ;                 // color array index
   //* Color attributes for directory names, and non-directory names *
   acAttr dirAttr = acAttr((acaFGb_CYAN) | (this->auxAttr & acaBG_MASK)) ;
   acAttr filAttr = acaUSEDFLT ;

   acArray = NULL ;

   //* Scan the current working directory *
   this->getCWD ( gsCwd ) ;
   this->readCwd( gsCwd.ustr(), gsDNames, dCnt, gsFNames, fCnt, fldWid, incltmp ) ;

   //* Concatenate the file sub-lists. *
   gsList.compose( " .. %s\n%S%S", parentMsg[this->lang], 
                   gsDNames.gstr(), gsFNames.gstr() ) ;

   //* Number of items in filename array: subdirectories, *
   //* non-directory files and parent-directory indicator.*
   itemCnt = dCnt + fCnt + 1 ;            // total items/attributes
   acArray = new acAttr[itemCnt + 2] ;    // allocate attribute array
   while ( ci <= dCnt )                   // initialize directory attributes
      acArray[ci++] = dirAttr ;
   while ( ci < (itemCnt + 2) )           // initialize filename attributes
      acArray[ci++] = filAttr ;

   return itemCnt ;

}  //* End readCwd() *

//*************************
//*      isFilename       *
//*************************
//********************************************************************************
//* Validate the characters of the filename or directory name.                   *
//* If inclSlash, then also allow forward slash as a pathname (but not as a      *
//* filename) character.                                                         *
//*                                                                              *
//* This method allows all alphanumeric characters for all alphabets and all     *
//* number systems, SPACE, PERIOD, and non-alnum characters that have proven     *
//* to be valid filename characters under Fedora Linux.                          *
//* (For Windoze system pathnames, we would also need to accept the backslash)   *
//* (character, '\\' but currently we don't.                                 )   *
//*                                                                              *
//* Input  : gsName   : (by reference) contains filename (or path name           *
//*                     to be tested                                             *
//*          inclSlash: (optional, default: false)                               *
//*                     if 'false' test for filename characters only             *
//*                     if 'true'  include the '/' slash character               *
//*                                This allows test for full filespec.           *
//*          vfatChars: (optional, default: false)                               *
//*                     if 'false' allow all Linux-system compatible characters  *
//*                     if 'true'  allow only "globally compatible" characters   *
//*                                (see notes below)                             *
//*          failIndex: (optional, default: null pointer)                        *
//*                     if specified, target receives the index of the first     *
//*                     character which failed the test. (If no fail, then -1)   *
//* Returns: true if filename string meets criteria, else false                  *
//********************************************************************************
//* Linux / UNIX: Valid filename characters                                      *
//* ---------------------------------------                                      *
//* Under Linux all characters except '\0' and '/' are valid filename            *
//* characters. However, some operating systems and filesystems will not         *
//* accept all valid Linux filename characters. While we are not going to        *
//* force the ultra-limited POSIX (global compatibility) standard on anyone,     *
//* the user must be aware of interoperability issues.                           *
//*                                                                              *
//* Filenames that must travel across the web should be aware of the following.  *
//*  #      <     $     +   %      >     !     `                                 *
//*  &      *     ‘     |   {      ?     "     =                                 *
//*  }      /     :     \   @      whitespace                                    *
//*                                                                              *
//* The filenames '.' and '..' are reserved, but we rely on the user to know     *
//* that.                                                                        *
//*                                                                              *
//* Linux/UNIX Reserved Characters                                               *
//* ------------------------------                                               *
//* Avoid using the following characters in file names:                          *
//*  '   "   ?   :   ;   \   &   >   <   |   *                                   *
//*                                                                              *
//* All of these characters are allowed when the 'linuxSpec' flag is set, but    *
//* but use these characters with caution when naming files.                     *
//*  a) Some of these characters are reserved characters under VFAT/NTFS         *
//*     filesystems (see below).                                                 *
//*  b) Only a moron would use a control character in a filename, so even though *
//*     they are technically valid Linux filename characters, 0x00-0x1F and      *
//*     delete (0x7F) are always rejected by this method.                        *
//*     NOTE: If you insist on being a moron, you can comment out the two        *
//*           lines near the top of the loop for testing those characters.       *
//*                                                                              *
//* The shell program will require that certain 'special' characters have to     *
//* be 'escaped' in order to perform operations on filenames that contain        *
//* those characters.                                                            *
//*                                                                              *
//* These characters are interpreted as instructions to the 'bash' shell or      *
//* other shell programs UNLESS they are "escaped" i.e. preceeded by the '\'     *
//* escape character when sent to the shell.                                     *
//*                                                                              *
//* VFAT (Virtual FAT) and NTFS Reserved Characters                              *
//* -----------------------------------------------                              *
//* Note that while Linux filesystems allow all UTF-8 characters in filenames,   *
//* under VFAT and NTFS filesystems (often used for data backup), certain        *
//* characters are reserved:  0x00-0x1F 0x7F " * / : < > ? \ |                   *
//*  a) Control characters (0x00-0x1F) and Delete (0x7F) are invalid in          *
//*     vfat/ntfs filenames, and are always rejected.                            *
//*  b) The Linux cp (copy) and mv (rename) commands will refuse to operate      *
//*     on filenames with the reserved characters when the target filesystem     *
//*     is VFAT or NTFS. Therefore, the 'vfatChars' flag should be set if        *
//*     the resulting filename may be stored on those filesystems.               *
//********************************************************************************

bool AcEdit::isFilename ( const gString& gsName, 
                          bool inclSlash, bool vfatChars, int* failIndx )
{
   //* For scanning text data. List of Linux/UNIX shell "special" characters.*
   const short   linuxSpecialCount = 11 ;
   const wchar_t linuxSpecialChars[linuxSpecialCount] = 
   { L'\\', L'\'', L'"', L'?', L':', L';', L'&', L'>', L'<', L'|', L'*' } ;
   const short   restrictedFnCount = 12 ;
   //* For scanning text data list of restricted characters *
   //* and shell characters for VFAT/NTFS filesystems.      *
   const wchar_t restrictedFnChars[restrictedFnCount] = 
   { L'/', L'<', L'>', L'|', L':', L';', L'&', L'*', L'?', L'"', L'`', L'\\' } ;
   const wchar_t CC_MIN = 0x0000 ;
   const wchar_t CC_MAX = 0x001F ;
   const wchar_t CC_DEL = 0x007F ;

   const wchar_t* wbuff = gsName.gstr() ; // pointer to text string
   wchar_t wch ;                          // character under test
   int   ci = ZERO ;                      // character index
   short i  = ZERO ;                      // special char array index
   bool  goodName = true ;                // return value

   if ( failIndx != NULL )                // if index pointer provided
      *failIndx = -1 ;                    // assume success

   while ( wbuff[ci] != NULLCHAR )
   {
      wch = wbuff[ci++] ;

      // control characters
      if ( ((wch >= CC_MIN) && (wch <= CC_MAX)) || (wch == CC_DEL) )
      { goodName = false ; break ; }

      // alpha-numeric characters
      if ( ((iswalnum ( wch )) != ZERO) || (wch == DOT) || (wch == SPACE) )
      { continue ; }

      if ( (iswpunct ( wch )) )
      {
         if ( (inclSlash && wch == L'/') )   // pathnames may include forward slash
            continue ;

         //* Reject VFAT/NTFS reserved characters and *
         //* "special" shell-comand characters.       *
         if ( vfatChars )
         {
            for ( i = ZERO ; i < restrictedFnCount ; ++i )
            {
               if ( wch == restrictedFnChars[i] )
                  break ;
            }
            if ( i < restrictedFnCount )
            { goodName = false ; break ; }
         }

         //* Allow "special" shell-command characters. (see notes above) *
         else
         {
            for ( i = ZERO ; i < linuxSpecialCount ; ++i )
            {
               if ( wch == linuxSpecialChars[i] )
                  break ;
            }
            if ( i < linuxSpecialCount )  // if one of the linux special characters
               continue ;
            //* If not one of the special characters and also     *
            //* not a forward stroke or a backtick '`' character, *
            //* (which should be considered as having leprosy),   *
            //* then it is a valid Linux filename character.      *
            if ( (wch == L'`') || (wch == L'/') )
            { goodName = false ; break ; }
         }
      }
      else
      { goodName = false ; break ; }

   }

   if ( !goodName && (failIndx != NULL) )
      *failIndx = ci - 1 ;

   return goodName ;

}  //* End isFilename() *

//*************************
//*        rcfSort        *
//*************************
//********************************************************************************
//* Non-member Method:                                                           *
//* ------------------                                                           *
//* Sort the display records alphabetically.                                     *
//* -- The sort is alphabetical, case insensitive and low-to-high.               *
//* -- Max record width is enforced. Records are truncated if necessary          *
//*    so that all records fit within the target field.                          *
//*                                                                              *
//* a) The individual records are of the form: " - fname\n"                      *
//* b) The three(3) prefix characters are irrelevant in the comparisons and are  *
//*    ignored. The filenames drive the comparisons.                             *
//* c) It is assumed that each record is terminated by a newline character.      *
//* d) In the (unlikely) case of a record being wider than field in which the    *
//*    records will be displayed, the filename is truncated to fit.              *
//* e) It is assumed that the records are well formed and that caller did not    *
//*    lie to us.                                                                *
//*                                                                              *
//* Input  : gsSrc : (by reference) contains the source records, separated       *
//*                  by newline characters ( '\n' )                              *
//*          recs  : number of source records                                    *
//*          fCols : width of target field in columns                            *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* Programmer's Note: The sort algorithm is taken directly from the author's    *
//* "bubblesort" student exercise, available on the author's website.            *
//********************************************************************************

static void rcfSort ( gString& gsSrc, int recs, int fCols )
{
   if ( recs < 2 )                        // if zero or one record, then nothing to sort
      return ;                            // note the early return

   gString gs ;                           // text formatting
   wchar_t*  wbuff = new wchar_t[gsSrc.gschars() + recs * 2] ; // work buffer
   wchar_t** recPtr = new wchar_t*[recs + 1] ; // array of pointers to records
   const wchar_t* srcPtr = gsSrc.gstr() ;
   int //srcChars = gsSrc.gschars(),        // number of source characters
       si = ZERO,                         // source buffer index
       ti = ZERO,                         // target buffer index
       ri = ZERO ;                        // record index

   //* Extract the individual records *
   while ( ri < recs )
   {
      recPtr[ri++] = &wbuff[ti] ;         // pointer to record

      //* Copy the record including the newline delimiter *
      while ( srcPtr[si] != NEWLINE )
         wbuff[ti++] = srcPtr[si++] ;
      wbuff[ti++] = srcPtr[si++] ;        // copy the newline
      wbuff[ti++] = NULLCHAR ;            // terminate the record
   }

   //* Sort the record pointers *
   gString gsI, gsL, gsG ;       // temp buffers
   wchar_t *saveLow, *saveHigh ; // temp pointers
   int lti = ZERO,               // less-than index
       gti = recs - 1,           // greater-than index
       ini ;                     // inner-loop index

   for ( lti = 0, gti = recs-1 ; (gti-lti) > ZERO ; ++lti, --gti )
   {
      for ( ini = lti+1 ; ini <= gti ; ++ini )
      {
         gsI = recPtr[ini] ;              // load records for comparison
         gsL = recPtr[lti] ;

         //* Compare the records.                  *
         //* If current record is less than        *
         //* lower-limit record, swap the records. *
         if ( (gsI.compare( gsL, false )) < ZERO )
         {
            saveLow     = recPtr[lti] ;
            recPtr[lti] = recPtr[ini] ;
            recPtr[ini] = saveLow ;
            gsL = gsI ;                   // refresh the comparison data
         }

         //* If current index less that index of limit *
         //*   (avoid comparing a record to itself)    *
         if ( ini < gti )
         {
            gsG = recPtr[gti] ;

            //* Compare the records.                     *
            //* If record at upper limit is less than    *
            //* target record, swap the records.         *
            if ( (gsG.compare( gsI, false )) < ZERO )
            {
               saveHigh    = recPtr[gti] ;
               recPtr[gti] = recPtr[ini] ;
               recPtr[ini] = saveHigh ;
               gsI = gsG ;                // refresh the comparison data
            }

            //* Compare the records.               *
            //* If current record less than target *
            //* record, swap the records.          *
            if ( (gsI.compare( gsL, false )) < ZERO )
            {
               saveLow     = recPtr[lti] ;
               recPtr[lti] = recPtr[ini] ;
               recPtr[ini] = saveLow ;
            }
         }
      }
   }

   //* Concatenate the records in the new order. *
   //* Truncate records wider than the target    *
   //* field (less automatic Scrollbox padding). *
   gsSrc.clear() ;
   for ( ri = ZERO ; ri < recs ; ++ri )
   {
      gs = recPtr[ri] ;                   // load a record
      if ( (gs.gscols()) > (fCols - 2) )  // if record is wider than field
      {
         gs.limitCols( fCols - 2 ) ;      // truncate the filename
         gs.append( NEWLINE ) ;           // append the newline delimiter
      }
      gsSrc.append( gs.gstr() ) ;         // add record to the list
   }

   delete [] wbuff ;                      // release the dynamic allocations
   delete [] recPtr ;

}  //* End rcfSort() *

//*************************
//*    formatTimestamp    *
//*************************
//********************************************************************************
//* Produce a formatted output string based on data in a localTime object.       *
//*   Format:  "dd Mmm yyyy hh:mm:ss"                                            *
//*                                                                              *
//* Input  : ft     : initialized localTime structure (by reference)             *
//*                   Note: 'day', the day-of-the-week field is ignored.         *
//*          tStamp : (by reference) receives formatted data                     *
//*          canon  : (optional, 'false' by default)                             *
//*                   if 'false', user-friendly format including month name      *
//*                   if 'true',  canonical timestamp string                     *
//*          secs   : (optional, 'true' by default)                              *
//*                   'true'  include the seconds in the output                  *
//*                   'false' do not include seconds in the output               *
//*          dow    : (optional, 'false' by default)                             *
//*                   (referenced ONLY if 'canonical' is set)                    *
//*                   'true'  insert the day-of-the-week into the output         *
//*                   'false' do not insert the day-of-the-week into output      *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void AcEdit::formatTimestamp ( const localTime& lt, gString& tStamp, 
                               bool canon, bool secs, bool dow ) const
{
   //* Days-of-the Week               *
   //* Note that lt.day is zero-based. *
   static const wchar_t* dowStr[] =
   {
      L"Sun, ",
      L"Mon, ",
      L"Tue, ",
      L"Wed, ",
      L"Thu, ",
      L"Fri, ",
      L"Sat, "
   } ;
   //* Month-name strings for file info formatting *
   //* (Note that lt.month is one-based.)          *
   static const wchar_t wMonthString[13][4] =
   {
      { L"   " },
      { L"Jan" }, { L"Feb" }, { L"Mar" }, { L"Apr" }, { L"May" }, { L"Jun" },
      { L"Jul" }, { L"Aug" }, { L"Sep" }, { L"Oct" }, { L"Nov" }, { L"Dec" }
   } ;

   if ( canon )               // canonical format
   {
      tStamp.compose( "%04hd-%02hd-%02hdT%02hd:%02hd:%02hd",
                      &lt.year, &lt.month, &lt.date,
                      &lt.hours, &lt.minutes, &lt.seconds ) ;
   }
   else                       // non-technical format
   {
      tStamp.compose( L"%02hu %S %04hu %02hu:%02hu:%02hu",
                      &lt.date, wMonthString[lt.month], &lt.year, 
                      &lt.hours, &lt.minutes, &lt.seconds ) ;
   }
   if ( ! secs )
      tStamp.limitChars( tStamp.gschars() - 4 ) ;
   if ( dow )
      tStamp.insert( dowStr[lt.day] ) ;

}  //* End formatTimestamp() *

//*************************
//*    epoch2Timestamp    *
//*************************
//********************************************************************************
//* Convert epoch time (64-bit integer) to a canonical timestamp string.         *
//*                                                                              *
//*                                                                              *
//* Input  : epoch   : epoch time integer (this is implicitly a "time_t" value)  *
//*          gsTstamp: receives the formatted timestamp                          *
//*          canon   : (optional, 'true' by default)                             *
//*                    if 'false', user-friendly format including month name     *
//*                    if 'true',  canonical timestamp string                    *
//*          ltptr   : (optional, default: null pointer)                         *
//*                    if specified, receives the 'broken-down' time             *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* Programmer's Note: The localtime_r() function decodes the epoch value based  *
//* on the system timezone. If the epoch value represents less than one full     *
//* day, then the broken-down time may represent a time _before_ the actual      *
//* epoch. For instance, for a system on New York time, an epoch value of:       *
//*   0x0000 yields: 1969-12-31T19:00:00                                         *
//* while the true GMT/UTC broken-down time is decoded as:                       *
//*   0x4650 yields: 1970-01-01T00:00:00                                         *
//* This is a difference of 5 hours or 18,000 seconds                            *
//*                                                                              *
//* To avoid creation of an invalid broken-down time report, an epoch value      *
//* less that one day (86,400 seconds or 0x15180 hex) is replaced with the       *
//* current system time.                                                         *
//********************************************************************************

void AcEdit::epoch2Timestamp ( int64_t epoch, gString& gsTstamp, bool canon, 
                               localTime* ltptr ) const
{
   const int64_t SECS_PER_HOUR = 0x00E10 ;
   const int64_t SECS_PER_DAY  = SECS_PER_HOUR * 24 ;
   localTime lt ;                         // receives broken-down time
   localTime *ltp = &lt ;                 // pointer to localTime object
   if ( ltptr != NULL )                   // if caller specified a localTime object
      ltp = ltptr ;
   Tm tm ;                                // Linux time structure

   //* Range check the epoch value. (see note above) *
   if ( epoch < SECS_PER_DAY )
      epoch = time ( NULL ) ;

   //* Decode the system time *
   if ( (localtime_r ( &epoch, &tm )) != NULL )
   {
      //* Translate to localTime format *
      ltp->julian  = tm.tm_yday ;         // Julian date
      ltp->day     = tm.tm_wday ;         // 0 == Sunday ... 6 == Saturday
      ltp->date    = tm.tm_mday ;         // today's date
      ltp->month   = tm.tm_mon + 1 ;      // month
      ltp->year    = tm.tm_year + 1900 ;  // year
      ltp->hours   = tm.tm_hour ;         // hour
      ltp->minutes = tm.tm_min ;          // minutes
      ltp->seconds = tm.tm_sec ;          // seconds
   }
   this->formatTimestamp ( *ltp, gsTstamp, canon, true ) ;

}  //* End epoch2Timestamp() *

//************************
//*    GetLocalTime      *
//************************
//********************************************************************************
//* Get the system timecode and convert it to localTime format.                  *
//*                                                                              *
//* Input  : ft  : (by reference, initial values ignored)                        *
//*                on return, contains decoded date/time                         *
//*                                                                              *
//* Returns: true if successful, false if system call fails                      *
//********************************************************************************
//* Programmer's Note: Currently we do not decode the timezone and DST fields.   *
//*                                                                              *
//* -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - *
//* Note that we do a bit of defensive programming in anticipation of the        *
//* dreaded year 2038 overflow of the 32-bit time_t type.                        *
//********************************************************************************

bool AcEdit::GetLocalTime ( localTime& ft )
{
   bool success = false ;                       // return value
  
   ft.reset() ;                                 // set to default values
   if ( (time ( &ft.sysepoch )) != (-1) )
   {
      if ( ft.sysepoch >= ZERO )                // reported time is AFTER the epoch
      {
         ft.epoch = (int64_t)ft.sysepoch ;      // promote to 64-bit
         //* Decode the system time *
         Tm    tm ;                             // Linux time structure
         if ( (localtime_r ( &ft.sysepoch, &tm )) != NULL )
         {
            //* Translate to localTime format *
            ft.julian   = tm.tm_yday ;          // Julian date
            ft.day      = tm.tm_wday ;          // 0 == Sunday ... 6 == Saturday
            ft.date     = tm.tm_mday ;          // today's date
            ft.month    = tm.tm_mon + 1 ;       // month
            ft.year     = tm.tm_year + 1900 ;   // year
            ft.hours    = tm.tm_hour ;          // hour
            ft.minutes  = tm.tm_min ;           // minutes
            ft.seconds  = tm.tm_sec ;           // seconds
            success = true ;
         }
      }
      else
      {  /* SYSTEM ERROR - time_t OVERFLOW */ }
   }
   return success ;

}  //* End GetLocalTime() *

//*************************
//*    IdentifyUser       *
//*************************
//********************************************************************************
//* Private Method.                                                              *
//* Get application user's information.                                          *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: true if successful, else false                                      *
//********************************************************************************
//* Programmer's Note:                                                           *
//*                                                                              *
//* The passwd structure - from pwd.h                                            *
//* struct passwd                                                                *
//* {                                                                            *
//*   char *pw_name;                /* Username.       */                        *
//*   char *pw_passwd;              /* Password.       */                        *
//*   __uid_t pw_uid;               /* User ID.        */                        *
//*   __gid_t pw_gid;               /* Group ID.       */                        *
//*   char *pw_gecos;               /* Real name.      */                        *
//*   char *pw_dir;                 /* Home directory. */                        *
//*   char *pw_shell;               /* Shell program.  */                        *
//* };                                                                           *
//*                                                                              *
//* The group structure - from grp.h                                             *
//* struct group                                                                 *
//*   {                                                                          *
//*     char *gr_name;              /* Group name.  */                           *
//*     char *gr_passwd;            /* Password.    */                           *
//*     __gid_t gr_gid;             /* Group ID.    */                           *
//*     char **gr_mem;              /* Member list. */                           *
//*   };                                                                         *
//*                                                                              *
//* See also the getgroups ( int COUNT, gid_t *GROUPS ) function.                *
//*  Note that user's effective group ID may, or may not be returned in          *
//*  this list. If it is the ONLY item in the supplimentary list, we don't       *
//*  display it a second time.                                                   *
//*                                                                              *
//* Note also that the actual password strings will probably not be returned     *
//*  by the system calls, but that is system dependent.                          *
//*                                                                              *
//* In an earlier version of this method, we used a dynamically-allocated        *
//* block of memory to store all this data, but it was just too awkward.         *
//* We have made a decision that no password, home path or user name string is   *
//* likely to be more than 128 bytes, so we reserve an area of the following     *
//* size to hold the strings: 128 bytes per string * 7 strings == 896 bytes,     *
//* MAX_STRING bytes to be "safe".                                               *
//* The user could belong to an unlimited number of supplimentary groups, but    *
//* we store only the first uiMAX_sGID supplimentary-group IDs.                  *
//********************************************************************************

bool AcEdit::IdentifyUser ( void )
{
   #define DEBUG_IdentifyUser (0)   // for debugging only

   char pwBuff[gsDFLTBYTES] ;       // temporary buffer
   char gpBuff[gsDFLTBYTES] ;       // temporary buffer
   PwdInfo  pw ;                    // receives copy of kernel structure for user info
   PwdInfo* pwp ;                   // (this is completely worthless)
   GrpInfo  gp ;                    // pointer to kernel structure for group info
   GrpInfo* gpp ;                   // (this is completely worthless)
   uid_t    userID = getuid () ;    // get user's ID
   gid_t    grpID  = getgid () ;    // get user's group ID

   //* Get the user's name and other user information *
   this->userInfo.goodData = false ;

   if ( (getpwuid_r ( userID, &pw, pwBuff, gsDFLTBYTES, &pwp )) == ZERO )
   {
      this->userInfo.userID = pw.pw_uid ;
      this->userInfo.grpID  = pw.pw_gid ;

      //* Get the user's group information *
      if ( (getgrgid_r ( grpID, &gp, gpBuff, gsDFLTBYTES, &gpp )) == ZERO )
      {
         //* Get number of supplimentary groups user belongs to.    *
         //* Save the second copy of group ID and declare good data.*
         gid_t tg ;
         this->userInfo.suppGroups = getgroups ( ZERO, &tg ) ;
         this->userInfo.ggrpID = (gid_t)gp.gr_gid ;
         this->userInfo.goodData = true ;
      }

      //* Save the string data and initialize pointers to each string *
      char *tptr = (char*)this->userInfo.stringAlloc,    // target pointer
           *sptr,                                        // source pointer
           *eptr = tptr + uiMAX_STRING - 1 ;             // pointer to EOD
      short i = ZERO ;                                   // character index
      for ( short p = ZERO ; p < 7 ; ++p )
      {
         switch ( p )
         {
            case 0:  this->userInfo.userName = tptr ;             // user name
                     sptr = pw.pw_name ;              break ;
            case 1:  this->userInfo.realName = tptr ;             // real name
                     sptr = pw.pw_gecos ;             break ;
            case 2:  this->userInfo.shellProg = tptr ;            // shell program
                     sptr = pw.pw_shell ;             break ;
            case 3:  this->userInfo.homeDir = tptr ;              // home directory
                     sptr = pw.pw_dir ;               break ;
            case 4:  this->userInfo.grpName = tptr ;              // group name
                     sptr = gp.gr_name ;              break ;
            case 5:  this->userInfo.passWord = tptr ;             // password
                     sptr = pw.pw_passwd ;            break ;
            case 6:  this->userInfo.grpPWord = tptr ;             // group password
                     sptr = gp.gr_passwd ;            break ;
            default: break ;     // (should not happen)
         } ;
         i = ZERO ;
         while ( (sptr[i] != NULLCHAR) && (tptr < eptr) )
         { *(tptr++) = sptr[i++] ; } *(tptr++) = NULLCHAR ;
      }
      *eptr = NULLCHAR ;   // be sure string data are terminated

      //* If user belongs to supplimentary groups, store the first uiMAX_sGIDs *
      if ( this->userInfo.suppGroups > ZERO )
      {
         getgroups ( uiMAX_sGID, this->userInfo.suppGID ) ;
         //* If the only supplimentary group ID is a duplicate *
         //* of the primary GID, don't report it again         *
         if ( this->userInfo.suppGroups == 1 
              && (this->userInfo.suppGID[0] == this->userInfo.grpID) )
         { this->userInfo.suppGroups = ZERO ; }
         //* We only report the first uiMAX_SGID groups *
         else if ( this->userInfo.suppGroups > uiMAX_sGID )
            this->userInfo.suppGroups = uiMAX_sGID ;
      }
   }

   #if DEBUG_ACE != 0 && DEBUG_IdentifyUser != 0
   if ( this->win != NULL )   // Dump user info to display
   {
      WinPos wp( 2, 98 ) ;
      gString gs( " User Stats                    \n" ) ;
      this->win->acSetMod ( aesUNDERLINE ) ;
      wp = this->win->acWrite ( wp, gs ) ;
      this->win->acSetMod ( aesUNDERLINE_OFF ) ;

      gs.compose( " userName : '%s'\n"
                  " realName : '%s'\n"
                  "  grpName : '%s'\n"
                  "shellProg : '%s'\n"
                  "  homeDir : '%s'\n"
                  " passWord : '%s'\n"
                  " grpPWord : '%s'\n",
                  this->userInfo.userName, this->userInfo.realName,
                  this->userInfo.grpName, this->userInfo.shellProg,
                  this->userInfo.homeDir,
                  this->userInfo.passWord, this->userInfo.grpPWord ) ;
      wp = this->win->acWrite ( wp, gs ) ;
      if ( this->userInfo.suppGroups > ZERO )
      {
         gs = "  suppGID :" ;
         for ( short g = ZERO ; g < this->userInfo.suppGroups ; ++g )
            gs.append( " %u", &this->userInfo.suppGID[g] ) ;
         gs.append( L'\n' ) ;
         wp = this->win->acWrite ( wp, gs ) ;
      }
      gs.compose( "  userID  : %u\n"
                  "   grpID  : %u\n"
                  "  ggrpID  : %u\n"
                  "suppGroups: %hd goodData: %s\n"
                  "  Press Any Key...",
                  &this->userInfo.userID, &this->userInfo.grpID,
                  &this->userInfo.ggrpID, &this->userInfo.suppGroups,
                  (this->userInfo.goodData ? "Ok" : "Error" ) ) ;
      wp = this->win->acWrite ( wp, gs ) ;
      this->win->acPause () ;
   }
   #endif   // DEBUG_ACE && DEBUG_IdentifyUser

   return this->userInfo.goodData ;

   #undef DEBUG_IdentifyUser
}  //* End IdentifyUser() *

#if 0    // OBSOLETE - REFERENCE - scan4_Orphans()
bool AcEdit::scan4_Orphans ( void )
{
   #define DEBUG_ORPHANS (0)
   const int recWid = 128 ;   // nominal record width (window size unknown)
   gString gsCwd, gsDNames, gsFNames, gsSrc, gsTmp, gs ;
   tnFName tnfSrc, tnfTmp ;               // stats for source and temp files
   const wchar_t* wptr ;                  // pointer to source data
   wchar_t wbuff[recWid + 2] ;            // working buffer (isolate filenames)
   int   chCnt,                           // number of source characters
         sindx = ZERO,                    // source data index
         tindx ;                          // target buffer index
   short   dnCnt, fnCnt ;
   bool abort = false ;

   //* Scan the CWD. If scan fails, there is nothing to do, so early return.*
   this->getCWD ( gsCwd ) ;
   if ( !(this->readCwd ( gsCwd.ustr(), gsDNames, dnCnt, 
          gsFNames, fnCnt, recWid, true )) )
   { return abort ; }

   //* Scan the filename list for potential abandoned temp files.*
   wptr  = gsFNames.gstr() ;
   chCnt = gsFNames.gschars() ;
   while ( sindx < chCnt )
   {
      tindx = ZERO ;
      sindx += 3 ;                        // reference 1st char of filename
      if ( wptr[sindx] == PCHAR )
      {
         if ( wptr[sindx + 1] == L'#' )
         {
            while ( (wptr[sindx] != NEWLINE) && (wptr[sindx] != NULLCHAR) )
            {
               wbuff[tindx++] = wptr[sindx++] ;
               //* In the unlikely case that a filename is longer than *
               //* the target buffer, ...
            }
            wbuff[tindx] = NULLCHAR ;
            if ( wbuff[tindx - 1] == L'#' )
            {
               gString gssSpec, gstSpec ;    // filespec
               uint64_t srcBytes, tmpBytes ; // size of files in bytes
               int64_t tmpEpoch, srcEpoch ;  // mod time timestamp
               bool discard = false ;
               gsTmp = wbuff ;
               gsTmp.strip() ;            // remove leading/trailing whitespace

               //* Construct the matching source filename, and  *
               //* both filespecs.                              *
               //* We "know" that the temp file exists; however *
               //* it is not certain that source file exists.   *
               gsSrc = gsTmp ;
               gsSrc.limitChars( gsSrc.gschars() - 2 ) ;
               gsSrc.shiftChars( -2 ) ;
               gssSpec.compose( "%S/%S", gsCwd.gstr(), gsSrc.gstr() ) ;
               gstSpec.compose( "%S/%S", gsCwd.gstr(), gsTmp.gstr() ) ;
               if ( ((this->GetFileStats ( tnfSrc, gssSpec )) == ZERO) &&
                    ((this->GetFileStats ( tnfTmp, gstSpec )) == ZERO) )
               {
                  srcBytes = tnfSrc.fBytes ;
                  tmpBytes = tnfTmp.fBytes ;
                  srcEpoch = tnfSrc.modTime.epoch ;
                  tmpEpoch = tnfTmp.modTime.epoch ;

                  #if DEBUG_ACE != 0 && DEBUG_ORPHANS != 0
                  gsdbg.compose( "src: '%S'\n"
                                 "     epoch:%08llX  bytes:%llu\n"
                                 "tmp: '%S'\n"
                                 "     epoch:%08llX  bytes:%llu",
                                 gsSrc.gstr(), &srcEpoch, &srcBytes,
                                 gsTmp.gstr(), &tmpEpoch, &tmpBytes
                               ) ;
                  ofsdbg << gsdbg << endl ;
                  #endif   // DEBUG_ACE && DEBUG_ORPHANS

                  //* Compare timestamps. If source is newer, then *
                  //* mark temp file for deletion.                 *
                  if ( (srcEpoch >= tmpEpoch) ||
                       ((tmpEpoch - srcEpoch) <= 2) )
                  { discard = true ; }

                  //* Timestamp comparison was not definitive. *
                  //* Compare the files.                       *
                  else
                  {
#if 0    //       - Create a temp file which compares the files.
         //       - If identical, mark temp file for deletion.
         //       - Else:
         //       -   a) Insert the header information.
         //       -   b) Open dialog and ask user what to do: 
         //              display comparison, discard temp file, open temp file for edit,
         //              open BOTH for edit? Replace source with temp?
         //              Rename source, then name temp as source.
         //              Ask what to do with old source.
                     //* Create a log file to capture comparison data *
                     gString gsTempfile ;
                     this->win->acCreateTempfile ( gsTempfile ) ;

                     //* If files are identical, mark temp file for deletion *
                     if ( (this->compareFiles ( tnfSrc, tnfTmp, gsTempfile, L'L' )) )
                     {
                        discard = true ;
                     }

                     //* Show user results of the comparison *
                     else
                     {
                        this->orphanDialog ( tnfSrc, tnfTmp  ) ;
                     }


                     //* Delete the log file *
                     this->win->acDeleteTempfile ( gsTempfile ) ;
#endif   // U/C
                  }  // (timestamp compare)
               }     // (source and temp files exist)

               //* If no loss of data, delete the temp file *
               if ( discard )
               {
               }
               //* Else, ask user how to proceed *
               else
               {
               }
            }     // (is app temp filename)
         }        // (potentially app temp filename)
         else     // step over the record
            while ( (wptr[++sindx] != NEWLINE) && (wptr[sindx] != NULLCHAR) ) ;
      }
      //* Else, because the file sort will have placed the  *
      //* hidden files at the top of the list, we're done.  *
      else
         break ;
   }

   return abort ;

   #undef DEBUG_ORPHANS
}  //* End scan4_Orphans() *
#endif   // REFERENCE - scan4_Orphans()

//*************************
//*     compareFiles      *
//*************************
//********************************************************************************
//* Invoke the 'diff' utility to compare two files.                              *
//* The data are captured, formatted and written to the file specified by caller.*
//* Any existing data in the target file will be overwritten.                    *
//*                                                                              *
//* Files are compared for the following criteria:                               *
//* 1) timestamps                                                                *
//* 2) file size                                                                 *
//* 3) file type                                                                 *
//* 4) permission bits                                                           *
//* 5) Contents:                                                                 *
//*    The 'diff' utility has several options for scan and output, however,      *
//*    this method uses the side-by-side comparison option, with one of the      *
//*    following sub-options.                                                    *
//*    B) Brief report (summary only).                                           *
//*    F) Full side-by-side file display.                                        *
//*    L) Left column displays full text, right column displays only differences.*
//*    D) Differences only are reported                                          *
//*       (bracketed by a context line above and below).                         *
//*                                                                              *
//* Input  : priStat   : (by reference) full stats, including filespec of        *
//*                      primary file                                            *
//*          secStat   : (by reference) full stats, including filespec of        *
//*                      secondary file                                          *
//*          logFile   : (by reference) filespec of temporary file created by    *
//*                      caller.                                                 *
//*                      On return, file will contain results of comparison.     *
//*          format    : output format                                           *
//*                      B) Brief output: report only whether the files are      *
//*                         identical or different.                              *
//*                      F) Full text of both files, one in the left column and  *
//*                         the other in the right column                        *
//*                      L) Left column displays full text, but only differences *
//*                         are displayed in the right column.                   *
//*                      D) Report only the differences between the files.       *
//*                                                                              *
//* Returns: 'true' if files are identical, else 'false'                         *
//********************************************************************************
//* Useful 'diff' features:                                                      *
//* -----------------------                                                      *
//* This application uses only a few of the 'diff' options; however, greater     *
//* flexibility is available. See 'diff' docs for details.                       *
//*    a) --brief (-q) whether or not files are different                        *
//*        PLUS '-s' (--report-identical-files)                                  *
//*    b) --text  (-a) treat all files as if they were text                      *
//*    c) --context=LINES (-C LINES) amount of context around differing groups   *
//*    d) --side-by-side (-y) print output in two columns, one for each file     *
//*    e) --left-column  with side-by-side, print common lines only for the      *
//*         left column                                                          *
//*    f) --suppress-common-lines with side-by-side, do not print the common     *
//*         lines, only the differing lines                                      *
//*    g) --tabsize=COLUMNS i.e. 1 to avoid expanding tab characters in output   *
//*    h) --width=NUM (-W NUM) maximum number of columns in output               *
//*         default == 130 columns, BUT actual max == width of terminal          *
//*    i) --expand-tabs' ('-t'): replace tabs with equivalent number of spaces.  *
//*         Note that this actually screws up the tabs by pushing right-column   *
//*         data to the right, with the difference indicator in the right        *
//*         column rather than between columns as it should be.                  *
//*    j) --initial-tab places a tab after the indicator (single-column output)  *
//*         which pushes all data lines over by the width of a tab.              *
//********************************************************************************

bool AcEdit::compareFiles ( const tnFName& priStat, const tnFName& secStat, 
                            const gString& logFile, wchar_t format )
{
   const char* statHDR = "\n             TYPE    SIZE USR GRP OTH    INODE  MODIFIED" ;

   const short ldSIZE = (gsDFLTBYTES * 2) + 1 ;
   char  lineData[ldSIZE] ;               // raw UTF-8 input
   short termCols = this->wInfo.tcfg.termCols ;
   gString gsStats, gsNum, gsFmt, gsTs, gs ;
   gString gsCmd( "diff --width=%hd --brief -s %s %s 1>'%S' 2>&1", 
                  &termCols,
                  priStat.fSpec, secStat.fSpec, logFile.gstr() ) ;
   bool differ = false ;                  // set if files differ

   //* Range check the 'format' parameter. *
   switch ( format )
   {
      case L'B':  case L'F':  case L'L':  case L'D':  break ;
      case L'b':  format = L'B' ;      break ;
      case L'f':  format = L'F' ;      break ;
      case L'l':  format = L'L' ;      break ;
      case L'd':  format = L'D' ;      break ;
      default:    format = L'B' ;      break ;
   } ;


   //* Get the file difference in brief (identical/different) format. *
   //* Written to the caller's temporary file. The captured data      *
   //* should contain only one line, which ends with one of the       *
   //* following tokens: "identical" or "differ".                     *
   //* Note that the AnsiCmd library 'ShellOut' method is not invoked *
   //* here because is hides the window during the system call which  *
   //* is unnecessary here.                                           *
   system ( gsCmd.ustr() ) ;
   ifstream ifs( logFile.ustr(), ifstream::in ) ;
   if ( ifs.is_open() )
   {
      ifs.getline ( lineData, gsDFLTBYTES, NEWLINE ) ;
      if ( ifs.good() || ifs.gcount() > ZERO )
      {
         gs = lineData ;
         if ( (gs.find( "differ" )) >= ZERO )
            differ = true ;
      }
      ifs.close() ;
   }

   gsStats.compose( "FileA Path : %s\n"
                    "FileB Path : %s\n"
                    "%s\n",
                    priStat.fSpec, secStat.fSpec, statHDR ) ;
   gsNum.formatInt( priStat.fBytes, 7 ) ;
   this->formatTimestamp ( priStat.modTime, gsTs ) ;
   gsFmt = decodePermissions ( priStat.rawStats.stx_mode ) ;
   gsStats.append( "FileA Stats:  REG %S %S %08lld  %S\n", 
                   gsNum.gstr(), gsFmt.gstr(), &priStat.rawStats.stx_ino, gsTs.gstr() ) ;

   gsNum.formatInt( secStat.fBytes, 7 ) ;
   this->formatTimestamp ( secStat.modTime, gsTs ) ;
   gsFmt = decodePermissions ( secStat.rawStats.stx_mode ) ;
   gs.compose( "FileB Stats:  REG %S %S %08lld  %S\n", 
               gsNum.gstr(), gsFmt.gstr(), &secStat.rawStats.stx_ino, gsTs.gstr() ) ;
   gsTs.clear() ;                         // create a divider line
   gsTs.padCols( gs.gscols(), L'-' ) ;
   gs.append( gsTs.gstr() ) ;             // underline the file stats
   gsStats.append( "%S\n\n"
                   "Files %s and %s %s.\n\n",
                   gs.gstr(), priStat.fName, secStat.fName, 
                   (differ ? "differ" : "are identical") ) ;

   //* If Brief report or if reporting differences only,    *
   //* draw a line below the formatted data.                *
   //* Else draw the column headings for detailed output.   *
   if ( (format == L'B') || (format == L'D') )
      gsStats.append( "%S\n", gsTs.gstr() ) ;
   else
   {
      gs.clear() ;
      gs.padCols( this->wInfo.tcfg.termCols / 2 - 5, L'-' ) ;
      gsFmt.compose( "FileA: %s", priStat.fName ) ;
      gsFmt.padCols( termCols / 2 - 2 ) ;
      gsStats.append( "%S%s\n%S   %S", gsFmt.gstr(), secStat.fName, gs.gstr(), gs.gstr() ) ;
   }

   //* Write the data from the initial scan to the temp file.*
   ofstream ofs( logFile.ustr(), ofstream::trunc ) ;
   if ( ofs.is_open() )
   { ofs << gsStats.ustr() << endl ; ofs.close() ; }

   //* Adjust the 'diff' command to report the full text of source *
   //* on the left, and only differing lines on the right.         *
   if ( format == L'L' )      // full text in left column only
   {
      gsCmd.replace( L"--brief -s ", "--side-by-side --left-column " ) ;
   }
   else if ( format == L'F' ) // full text in both left and right columns
   {
      gsCmd.replace( L"--brief -s ", "--side-by-side " ) ;
   }
   else if ( format == L'D' ) // display only differences between files.
   {
      gsCmd.replace( L"--brief -s ", "--suppress-common-lines --context=1 " ) ;
   }
   if ( format != L'B' )      // detailed data will be appended to brief data
   {
      gsCmd.replace( L"1>", L"1>>" ) ;
      system ( gsCmd.ustr() ) ;
   }

   return differ ;

}  //* End compareFiles() *

//*************************
//*   decodePermissions   *
//*************************
//********************************************************************************
//* Non-member method:                                                           *
//* ------------------                                                           *
//* Decode the 'mode' bitfield permission bits into human readable form.         *
//*                                                                              *
//* Input  : mode     : the 'mode' bitfield to be decoded                        *
//*          compress : (optional, default: false)                               *
//*                     if set,   compress the output string by removing         *
//*                               some of the whitespace.                        *
//*                     if reset, use default format spacing                     *
//*                                                                              *
//* Returns: const char* to formatted text                                       *
//********************************************************************************

const char* decodePermissions ( uint16_t mode, bool compress )
{
   const char* widePerms = "%C %C %C  %C %C %C  %C %C %C" ;
   const char* compPerms = "%C%C%C %C%C%C %C%C%C" ;
   wchar_t r = L'r', w = L'w', x = L'x', n = L'-' ;
   static char perm[21] ;
   const char* pTemplate = compress ? compPerms : widePerms ;
   gString gs( pTemplate, 
               ((mode & S_IRUSR) ? &r : &n),
               ((mode & S_IWUSR) ? &w : &n),
               ((mode & S_IXUSR) ? &x : &n),

               ((mode & S_IRGRP) ? &r : &n),
               ((mode & S_IWGRP) ? &w : &n),
               ((mode & S_IXGRP) ? &x : &n),

               ((mode & S_IROTH) ? &r : &n),
               ((mode & S_IWOTH) ? &w : &n),
               ((mode & S_IXOTH) ? &x : &n)
             ) ;

   gs.copy( perm, 21 )  ;
   return ( perm ) ;

}  //* End decodePermissions() *

//*************************
//*    decodeFiletype     *
//*************************
//********************************************************************************
//* Non-member Method - For Development Only:                                    *
//* -----------------------------------------                                    *
//* Return a human-readable string corresponding to the specified file type.     *
//*                                                                              *
//* Input  : ftype : member of fmFType (Linux filetypes)                         *
//*                                                                              *
//* Returns: const pointer to file-type name                                     *
//********************************************************************************

const char* decodeFiletype ( fmFType ftype )
{
   const char* fTypes[fmTYPES] = 
   {
      "fmDIR_TYPE    ", "fmREG_TYPE    ", "fmLINK_TYPE   ", "fmCHDEV_TYPE  ", 
      "fmBKDEV_TYPE  ", "fmFIFO_TYPE   ", "fmSOCK_TYPE   ", "fmUNKNOWN_TYPE"
   } ;
   if ( (ftype < ZERO) || (ftype >= fmTYPES) )
      ftype = fmUNKNOWN_TYPE ;
   return ( fTypes[ftype] ) ;

}  //* End decodeFiletype() *

//*************************
//*   reportTermWinUser   *
//*************************
//********************************************************************************
//* Display terminal window and application window configurations.               *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* Notes:                                                                       *
//* 1) 'csize' bits: 0x00==5 bits, 0x01==6 bits, 0x02==7 bits, 0x03==8 bits.     *
//*    Obviously, they never heard of wchar_t characters i.e. byte stream only.  *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//* Get the terminal emulator name, by grepping the parents process name.        *
//* Therefore it works with every terminal emulator.                             *
//********************************************************************************

void AcEdit::reportTermWinUser ( void )
{
   static const char* rtwTitle[uilENV] =
   {
      " Terminal and Application Configuration Data ",   // English
      " 终端与应用程序配置数据 ",                                   // Zhongwen
   } ;
   static const char* rtwWTemplate[uilENV] =
   {
    // English                                                               80 COLS->|
      "Application Window Configuration              User Information\n"
      "─────────────────────────────────────────────┬──────────────────────────────────\n"
      "Position: %hd,%hd   Height:%-2hd   Width:%-3hd        │ User ID : %04d %s\n"
      "Field Count:%-2hd  Focus:%-2hd    Interior:%hd,%-3hd  │ Group ID: %04d %s\n"
      "                                             │ UserName: %s\n"
      "Terminal Window Configuration                │ Home    : %s\n",
    // Zhongwen                                                              80 COLS->|
      "应用程序窗口配置                              用户信息\n"
      "─────────────────────────────────────────────┬──────────────────────────────────\n"
      "位置: %hd,%hd       高度:%-2hd     宽度:%-3hd         │ 用户身份: %04d %s\n"
      "字段计数:%-2hd     焦点字段:%-2hd 内部尺寸:%hd,%-3hd  │ 组标识符: %04d %s\n"
      "                                             │ 用户名  : %s\n"
      "终端窗口配置                                 │ 主目录  : %s\n",
   } ;
   static const char* rtwTTemplate1[uilENV] =
   {
    // English                                                               80 COLS->|
      "─────────────────────────────────────────────┼──────────────────────────────────\n"
      "istrip : 7-bit input                %S │ c_cc[%hd] \n"
      "opost  : output flags               %S │ c_cc[VTIME==%hd]:%02hhXh \n"
      "onlcr  : convert newlines to cr/lf  %S │ c_cc[VMIN==%hd]:%02hhXh \n"
      "xtabs  : convert tabs to spaces     %S │ inpBuf:%hhd suspCap:%hhd flowCap:%hhd\n"
      "cread  : read terminal input        %S │ inpBlk:%hhd quitCap:%hhd inpEch :%hd(%s)\n"
      "csize  : character width            %hd bits   │ curType:%hd(%s) cchType:%hd(%s)\n"
      ,

    // Zhongwen                                                              80 COLS->|
      "─────────────────────────────────────────────┼──────────────────────────────────\n"
      "istrip : 7-bit input                %S │ c_cc[%hd] \n"
      "opost  : output flags               %S │ c_cc[VTIME==%hd]:%02hhXh \n"
      "onlcr  : convert newlines to cr/lf  %S │ c_cc[VMIN==%hd]:%02hhXh \n"
      "xtabs  : convert tabs to spaces     %S │ inpBuf:%hhd suspCap:%hhd flowCap:%hhd\n"
      "cread  : read terminal input        %S │ inpBlk:%hhd quitCap:%hhd inpEch :%hd(%s)\n"
      "csize  : character width            %hd bits   │ curType:%hd(%s) cchType:%hd(%s)\n"
      ,
   } ;
   static const char* rtwTTemplate2[uilENV] =
   {
    // English                                                               80 COLS->|
      "icanon : enable canonical input     %S │ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌\n"
      "echo   : echo character input       %S │ Dimensions: %s\n"
      "echonl : echo newlines              %S │ Term   : %s\n"
      "echoctl: echo control chars as (^x) %S │ Version: %s\n"
      "iexten : posix extensions           %S │ Shell  : %s\n\n",
    // Zhongwen                                                              80 COLS->|
      "icanon : enable canonical input     %S │ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌\n"
      "echo   : echo character input       %S │ 方面   : %s\n"
      "echonl : echo newlines              %S │ 终端   : %s\n"
      "echoctl: echo control chars as (^x) %S │ 版本   : %s\n"
      "iexten : posix extensions           %S │ 软件   : %s\n\n",
   } ;
   static const wchar_t* rtwFlags[uilENV][2] =
   {
      {                                   // English
         L"disabled",
         L"enabled "
      },
      {                                   // Zhongwen
         L"已禁用  ",
         L"已启用  "
      },
   } ;


   gString gsOut, gs ;                    // text formatting

   //* Get a copy of the skForm 'protected' members. *
   WinPos base( 0, 0 ) ; wchar_t pKey = L'?' ;
   short seli = ZERO ; bool foot = false, dlg = false ;
   this->wInfo.skf->skfProtectedMembers ( base, pKey, seli, foot, dlg ) ;
   //* Limit width of various information strings *
   const short charAVAIL = 36, colsAVAIL = 24 ;
   char buffA[charAVAIL], buffB[charAVAIL], buffC[charAVAIL], buffD[charAVAIL] ;
   gs = this->userInfo.userName;
   gs.limitCols( colsAVAIL ) ;
   gs.copy( buffA, charAVAIL ) ;
   gs = this->userInfo.grpName;
   gs.limitCols( colsAVAIL ) ;
   gs.copy( buffB, charAVAIL ) ;
   gs = this->userInfo.realName;
   gs.limitCols( colsAVAIL + 5 ) ;
   gs.copy( buffC, charAVAIL ) ;
   gs = this->userInfo.homeDir ;
   gs.limitCols( colsAVAIL ) ;
   gs.copy( buffD, charAVAIL ) ;

   gsOut.compose( rtwWTemplate[this->lang],
      &this->wInfo.winBase.row, &this->wInfo.winBase.col,
      &this->wInfo.height,      &this->wInfo.width,
      &this->userInfo.userID,   buffA,
      &this->wInfo.skf->fCnt,   &this->wInfo.skf->fi,   
      &this->wInfo.txtRows,     &this->wInfo.txtCols,
      &this->userInfo.grpID,    buffB, buffC, buffD
      ) ;

   short nccs  = NCCS,                    // C-library constants
         vtime = VTIME,
         vmin  = VMIN,
         csize  = short(((this->wInfo.tcfg.tios.c_cflag & CSIZE) >> 4) + 5) ; // (see note above)
   bool istrip  = (this->wInfo.tcfg.tios.c_iflag & ISTRIP), // human-readable bitfields
        opost   = (this->wInfo.tcfg.tios.c_oflag & OPOST),
        onlcr   = (this->wInfo.tcfg.tios.c_oflag & ONLCR),
        xtabs   = (this->wInfo.tcfg.tios.c_oflag & XTABS),
        cread   = (this->wInfo.tcfg.tios.c_cflag & CREAD),
        icanon  = (this->wInfo.tcfg.tios.c_lflag & ICANON),
        echo    = (this->wInfo.tcfg.tios.c_lflag & ECHO),
        echonl  = (this->wInfo.tcfg.tios.c_lflag & ECHONL),
        echoctl = (this->wInfo.tcfg.tios.c_lflag & ECHOCTL),
        iexten  = (this->wInfo.tcfg.tios.c_lflag & IEXTEN) ;
   char seOpt[16] ;                       // human-readable code for echo option
   wchar_t seoptChar = (this->wInfo.tcfg.inpEch == softEchoC ? L'c' :
                        this->wInfo.tcfg.inpEch == softEchoD ? L'd' :
                        this->wInfo.tcfg.inpEch == softEchoE ? L'e' : L'a' ) ;
   gs.compose( "s%C", &seoptChar ) ;
   gs.copy( seOpt, 16 ) ;

   gsOut.append( rtwTTemplate1[this->lang],
      rtwFlags[this->lang][istrip], &nccs, 
      rtwFlags[this->lang][opost],  &vtime, &this->wInfo.tcfg.tios.c_cc[VTIME], 
      rtwFlags[this->lang][onlcr],  &vmin, &this->wInfo.tcfg.tios.c_cc[VMIN], 
      rtwFlags[this->lang][xtabs],  &this->wInfo.tcfg.inpBuf, &this->wInfo.tcfg.suspCap, &this->wInfo.tcfg.flowCap,
      rtwFlags[this->lang][cread],  
      &this->wInfo.tcfg.inpBlk, &this->wInfo.tcfg.quitCap, &this->wInfo.tcfg.inpEch,
      (this->wInfo.tcfg.inpEch == termEcho ? "t" : 
       this->wInfo.tcfg.inpEch == noEcho ? "n" : seOpt),
      &csize, &this->wInfo.tcfg.curStyle,
      (this->wInfo.tcfg.curStyle == csBblock ? "bb" : 
       this->wInfo.tcfg.curStyle == csSblock ? "sb" : 
       this->wInfo.tcfg.curStyle == csBuline ? "bu" :
       this->wInfo.tcfg.curStyle == csSuline ? "su" : 
       this->wInfo.tcfg.curStyle == csBvbar  ? "bv" : 
       this->wInfo.tcfg.curStyle == csSvbar  ? "sv" : "d"),
      &this->wInfo.tcfg.cchType,
      (this->wInfo.tcfg.cchType == cchtExit   ? "e" : 
       this->wInfo.tcfg.cchType == cchtUser   ? "u" : 
       this->wInfo.tcfg.cchType == cchtIgnore ? "i" :
       this->wInfo.tcfg.cchType == cchtCopy   ? "c" : "t")
      ) ;

   //* Scan the system files to obtain the name and procID for the *
   //* terminal emulator program which contains the application.   *
   gString gsTemp ;
   this->win->acCreateTempfile ( gsTemp ) ;
   getEmulator ( gs, gsTemp.ustr() ) ;
   this->win->acDeleteTempfile ( gsTemp ) ;
   gs.gscanf( "%s : %s : %s", buffA, buffB, buffC ) ;

   gs = buffA ;
   gs.limitCols( colsAVAIL ) ;
   gs.copy( buffA, charAVAIL ) ;
   gs = buffC ;
   gs.limitCols( colsAVAIL ) ;
   gs.copy( buffB, charAVAIL ) ;
   gs.compose( "%hd x %hd", &this->wInfo.tcfg.termRows, &this->wInfo.tcfg.termCols ) ;
   // Programmer's Note: The word 'Dimensions' cuts into the space available
   // for the 'buffC' argument; however, the arg is only two numeric values.
   gs.limitCols( colsAVAIL - 3 ) ;
   gs.copy( buffC, charAVAIL ) ;
   gs = this->userInfo.shellProg ;
   gs.limitCols( colsAVAIL ) ;
   gs.copy( buffD, charAVAIL ) ;

   gsOut.append( rtwTTemplate2[this->lang],
      rtwFlags[this->lang][icanon],
      rtwFlags[this->lang][echo], buffC, 
      rtwFlags[this->lang][(echonl && icanon)], buffA, 
      rtwFlags[this->lang][echoctl], buffB, 
      rtwFlags[this->lang][iexten], buffD
      ) ;

   this->infoDialog ( gsOut, false, rtwTitle[this->lang] ) ;

}  //* End reportTermWinUser() *

//*************************
//*      getEmulator      *
//*************************
//********************************************************************************
//* Non-member Method:                                                           *
//* ------------------                                                           *
//* Traverse the parent PID records until the terminal emulator name is found.   *
//* or until the top of the process tree is reached.                             *
//* Return the two substring values separated by a colon token (" :").           *
//*    Example: "gnome-terminal :4495"                                           *
//*             |               |.... process ID                                 *
//*             |.............. emulator name                                    *
//* If specified, also return the version number substring for the emulator.     *
//*    Example: "gnome-terminal :4495 :3.56.3                                    *
//*             |               |     |...... process ID                         *
//*             |               |.... process ID                                 *
//*             |.............. emulator name                                    *
//*                                                                              *
//* The interesting data are in (at least) two files:                            *
//*      '/proc/PROCNUM/stat' and '/proc/PROCNUM/comm'                           *
//*                                                                              *
//* The 'stat' file's first line contains a bunch of stuff, but it is the        *
//* third (2nd) and fourth (4th) tokens that are of interest:                    *
//* token #1: this is the process ID for the current file.                       *
//* token #2: this is the name of the the process (enclosed in parentheses)      *
//*           Note that this may be some variation of the actual name; however,  *
//*           it is the canonical application name that is returned.             *
//* token #3: this is the letter 'S'. and we currently don't care what it means. *
//* token #4: this is the parent process's procID.                               *
//*                                                                              *
//* The 'comm' file (usually) has only one line and (usually) that line contains *
//* only the emulator name; but because this string is a duplicate of token #3   *
//* of the 'stat' file, we take a shortcut and do not open the 'comm' file.      *
//*                                                                              *
//* Input  : gsEmu  : (by reference) receives name of terminal emulator and      *
//*                   emulator version (see example)                             *
//*          tmpSpec: (optional, default:null pointer) if specified, this is a   *
//*                   filespec for a temporary file to be used to obtain the     *
//*                   version number of the terminal emulator.                   *
//*                   Note: If a valid emulator name but unable to capture       *
//*                         version number, the returned data will not include   *
//*                         the version number.                                  *
//*                                                                              *
//* Returns: 'true' if emulator identified, process name and process ID returned.*
//*          'false' if unable to determine emulator name.                       *
//*                  (buffer will contain an empty string)                       *
//*                  Note: It is possible that a valid, but unrecognized terminal*
//*                        emulator name may be encountered. The name of that    *
//*                        emulator may be added to the 'emuNames' array.        *
//********************************************************************************
//* Invoking a version number report for each of the listed emulator programs    *
//* is shown.                                                                    *
//*   gnome-terminal --version                                                   *
//*      # GNOME Terminal 3.56.3 using VTE 0.80.5 +BIDI +GNUTLS +ICU +SYSTEMD    *
//*   konsole --version (-v)                                                     *
//*      konsole 25.12.3                                                         *
//*   xterm -version                                                             *
//*      XTerm(397)                                                              *
//*   guake ?                                                                    *
//*   tilda --version                                                            *
//*   terminator --version                                                       *
//*   yakuake --version (-v)                                                     *
//*                                                                              *
//* Other Emulators:                                                             *
//* Alacritty, Kitty, Tilix, WezTerm, Foot, LXterminal, Sakura, st, PuTTY,       *
//* xfce4-terminal, urxvt, rxvt,                                                 *
//*  ---  ---   ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  *
//* Programmer's Note: This method leverages the gString-class functionality     *
//* for handling the substrings; however, if you are still programming with      *
//* stone knives and bear skins, the 'sscanf' and 'snprintf' functions could     *
//* be used directly.                                                            *
//*  ---  ---   ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  *
//* Programmer's Note: The name of the current shell program could be obtained   *
//* using this same algorithm; however, we get that information from a different *
//* source. See IdentifyUser().                                                  *
//********************************************************************************

static bool getEmulator ( gString& gsEmu, const char* tmpSpec )
{
   const short enMAX = 7 ;
   const char* emuNames[enMAX]
   {
      "gnome-terminal",
      "konsole",
      "xterm",
      "guake",
      "tilda",
      "terminator",
      "yakuake",
   } ;
   const char* emuVers[enMAX]
   {
      "gnome-terminal --version",
      "konsole --version",
      "xterm -version",
      "guake --version", // (uncertain)
      "tilda --version",
      "terminator --version",
      "yakuake --version",
   } ;
   const char* scanTemplate = "%d %s %s %d" ;
   gString gsSpec, gs ;
   pid_t pid = getppid (),    // PARENT process ID 
         spid, ppid ;
   char raw[256], pname[64], toss[64] ;
   ifstream ifs ;
   int indx ;
   bool status = true ;
   gsEmu.clear() ;                        // initialize caller's buffer

   for ( short p = 5 ; (p > ZERO) && (status != false) ; --p )
   {
      gsSpec.compose( "/proc/%d/stat", &pid ) ;
      ifs.open ( gsSpec.ustr(), ifstream::in ) ;
      if ( ifs.is_open() )
      {
         ifs.getline ( raw, 256, NEWLINE ) ; // get the first line of the file
         if ( (ifs.good() || ifs.gcount() > ZERO) )
         {
            gs = raw ;
            if ( (gs.gscanf( scanTemplate, &spid, pname, toss, &ppid )) == 4 )
            {
               //* Note that a ppid==1 indicates the top of the process tree.*
               if ( (spid == pid) && (ppid > 1) )
               {
                  //* Strip the '()' from 'pname' , then *
                  //* test for something that looks like *
                  //* the name of a terminal emulator.   *
                  gs = pname ;
                  if ( (indx = gs.find( L'(')) >= ZERO )
                     gs.erase( indx, 1 ) ;
                  if ( (indx = gs.find( L')')) >= ZERO )
                     gs.erase( indx, 1 ) ;
                  gs.copy( pname, 64 ) ;
                  for ( indx = ZERO ; indx < enMAX ; ++indx )
                  { if ( (gs.find( emuNames[indx] )) >= ZERO ) { break ; } }

                  //* If a matching emulator name, *
                  //* construct the return value.  *
                  if ( indx < enMAX )
                  {
                     gsEmu.compose( "%s :%d", emuNames[indx], &pid ) ;
                     ifs.close() ;        // close the file
                     break ;              // success
                  }

                  pid = ppid ;            // move to the next higher level
               }
               else
                  status = false ;
            }
            else { status = false ; }
         }
         else { status = false ; }
         ifs.close() ;                    // close the file
      }
   }

   //* If specified, capture the version number.*
   if ( status && tmpSpec != NULL )
   {
      const char* cmdTemplate = 
               "%s 1>\"%s\" 2>/dev/null" ;
      //* Invoke the application, requesting version number. 
      gsSpec.compose( cmdTemplate, emuVers[indx], tmpSpec ) ;
      system ( gsSpec.ustr() ) ;
      ifs.open ( tmpSpec, ifstream::in ) ;
      if ( ifs.is_open() )
      {
         ifs.getline ( raw, 256, NEWLINE ) ; // get the first line of the file
         if ( (ifs.good()) || (ifs.gcount() > ZERO) )
         {
            //* Extract the first numeric substring *
            gs = raw ;
            const wchar_t* wptr = gs.gstr() ;
            wchar_t verString[64] ;
            int   eof = gs.gschars(), veri = ZERO ;
            indx = ZERO ;
            while ( (indx < eof) && 
                    !((wptr[indx] >= L'0') && (wptr[indx] <= L'9')) )
            { ++indx ; }
            if ( indx < eof )
            {
               while ( (indx < eof) && 
                       (((wptr[indx] >= L'0') && (wptr[indx] <= L'9')) 
                         || (wptr[indx] == L'.')) )
               { verString[veri++] = wptr[indx++] ; }
               verString[veri] = NULLCHAR ;
               gsEmu.append( " :%S", verString ) ;
            }
         }
         ifs.close() ;
      }
   }

   return status ;

}  //* End getEmulator() *

// ==============================================================================

//*************************
//*      dumpWinInfo      *
//*************************
//********************************************************************************
//* For Development Only:                                                        *
//* ---------------------                                                        *
//* Display the window configuration information.                                *
//* NOTE: The 'wInfo' member must have been initialized.                         *
//*                                                                              *
//* Input  : outRow : output row                                                 *
//*          outCol : output column                                              *
//*          ofsdump: (optional, default: null pointer)                          *
//*                   if specified, && if target file is open,                   *
//*                                    write data to target file                 *
//*                   else, write data to terminal window                        *
//*                                                                              *
//* Returns: current cursor position                                             *
//********************************************************************************

#if DEBUG_ACE != 0
WinPos AcEdit::dumpWinInfo ( short outRow, short outCol, ofstream* ofsdump )
{
   //* Formatting template for display of field info.*
   const char* fldTemplate = "%-9s: orig:%2hd,%2hd  hgt,wid:%3hd:%3hd %hhd:%hhd\n" ;
   bool dump2file = bool((ofsdump != NULL) && (ofsdump->is_open())) ;

   WinPos wpdbg( outRow, outCol ) ;

   this->updateWindow () ;
   #if DEBUG_DUMP_WINFO != 0
   if ( ! dump2file )
   {
      this->win->acSetAttributes ( acaATTR_DFLT ) ;
      this->win->acClearArea ( outRow, outCol, short(fiFIELDS + 5), 43/*, L'.'*/ ) ;
   }
   #endif   // DEBUG_DUMP_WINFO

   //* Get a copy of the skForm 'protected' members. *
   WinPos base( 0, 0 ) ; wchar_t pKey = L'?' ;
   short seli = ZERO ; bool foot = false, dlg = false ;
   this->wInfo.skf->skfProtectedMembers ( base, pKey, seli, foot, dlg ) ;

   gsdbg.compose(
      "Window Configuration\n"
      "-------------------------------------------\n"
      "Position: %hd,%hd   Height:%hd   Width:%hd\n"
      "                Interior:%hd,%hd\n"
      "skForm fCnt:%hd   fi:%hd   base:%hd,%hd      hi:ro\n",
      &this->wInfo.winBase.row,  &this->wInfo.winBase.col,
      &this->wInfo.height,       &this->wInfo.width,
      &this->wInfo.txtRows,      &this->wInfo.txtCols,
      &this->wInfo.skf->fCnt,    &this->wInfo.skf->fi,   
      &base.row, &base.col
      ) ;
   if ( dump2file )
   { *ofsdump << gsdbg ; ofsdump->flush() ; }
   #if DEBUG_DUMP_WINFO != 0
   else { wpdbg = this->win->acWrite ( wpdbg, gsdbg ) ; }
   #endif   // DEBUG_DUMP_WINFO

   gsdbg.compose( fldTemplate, "fiLINEvl",
      &this->wInfo.skf->fld[fiLINEvl].orig.row,  &this->wInfo.skf->fld[fiLINEvl].orig.col,
      &this->wInfo.skf->fld[fiLINEvl].hgt,       &this->wInfo.skf->fld[fiLINEvl].wid,
      &this->wInfo.skf->fld[fiLINEvl].hide,      &this->wInfo.skf->fld[fiLINEvl].ro ) ;
   gsdbg.append( fldTemplate, "fiLINEvr",
      &this->wInfo.skf->fld[fiLINEvr].orig.row,  &this->wInfo.skf->fld[fiLINEvr].orig.col,
      &this->wInfo.skf->fld[fiLINEvr].hgt,       &this->wInfo.skf->fld[fiLINEvr].wid,
      &this->wInfo.skf->fld[fiLINEvr].hide,      &this->wInfo.skf->fld[fiLINEvr].ro ) ;
   gsdbg.append( fldTemplate, "fiLINEhu",
      &this->wInfo.skf->fld[fiLINEhu].orig.row,  &this->wInfo.skf->fld[fiLINEhu].orig.col,
      &this->wInfo.skf->fld[fiLINEhu].hgt,       &this->wInfo.skf->fld[fiLINEhu].wid,
      &this->wInfo.skf->fld[fiLINEhu].hide,      &this->wInfo.skf->fld[fiLINEhu].ro ) ;
   gsdbg.append( fldTemplate, "fiLINEhl",
      &this->wInfo.skf->fld[fiLINEhl].orig.row,  &this->wInfo.skf->fld[fiLINEhl].orig.col,
      &this->wInfo.skf->fld[fiLINEhl].hgt,       &this->wInfo.skf->fld[fiLINEhl].wid,
      &this->wInfo.skf->fld[fiLINEhl].hide,      &this->wInfo.skf->fld[fiLINEhl].ro ) ;
   if ( dump2file )
   { *ofsdump << gsdbg ; ofsdump->flush() ; }
   #if DEBUG_DUMP_WINFO != 0
   else { wpdbg = this->win->acWrite ( wpdbg, gsdbg ) ; }
   #endif   // DEBUG_DUMP_WINFO

   gsdbg.compose( fldTemplate, "fiTEXTfw",
      &this->wInfo.skf->fld[fiTEXTfw].orig.row,  &this->wInfo.skf->fld[fiTEXTfw].orig.col, 
      &this->wInfo.skf->fld[fiTEXTfw].hgt,       &this->wInfo.skf->fld[fiTEXTfw].wid,
      &this->wInfo.skf->fld[fiTEXTfw].hide,      &this->wInfo.skf->fld[fiTEXTfw].ro ) ;
   gsdbg.append( fldTemplate, "fiTEXThu",
      &this->wInfo.skf->fld[fiTEXThu].orig.row,  &this->wInfo.skf->fld[fiTEXThu].orig.col, 
      &this->wInfo.skf->fld[fiTEXThu].hgt,       &this->wInfo.skf->fld[fiTEXThu].wid,
      &this->wInfo.skf->fld[fiTEXThu].hide,      &this->wInfo.skf->fld[fiTEXThu].ro ) ;
   gsdbg.append( fldTemplate, "fiTEXThl",
      &this->wInfo.skf->fld[fiTEXThl].orig.row, &this->wInfo.skf->fld[fiTEXThl].orig.col, 
      &this->wInfo.skf->fld[fiTEXThl].hgt,      &this->wInfo.skf->fld[fiTEXThl].wid,
      &this->wInfo.skf->fld[fiTEXThl].hide,     &this->wInfo.skf->fld[fiTEXThl].ro ) ;
   gsdbg.append( fldTemplate, "fiTEXTvl",
      &this->wInfo.skf->fld[fiTEXTvl].orig.row, &this->wInfo.skf->fld[fiTEXTvl].orig.col, 
      &this->wInfo.skf->fld[fiTEXTvl].hgt,      &this->wInfo.skf->fld[fiTEXTvl].wid,
      &this->wInfo.skf->fld[fiTEXTvl].hide,     &this->wInfo.skf->fld[fiTEXTvl].ro ) ;
   gsdbg.append( fldTemplate, "fiTEXTvr",
      &this->wInfo.skf->fld[fiTEXTvr].orig.row, &this->wInfo.skf->fld[fiTEXTvr].orig.col, 
      &this->wInfo.skf->fld[fiTEXTvr].hgt,      &this->wInfo.skf->fld[fiTEXTvr].wid,
      &this->wInfo.skf->fld[fiTEXTvr].hide,     &this->wInfo.skf->fld[fiTEXTvr].ro ) ;
   if ( dump2file )
   { *ofsdump << gsdbg ; ofsdump->flush() ; }
   #if DEBUG_DUMP_WINFO != 0
   else { wpdbg = this->win->acWrite ( wpdbg, gsdbg ) ; }
   #endif   // DEBUG_DUMP_WINFO

   gsdbg.compose( fldTemplate, "fiHDEV",
      &this->wInfo.skf->fld[fiHDIV].orig.row,    &this->wInfo.skf->fld[fiHDIV].orig.col,
      &this->wInfo.skf->fld[fiHDIV].hgt,         &this->wInfo.skf->fld[fiHDIV].wid,
      &this->wInfo.skf->fld[fiHDIV].hide,        &this->wInfo.skf->fld[fiHDIV].ro ) ;
   gsdbg.append( fldTemplate, "fiGUTTER",
      &this->wInfo.skf->fld[fiGUTTER].orig.row,  &this->wInfo.skf->fld[fiGUTTER].orig.col, 
      &this->wInfo.skf->fld[fiGUTTER].hgt,       &this->wInfo.skf->fld[fiGUTTER].wid,
      &this->wInfo.skf->fld[fiGUTTER].hide,      &this->wInfo.skf->fld[fiGUTTER].ro ) ;
   gsdbg.append( fldTemplate, "fiMENUBAR",
      &this->wInfo.skf->fld[fiMENUBAR].orig.row, &this->wInfo.skf->fld[fiMENUBAR].orig.col, 
      &this->wInfo.skf->fld[fiMENUBAR].hgt,      &this->wInfo.skf->fld[fiMENUBAR].wid,
      &this->wInfo.skf->fld[fiMENUBAR].hide,     &this->wInfo.skf->fld[fiMENUBAR].ro ) ;
   gsdbg.append( fldTemplate, "fiM_FILE",
      &this->wInfo.skf->fld[fiM_FILE].orig.row,  &this->wInfo.skf->fld[fiM_FILE].orig.col, 
      &this->wInfo.skf->fld[fiM_FILE].hgt,       &this->wInfo.skf->fld[fiM_FILE].wid,
      &this->wInfo.skf->fld[fiM_FILE].hide,      &this->wInfo.skf->fld[fiM_FILE].ro ) ;
   if ( dump2file )
   { *ofsdump << gsdbg ; ofsdump->flush() ; }
   #if DEBUG_DUMP_WINFO != 0
   else { wpdbg = this->win->acWrite ( wpdbg, gsdbg ) ; }
   #endif   // DEBUG_DUMP_WINFO

   gsdbg.compose( fldTemplate, "fiM_EDIT",
      &this->wInfo.skf->fld[fiM_EDIT].orig.row,  &this->wInfo.skf->fld[fiM_EDIT].orig.col, 
      &this->wInfo.skf->fld[fiM_EDIT].hgt,       &this->wInfo.skf->fld[fiM_EDIT].wid,
      &this->wInfo.skf->fld[fiM_EDIT].hide,      &this->wInfo.skf->fld[fiM_EDIT].ro ) ;
   gsdbg.append( fldTemplate, "fiM_VIEW",
      &this->wInfo.skf->fld[fiM_VIEW].orig.row,  &this->wInfo.skf->fld[fiM_VIEW].orig.col, 
      &this->wInfo.skf->fld[fiM_VIEW].hgt,       &this->wInfo.skf->fld[fiM_VIEW].wid,
      &this->wInfo.skf->fld[fiM_VIEW].hide,      &this->wInfo.skf->fld[fiM_VIEW].ro ) ;
   gsdbg.append( fldTemplate, "fiM_UTIL",
      &this->wInfo.skf->fld[fiM_UTIL].orig.row,  &this->wInfo.skf->fld[fiM_UTIL].orig.col, 
      &this->wInfo.skf->fld[fiM_UTIL].hgt,       &this->wInfo.skf->fld[fiM_UTIL].wid,
      &this->wInfo.skf->fld[fiM_UTIL].hide,      &this->wInfo.skf->fld[fiM_UTIL].ro ) ;
   gsdbg.append( fldTemplate, "fiM_HELP",
      &this->wInfo.skf->fld[fiM_HELP].orig.row,  &this->wInfo.skf->fld[fiM_HELP].orig.col, 
      &this->wInfo.skf->fld[fiM_HELP].hgt,       &this->wInfo.skf->fld[fiM_HELP].wid,
      &this->wInfo.skf->fld[fiM_HELP].hide,      &this->wInfo.skf->fld[fiM_HELP].ro ) ;
   if ( dump2file )
   { *ofsdump << gsdbg ; ofsdump->flush() ; }
   #if DEBUG_DUMP_WINFO != 0
   else { wpdbg = this->win->acWrite ( wpdbg, gsdbg ) ; }
   #endif   // DEBUG_DUMP_WINFO

   return ( wpdbg ) ;

}  //* End dumpWinInfo() *
#endif   // DEBUG_ACE

//*************************
//*     dumpTermInfo      *
//*************************
//********************************************************************************
//* For Development Only:                                                        *
//* ---------------------                                                        *
//* Display the AnsiCmd terminal window configuration information.               *
//*                                                                              *
//* Input  : outRow : output row                                                 *
//*        : outCol : output column                                              *
//*          ofsdump: (optional, default: null pointer)                          *
//*                   if specified, && if target file is open,                   *
//*                                    write data to target file                 *
//*                   else, write data to terminal window                        *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* Notes:                                                                       *
//* 1) 'csize' bits: 0x00==5 bits, 0x01==6 bits, 0x02==7 bits, 0x03==8 bits.     *
//*    Obviously, they never heard of wchar_t characters i.e. byte stream only.  *
//*                                                                              *
//*                                                                              *
//********************************************************************************

WinPos AcEdit::dumpTermInfo ( short outRow, short outCol, ofstream* ofsdump )
{
   #if DEBUG_ACE != 0
   const wchar_t * en = L"enabled " ;
   const wchar_t * di = L"disabled" ;
   WinPos wp( outRow, outCol ) ;          // position for output (return value)
   gString gs ;                           // text formatting
   winInfo   winfo ;                      // receives configuration data
   this->win->WinInfo ( winfo ) ;         // request configuration data
   short csize  = short(((winfo.tcfg.tios.c_cflag & CSIZE) >> 4) + 5) ; // (see note above)
   bool istrip  = (winfo.tcfg.tios.c_iflag & ISTRIP), // human-readable bitfields
        opost   = (winfo.tcfg.tios.c_oflag & OPOST),
        onlcr   = (winfo.tcfg.tios.c_oflag & ONLCR),
        xtabs   = (winfo.tcfg.tios.c_oflag & XTABS),
        cread   = (winfo.tcfg.tios.c_cflag & CREAD),
        icanon  = (winfo.tcfg.tios.c_lflag & ICANON),
        echo    = (winfo.tcfg.tios.c_lflag & ECHO),
        echonl  = (winfo.tcfg.tios.c_lflag & ECHONL),
        echoctl = (winfo.tcfg.tios.c_lflag & ECHOCTL),
        iexten  = (winfo.tcfg.tios.c_lflag & IEXTEN) ;
   bool write2file = bool((ofsdump != NULL) && (ofsdump->is_open())) ;
   char seOpt[16] ;                       // human-readable code for echo option
   wchar_t seoptChar = (winfo.tcfg.inpEch == softEchoC ? L'c' :
                        winfo.tcfg.inpEch == softEchoD ? L'd' :
                        winfo.tcfg.inpEch == softEchoE ? L'e' : L'a' ) ;
   gs.compose( "s%C", &seoptChar ) ;
   gs.copy( seOpt, 16 ) ;

   //* Display the title *
   gs = "Terminal Configuration\n"
        "---------------------------------------------\n" ;
   if ( write2file )
   { *ofsdump << gs ; ofsdump->flush() ; }
   else
   {
      this->updateWindow () ;             // refresh the visible window
      this->win->acSetAttributes ( acaATTR_DFLT ) ; // set terminal default attributes
      wp = this->win->acWrite ( wp, gs ) ;
   }

   //* Display the interesting flags in human-readable format.*
   gs.compose( "istrip : 7-bit input                 %S\n"
               "opost  : output flags                %S\n"
               "onlcr  : convert newlines to cr/lf   %S\n"
               "xtabs  : convert tabs to spaces      %S\n"
               "cread  : read terminal input         %S\n"
               "csize  : character width             %hd bits\n"
               "icanon : enable canonical input      %S\n"
               "echo   : echo character input        %S\n"
               "echonl : echo newlines               %S\n"
               "echoctl: echo control chars as (^x)  %S\n"
               "iexten : posix extensions            %S\n",
               (istrip ? en : di),
               (opost ? en : di),
               (onlcr ? en : di),
               (xtabs ? en : di),
               (cread ? en : di),
               &csize,
               (icanon ? en : di),
               (echo ? en : di),
               ((echonl && icanon) ? en : di),
               (echoctl ? en : di),
               (iexten ? en : di) ) ;
   if ( write2file )
   { *ofsdump << gs ; ofsdump->flush() ; }
   else
      wp = this->win->acWrite ( wp, gs ) ;

   //* Array of control characters *
   short nccs  = NCCS,
         vtime = VTIME,
         vmin  = VMIN ;
   gs.compose( "c_cc[%hd] c_cc[VTIME==%hd]:%hhXh c_cc[VMIN==%hd]:%hhXh\n",
               &nccs, &vtime, &winfo.tcfg.tios.c_cc[VTIME], 
               &vmin, &winfo.tcfg.tios.c_cc[VMIN] ) ;
   if ( write2file )
   { *ofsdump << gs ; ofsdump->flush() ; }
   else
      wp = this->win->acWrite ( wp, gs ) ;

   //* Input buffering, signal processing and cursor configuration *
   gs.compose( "inpBuf:%hhd suspCap:%hhd flowCap:%hhd    curType:%hd(%s)\n"
               "inpBlk:%hhd quitCap:%hhd inpEch :%hd(%s) cchType:%hd(%s)\n",
              &winfo.tcfg.inpBuf, &winfo.tcfg.suspCap, &winfo.tcfg.flowCap,
              &winfo.tcfg.curStyle,
              (winfo.tcfg.curStyle == csBblock ? "bb" : 
               winfo.tcfg.curStyle == csSblock ? "sb" : 
               winfo.tcfg.curStyle == csBuline ? "bu" :
               winfo.tcfg.curStyle == csSuline ? "su" : 
               winfo.tcfg.curStyle == csBvbar  ? "bv" : 
               winfo.tcfg.curStyle == csSvbar  ? "sv" : "d"),
              &winfo.tcfg.inpBlk,
              &winfo.tcfg.quitCap, 
              &winfo.tcfg.inpEch, 
              (winfo.tcfg.inpEch == termEcho ? "t" : 
               winfo.tcfg.inpEch == noEcho ? "n" : seOpt),
              &winfo.tcfg.cchType,
              (winfo.tcfg.cchType == cchtExit   ? "e" : 
               winfo.tcfg.cchType == cchtUser   ? "u" : 
               winfo.tcfg.cchType == cchtIgnore ? "i" :
               winfo.tcfg.cchType == cchtCopy   ? "c" : "t")
            ) ;
   if ( write2file )
      *ofsdump << gs << endl ;
   else
      wp = this->win->acWrite ( wp, gs ) ;

   return ( wp ) ;
   #endif   // DEBUG_ACE

}  //* End dumpTermInfo() *

#if DEBUG_ACE != 0      // For use during development only.
//*************************
//*      dumpFileMan      *
//*************************
//********************************************************************************
//* For Development Only:                                                        *
//* ---------------------                                                        *
//* Display the data of the specified FileMan record.                            *
//*                                                                              *
//* Input  : fmanPtr: pointer to FileMan record to be displayed                  *
//*          dlg    : (optional, default: false)                                 *
//*                   false: report the data for target record to log file.      *
//*                   true : report the data for target record in a quick dialog *
//*          verbose: (optional, default: false)                                 *
//*                   in addition to the target record, report the indexing      *
//*                   data for 'scrdata'                                         *
//*          title  : (optional, default: null pointer)                          *
//*                   specify a title for record display                         *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void AcEdit::dumpFileMan ( const FileMan* fmanPtr, bool dlg, 
                           bool verbose, const char* title )
{
   #if DEBUG_DUMP_FINFO != 0
   const wchar_t* pathBreakPos = L"/AnsiCmd" ;
   const wchar_t* pathBreakIns = L" \n                " ;
   const int      pathBreakAdv = 25 ;

   gString gsOut, gs ;
   int     ix = ZERO ;
   short   fmindx = ZERO ;

   for (  ; fmindx < MAX_FMANS ; ++fmindx )  // calculate the field index
   {
      if ( fmanPtr == &this->fman[fmindx] )
         break ;
   }
   if ( title != NULL )                   // if title specified
      gs.compose( " - %s", title ) ;
   else
      gs.clear() ;

   gsOut.compose( "FileMan%S\n-------------------------------- \n"
                  "%02hd) Filename: '%s'\n"
                  "     srcSpec: '%s'\n" 
                  "     wrkSpec: '%s'\n",
                  gs.gstr(),
                  &fmindx, fmanPtr->tnf.fName,
                  fmanPtr->srcSpec,
                  fmanPtr->wrkSpec
                ) ;

   //* This splits the filespec string so it will fit in the dialog *
   while ( (ix = gsOut.find( pathBreakPos, ix )) > ZERO )
   { gsOut.insert( pathBreakIns, ix ) ; ix += pathBreakAdv ; }

   gs.formatInt( fmanPtr->scrCnt, 5, true ) ; // size of work buffer
   gsOut.append(
      "     teindx :%s  lnindx :%s  sindx :%02hd \n"
      "     vtop   :%-3d voff   :%-3d hoff   :%-3d edtime:%X\n"
      "     open   :%hhd   mods   :%hhd   wmods  :%hhd   active:%hhd",
      fldId[fmanPtr->tindx], fldId[fmanPtr->lindx], &fmanPtr->sindx,
      &fmanPtr->vtop, &fmanPtr->voff, &fmanPtr->hoff, &fmanPtr->edtime,
      &fmanPtr->open, &fmanPtr->mods, &fmanPtr->wmods, &fmanPtr->active ) ;

   if ( dlg )
   {
      gsOut.append( L"\n\n" ) ;
      this->infoDialog ( gsOut, false, "  FileMan Class  ", 1, -1 ) ;
   }
   else if ( ofsdbg.is_open() )
   {
      ofsdbg << gsOut << endl ;

      //* If verbose output specified, report the  *
      //* structured data of the work buffer.      *
      if ( verbose )
      {
// CZONE - the scratch buffer data structure is not yet defined. (also sbytes)
      }
   }
   // Note: If ofsdbg not available, it would be a program error.

   #endif   // DEBUG_DUMP_FINFO
}  //* End dumpFileMan() *

//*************************
//*      dumpFileMan      *
//*************************
//********************************************************************************
//* For Development Only:                                                        *
//* ---------------------                                                        *
//* Display all records in the FileMan array or a subset of the records.         *
//* Written to logfile only. Sequentially call the dumpFileMan() overload above  *
//* for each of the specified records.                                           *
//*                                                                              *
//* A developer may ask, "Why is this debugging routine so fancy?"               *
//* Answer:                                                                      *
//*    "Designing the debugging routines is a concrete expression of the         *
//*     thought process which informs the overall project development.           *
//*          FileMan functionality."                                             *
//*                                                                              *
//* Input  : filter : (optional, default: 0)                                     *
//*                   specify which group of records are to be reported          *
//*                   -1 - all records, summary only                             *
//*                    0 - all records, full record                              *
//*                    1 - files open for edit only (incl. file to be opened)    *
//*                    2 - recently edited records only (not open files)         *
//*                    3 - open && "recent" records                              *
//*                    4 - active group: fman[AREC] && fman[IREC]                *
//*                    5 - active record and its referenced record:              *
//*                        fman[AREC] && fman[fman[AREC].sindx]                  *
//*          verbose: (optional, default: false)                                 *
//*                   -- For summary report only (filter==-1)                    *
//*                      if set, report all records,                             *
//*                      if reset, report only non-empty record.                 *
//*                   -- For all other filter options:                           *
//*                      if set, then in addition to the target record,          *
//                               report contents of its scratch buffer (if any)  *
//*          title  : (optional, default: null pointer)                          *
//*                   specify a title for record display                         *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void AcEdit::dumpFileMan ( short filter, bool verbose, const char* title )
{
   #if DEBUG_DUMP_FINFO != 0
   if ( ofsdbg.is_open() )
   {
      gString gs ;
      short openRecords   = ZERO,
            recentRecords = ZERO,
            inuseRecords  = ZERO,
            freeRecords   = ZERO,
            cols ;

      //* Construct the header record *
      for ( short r = ZERO ; r < MAX_FILES ; ++r )
      {
         // Programmer's Note: Even unsaved buffers have data in the 'fName' member.
         if ( *this->fman[r].tnf.fName != NULLCHAR )
         {
            if ( this->fman[r].open )  { ++openRecords ; }
            else                       { ++recentRecords ; }
         }
         else                          { ++freeRecords ; }
      }
      inuseRecords = openRecords + recentRecords ;
      gs.compose( 
         "FileMan records: open:%02hd recent:%02hd inuse:%02hd free:%02hd\n",
         &openRecords, &recentRecords, &inuseRecords, &freeRecords ) ;
      cols = gs.gscols() ;
      gs.padCols( cols * 2, L'=' ) ;
      ofsdbg << gs << endl ;

      //* Summary-only report *
      if ( filter < ZERO )
      {
         if ( title != NULL )
         {
            gs.compose( "%s\n", title ) ;
            gs.padCols( (cols + gs.gscols()), L'-' ) ;
            ofsdbg << gs << endl ;
         }
         for ( short r = ZERO ; r < MAX_FMANS ; ++r )
         {
            if ( verbose || (r >= MAX_FILES) ||
                 (*this->fman[r].tnf.fName != NULLCHAR) )
            {
               gs.compose( "%02hd) open:%hhd active:%hhd ", &r, 
                           &this->fman[r].open, &this->fman[r].active ) ;
               if ( r >= MAX_FILES )
                  gs.append( "tindx:%02hd lindx:%02hd sindx:%02hd ",
                             &this->fman[r].tindx, &this->fman[r].lindx,
                             &this->fman[r].sindx ) ;
               gs.append( "'%s'", this->fman[r].tnf.fName ) ;
               ofsdbg << gs << endl ;
            }
         }
         return ;                         // note the early return
      }

      //* Format for display all records specified by the 'filter' option.*
      const FileMan* activeTarget = NULL ; // used only by option 5
      bool dumpit ;
      for ( short r = ZERO ; r < MAX_FMANS ; ++r )
      {
         dumpit = (filter == ZERO) ? true : false ;
         if ( (filter == 1) && (r < MAX_FILES) )
         { if ( this->fman[r].open ) dumpit = true ; }
         else if ( (filter == 2) && (r < MAX_FILES) )
         { if ( !this->fman[r].open && (*this->fman[r].srcSpec != NULLCHAR) ) dumpit = true ; }
         else if ( (filter == 3) && (r < MAX_FILES) )
         { if ( this->fman[r].open || (*this->fman[r].srcSpec != NULLCHAR) ) dumpit = true ; }
         else if ( (filter == 4) && (r >= MAX_FILES) ) dumpit = true ;
         else if ( (filter == 5) && (r >= MAX_FILES) && this->fman[r].active )
         {
            activeTarget = &this->fman[this->fman[r].sindx] ;
            dumpit = true ;
         }

         if ( dumpit )
            this->dumpFileMan ( &this->fman[r], false, verbose, title ) ;
      }
      if ( activeTarget != NULL )
         this->dumpFileMan ( activeTarget, false, verbose, "active target" ) ;
   }

   #endif   // DEBUG_DUMP_FINFO
}  //* End dumpFileMan() *

//*************************
//*     dumpFileStats     *
//*************************
//********************************************************************************
//* For Development Only:                                                        *
//* ---------------------                                                        *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//********************************************************************************

void AcEdit::dumpFileStats ( short fmIndx )
{
   const short timeLEN = 20 ; // timestamp buffer length
   const short unLEN   = 32 ; // user-name buffer length
   const short unWID   = 19 ; // user-id/name width
//   const wchar_t R = L'r', W = L'w', X = L'x', O = L'-' ;
//   const char* permTemplate = "%C%C%C %C%C%C %C%C%C" ;

   if ( (fmIndx >= ZERO) && (fmIndx < MAX_FMANS) && (ofsdbg.is_open()) )
   {
      const tnFName* tnfp = &this->fman[fmIndx].tnf ;
      gString gs;
      localTime lt ;
      char mtime[timeLEN], atime[timeLEN], ctime[timeLEN], btime[timeLEN] ;
      char uname[unLEN], fbytes[unLEN], ftype[unLEN], persimmons[unLEN] ;
//      wchar_t persimmons[21] = L"- - -  - - -  - - -" ;
//      uint16_t mode = tnfp->rawStats.stx_mode ;

      gs = decodeFiletype ( tnfp->fType) ;     // decode file type
      gs.copy( ftype, unLEN ) ;

      //* Format timestamps *
      this->epoch2Timestamp ( tnfp->rawStats.stx_mtime.tv_sec, gs ) ;
      gs.copy( mtime, timeLEN ) ;
      this->epoch2Timestamp ( tnfp->rawStats.stx_atime.tv_sec, gs ) ;
      gs.copy( atime, timeLEN ) ;
      this->epoch2Timestamp ( tnfp->rawStats.stx_ctime.tv_sec, gs ) ;
      gs.copy( ctime, timeLEN ) ;
      this->epoch2Timestamp ( tnfp->rawStats.stx_btime.tv_sec, gs ) ;
      gs.copy( btime, timeLEN ) ;

      gs.formatInt( tnfp->fBytes, 7, true ) ; // byte count
      gs.copy( fbytes, unLEN ) ;

      //* Format owner ID and name (if the same) *
      gs.compose( "%4d ", &tnfp->rawStats.stx_uid) ;
      if ( this->userInfo.goodData && 
           (this->userInfo.userID == tnfp->rawStats.stx_uid) )
         gs.append( "%s", this->userInfo.userName ) ;
      gs.padCols( unWID ) ;
      gs.limitCols( unWID ) ;          // truncate if necessary
      gs.copy( uname, unLEN ) ;

      //* Format the file permissions *
      gs = decodePermissions ( tnfp->rawStats.stx_mode, true ) ;
      gs.copy( persimmons, unLEN ) ;

      gs.compose( "%02hd) %s\n"
                  "    %s\n"
                  "    Modify: %s │ Type : %s\n"
                  "    Access: %s │ Bytes: %s\n"
                  "    Stats : %s │ INODE: %lld\n"
                  "    Birth : %s │ Links: %-3d\n"
                  "    Owner : %s │ Perms: %s",
                  &fmIndx, tnfp->fName, tnfp->fSpec,
                  mtime, ftype, 
                  atime, fbytes, 
                  ctime, &tnfp->rawStats.stx_ino, 
                  btime, &tnfp->rawStats.stx_nlink, 
                  uname, persimmons ) ;
      ofsdbg << gs << endl ;
   }

}  //* End dumpFileStats() *

//*************************
//*     dumpTimestamp     *
//*************************
//********************************************************************************
//* For Development Only:                                                        *
//* ---------------------                                                        *
//* Display the contents of a localTime object. This include the epoch time      *
//* and the broken-down (human readable) data.                                   *
//*                                                                              *
//* The header line is not used for output to the window footer; however,        *
//* for output to the log file only:                                             *
//* To fascilitate comparison of records the combination of title and header     *
//* may be configured to display two records on sequential lines.                *
//* TITLE    HDR                                                                 *
//* -----    ---                                                                 *
//* "xxx"      1  title\n header\n data                                          *
//* "xxx"      0  data\n title                                                   *
//* ""         1  header\n data                                                  *
//* ""         0  data                                                           *
//*                                                                              *
//*                                                                              *
//* Input  : lt     : (by reference) data to be displayed                        *
//*          title  : (optional, default: null pointer) if specified, title text *
//*                   Note: if target is footer output, title limited to 12 cols.*
//*          hdr    : (optional, default: true) - applies only to logfile output *
//*                   if 'true'  include header text in report                   *
//*                   if 'false' report only timestamp data (no header)          *
//*          footer : (optional, default: false)                                 *
//*                   if 'false' write the formatted data to the debug log       *
//*                   if 'true'  write the formatted data as a footnote in the   *
//*                              parent window                                   *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void AcEdit::dumpTimestamp ( const localTime& lt, const char* title, bool hdr, bool footer )
{
   #if DEBUG_DUMP_FINFO != 0
   const char* hdrText = 
      " epoch     sysepoch  dt mo year   hr mn se da ju  gmtoff  dst  loc   utc\n" ;
   gString gs ;

   //* If output to window footer, disable header *
   //* and limit width of title text.             *
   if ( footer )
   {
      hdr = false ;
      if ( title != NULL )
      {
         gs = title ;
         gs.limitCols( 12 ) ;
         title = NULL ;
      }
   }

   if ( (title != NULL) && hdr )
      gs.compose( "%s\n", title ) ;
   if ( hdr )
      gs.append( hdrText ) ;
   gs.append(
      " %08Xn %08Xh %02hu %02hu %4hu : %02hu %02hu %02hu %2hu %03hu"
      " %+6d  %2hhd  '%s' '%s'",
   &lt.epoch,   &lt.sysepoch,  &lt.date,   &lt.month,   &lt.year,
   &lt.hours,   &lt.minutes,   &lt.seconds, 
   &lt.day,     &lt.julian,    &lt.gmtoffset,  &lt.dst,
   lt.timezone, lt.utc_zone ) ;

   if ( (title != NULL) && !hdr )
      gs.append( "\n%s\n", title ) ;

   if ( !footer && (ofsdbg.is_open()) )
      ofsdbg << gs << endl ;

   else if ( footer && (this->win->EnableFootnotes ( true )) )
      this->win->Footnote ( gs, ZERO ) ;

   #endif   // DEBUG_DUMP_FINFO
}  //* End dumpTimestamp() *
#endif   // DEBUG_ACE   // For use during development only.

//*************************
//*   SetFileTimestamp    *
//*************************
//********************************************************************************
//* For Debugging Only. Not used in production code.                             *
//* ------------------------------------------------                             *
//* Set file Modify and/or Access date/timestamps.                               *
//* As a side effect, cTime will be set to current system time.                  *
//* (requires file write permission)                                             *
//*                                                                              *
//* Input  : pathBuff  : full path/filename specification                        *
//*          ft        : structure must be correctly initialized                 *
//*                      (except for 'day' which is ignored)                     *
//*          updateMod : (optional, 'true' by default)                           *
//*                      if true, update modification time                       *
//*          updateAcc : (optional, 'true' by default)                           *
//*                      if true, update access time                             *
//*                                                                              *
//* Returns: zero if successful,                                                 *
//*          else code from 'errno' file does not exist, no write permission,etc.*
//********************************************************************************
//* Notes:                                                                       *
//* 1) The utime() function takes an argument of:                                *
//*     struct utimbuf        // used for call to utime()                        *
//*     {                                                                        *
//*        time_t actime ;    // access time                                     *
//*        time_t modtime ;   // modification time                               *
//*     } ;                                                                      *
//* 2) To _also_ set the fractional seconds, see the utimes() function           *
//*    which takes an argument of: struct timeval TVP[2], i.e. one struct for    *
//*    each timestamp (modTime and accTime)                                      *
//*    From: /usr/include/bits/types/struct_timeval.h>                           *
//*     struct timeval                                                           *
//*     {                                                                        *
//*     #ifdef __USE_TIME64_REDIRECTS                                            *
//*       __time64_t tv_sec;            /* Seconds.  */                          *
//*       __suseconds64_t tv_usec;      /* Microseconds.  */                     *
//*     #else                                                                    *
//*       __time_t tv_sec;              /* Seconds.  */                          *
//*       __suseconds_t tv_usec;        /* Microseconds.  */                     *
//*     #endif                                                                   *
//*     };                                                                       *
//*    For most applications, this additional resolution is not necessary.       *
//********************************************************************************

#if DEBUG_ACE != 0
#include <utime.h>
typedef struct utimbuf uTime ;

short AcEdit::SetFileTimestamp ( const char* pathBuff, const localTime& ft, 
                                 bool updateMod, bool updateAcc )
{

   short status = ZERO ;                  // return value

   if ( updateMod || updateAcc )
   {
      FileStats rawStats ;
      if ( (statx ( sx_dirfd, pathBuff, sx_flags, sx_mask, &rawStats )) == ZERO )
      {  //* Retrieve the target file's current time values.   *
         //* Note that 'utime' always set both mod time and    *
         //* access time; therefore, initialize both values    *
         //* to current time.                                  *
         uTime ut ;
         ut.actime  = rawStats.stx_atime.tv_sec ;
         ut.modtime = rawStats.stx_mtime.tv_sec ;

         //* Convert formatted time to simple epoch time.*
         //* Note: 'ft' is const so we need a copy.      *
         //*      (copy integrity has been verified)     *
         localTime tmp_ft = ft ; 
         time_t newTime = (time_t)this->timestamp2Epoch ( tmp_ft ) ;

         if ( updateMod )  ut.modtime = newTime ;  // new modification time
         if ( updateAcc )  ut.actime = newTime ;   // new access time
         if ( (utime ( pathBuff, &ut )) != ZERO )
            status = errno ;
      }
      else
        status = errno ;
   }
   return status ;

}  //* End SetFileTimestamp() *

//*************************
//*    timestamp2Epoch    *
//*************************
//********************************************************************************
//* Convert human-readable time to GNU/UNIX epoch time code:                     *
//* (seconds since the epoch: Jan. 01, 1970)                                     *
//*                                                                              *
//* Input  : ftTime : (by reference)                                             *
//*                   members 'sysepoch' and 'epoch' are initialized             *
//*                                                                              *
//* Returns: 64-bit epoch time                                                   *
//********************************************************************************
//* Programmer's Note: If we are beyond the dreaded year 2038 overflow point,    *
//* the assignment to member time_t sysepoch may be truncated here.              *
//*                                                                              *
//* Note also that caller could have nonstandard data in one or more fields      *
//* which the 'timelocal' function may not catch OR may obligingly use to create *
//* an unexpected time.                                                          *
//* Note: The localTime class uses unsigned 16-bit values for most fields, but   *
//*       the struct tm equivalents are _signed_ int variables.                  *
//*       Positive values are handled smoothly; however, a negative value        *
//*       written to an unsigned target could cause the resulting epoch to be    *
//*       off by some number of years. Be Aware!                                 *
//* Note: 'timelocal' returns (-1) on error, but accepts and accurately encodes  *
//*        data like: 2016-02-21T19:95:72                                        *
//*               At this time, it's not certain that error                      *
//*               checking the input fields for positive values                  *
//*               is actually worth the additional CPU cycles.                   *
//*                                                                              *
//* Important Note:                                                              *
//* The timelocal() function does not simply perform the conversion from         *
//* human-readable time to epoch time. It calculates the timezone date (often    *
//* incorrectly) and adjusts the hour accordingly.                               *
//* From the docs:                                                               *
//* -- 'mktime' is functionally identical to timelocal except for variable names.*
//*       "Calling ‘mktime’ also sets the time zone state as if ‘tzset’ were     *
//*        called; ‘mktime’ uses this information instead of BROKENTIME's        *
//*        initial ‘tm_gmtoff’ and ‘tm_zone’ members.  See Time Zone State().    *
//*        ---                                                                   *
//*        Note that 'tzset' references the obsolescent TZ environment variable. *
//*       'mktime' emulates this by calculating tm_gmtoff as well as tm_zone     *
//*       tm_wday and tm_yday based on the other data members (and possibly      *
//*       other criteria.                                                        *
//*       -- It is allowed for those other data members to have "unnormalized"   *
//*          values (e.g. tm_hour == 50).                                        *
//*    Because the GMT offset is ignored, then if the computer has moved to      *
//*    another timezone, the calculated epoch may not be as expected.            *
//*    For this reason, comparing file timestamps on a travelling computer       *
//*    may not be reliable.                                                      *
//* See also:                                                                    *
//*     struct tm * gmtime_r ( const time_t *TIME, struct tm *RESULTP );         *
//*     time_t timegm ( struct tm*BROKENTIME );                                  *
//*                                                                              *
//* -- We have instituted a hack to correct this problem. If the source GMT      *
//*    offset has been initialized BUT it is different from the calculated       *
//*    GMT offset, then adjust the 'tm_hour' and 'tm_min' values and recalculate.*
//*    -- Both tm_hour and tm_min are needed because not all timezones are       *
//*       set on an even hour hour. At least one is set on the half-hour.        *
//*       (tm_sec should never need adjustment).                                 *
//*    This hack appears to work, but will likely never be executed unless, like *
//*    your humble author, you spend a lot of time in airports and hotel rooms.  *
//*    This hack should be revisited at intervals, especially when updating      *
//*    the compiler version.                                                     *
//********************************************************************************

int64_t AcEdit::timestamp2Epoch ( localTime& ftTime )
{
   #define EET_TZ_HACK (1)    // SEE NOTE ABOVE

   #if 0    // DEPRECATED
   //* In the unlikely event that we receive what caller    *
   //* probably though were negative values in the uint16_t *
   //* members, convert to positive values.                 *
   if ( ftTime.hours & 0x8000 )
      ftTime.hours = (ZERO - short(ftTime.hours)) & 0x07FFF ;
   if ( ftTime.minutes & 0x8000 )
      ftTime.minutes = (ZERO - short(ftTime.minutes)) & 0x07FFF ;
   if ( ftTime.seconds & 0x8000 )
      ftTime.seconds = (ZERO - short(ftTime.seconds)) & 0x07FFF ;
   if ( ftTime.date & 0x8000 )
      ftTime.date = (ZERO - short(ftTime.date)) & 0x07FFF ;
   if ( ftTime.month & 0x8000 )
      ftTime.month = (ZERO - short(ftTime.month)) & 0x07FFF ;
   if ( ftTime.year & 0x8000 )
      ftTime.year = (ZERO - short(ftTime.year)) & 0x07FFF ;
   #endif   // DEPRECATED

   //* Convert formatted time to simple epoch time *
   Tm tm ;
   tm.tm_mday = ftTime.date ;
   tm.tm_mon  = ftTime.month - 1 ;
   tm.tm_year = ftTime.year - 1900 ;
   tm.tm_hour = ftTime.hours ;
   tm.tm_min  = ftTime.minutes ;
   tm.tm_sec  = ftTime.seconds ;
   // The tm_gmtoff(GMT offset), tm_wday(day-of-the-week) and 
   // tm_yday(julian day) are ignored by the timelocal() call.

   ftTime.sysepoch = timelocal ( &tm ) ;     // (see mktime())

   #if EET_TZ_HACK != 0
   //* See notes, above...                               *
   //* If the calculation crosses into a different day,  *
   //* then the date can-o-worms opens, although the     *
   //* timelocal() functions SHOULD handle it smoothly.  *
   if ( (ftTime.gmtoffset != ZERO) && (tm.tm_gmtoff != ftTime.gmtoffset) )
   {
      // GMT total offset difference (in seconds)
      int gmtdiff  = ftTime.gmtoffset - tm.tm_gmtoff ;
      // GMT offset (full hours)
      int hourdiff = gmtdiff / 3600 ;
      // GMT offset remainder (minutes). Note: remainder in seconds SHOULD BE zero.
      int mindiff  = (gmtdiff % 60) ;
      if ( (tm.tm_hour + hourdiff) >= ZERO )
      {
         tm.tm_hour += hourdiff ;
         if ( (tm.tm_min  + mindiff) >= ZERO )
            tm.tm_min  += mindiff ;
         ftTime.sysepoch = timelocal ( &tm ) ;
      }
   }
   #endif   // EET_TZ_HACK

   if ( ftTime.sysepoch >= ZERO )
   {
      ftTime.epoch = (int64_t)ftTime.sysepoch ; // (see note above)
      ftTime.day = tm.tm_wday ;
      ftTime.julian = tm.tm_yday ;
   }
   else     // error condition returned, reset caller's time data
      ftTime.reset () ;

   return ftTime.epoch ;

}  //* End timestamp2Epoch() *

#if DEBUG_ACE != 0 && DEBUG_PSRC != 0
//*************************
//*         psrc          *
//*************************
//********************************************************************************
//* For Development Only: Protect Source                                         *
//* ------------------------------------                                         *
//* This operation protects the source files from modification.                  *
//* For each file listed in the 'fman' array, make a copy of the source file     *
//* with an encoded name and update the fman record to reference the copy,       *
//* NOT the original source file.                                                *
//*                                                                              *
//* Encoding of the Filename: The letter 'x' is inserted just before the '.'     *
//* character. Ex: "CommFile.cpp" becomes "CommFilex.cpp"                        *
//*                                                                              *
//* Input  : set : if set, then copy the souce file(s) using the encoding names. *
//*                if reset, delete the file(s) with encoded names               *
//*                                                                              *
//* Returns: number of records updated                                           *
//********************************************************************************
//* Notes:                                                                       *
//* ------                                                                       *
//* -- The tnf.fSpec member is modified to reference the encoded-name file.      *                                                                             *
//* -- The tnf.fName member retains the original source filename.                *
//*    This is the name reported in the Gutter field.                            *
//* -- The other relevant stats (e.g. mod date, r/w permissions, etc.) are       *
//*    duplicated in the target file and should be correct.                      *
//* -- (Yes, this is a bit kludgy, but again: "It's Only a Northern Song".)      *
//********************************************************************************

short AcEdit::psrc ( bool set )
{
   #define DEBUG_PS (0)                   // for debugging only

   static short updates = ZERO ;          // number of records updated
   gString gsPath, gsName, gsReal, gs ;
   int psi ;                              // text index
   //* If file has been modified, we may want to look *
   //* at it, so if 'retain' is set AND if timestamps *
   //* differ, don't delete file during shutdown.     *
   //* If not modified, always delete.                *
   tnFName tnf ;
   time_t tStamp ;
   bool retain = false ;

   for ( short r = ZERO ; r < MAX_FILES ; ++r )
   {
      //* If the record references an existing file *
      if ( *this->fman[r].tnf.fSpec != NULLCHAR )
      {
         this->GetRealpath ( this->fman[r].tnf.fSpec, gsReal ) ; // source filespec

         #if DEBUG_PS!= 0
         if ( ofsdbg.is_open() )
         {
            gsdbg.compose( "psrc %02hd) spec  '%s'\n"
                           "   r:%02hd  real  '%s'",
              &updates, this->fman[r].tnf.fSpec, &r, gsReal.ustr() ) ;
            ofsdbg << gsdbg << endl ;
         }
         #endif   // DEBUG_PS

         if ( set )                       // create encoded-name file
         {
            this->ExtractPathname ( gsPath, gsReal ) ;   // pathspec
            this->ExtractFilename ( gsName, gsReal ) ;   // filename

            #if DEBUG_PS!= 0
            if ( ofsdbg.is_open() )
            {
               gsdbg.compose( "         path  '%s'\n"
                              "         name  '%s'", gsPath.ustr(), gsName.ustr() ) ;
               ofsdbg << gsdbg << endl ;
            }
            #endif   // DEBUG_PS

            if ( ((psi = gsName.findlast( L'.' )) > ZERO) &&
                  (gsName.gstr()[psi] != L'x') )
            {
               gsName.insert( L'x', psi ) ;
               gs.compose( "%S/%S", gsPath.gstr(), gsName.gstr() ) ;
               this->CopyFile ( gsReal.ustr(), gs.ustr() ) ;
               gs.copy( this->fman[r].tnf.fSpec, MAX_PATH ) ;
               ++updates ;

            #if DEBUG_PS!= 0
            if ( ofsdbg.is_open() )
            {
               gsdbg.compose( "        pname  '%s'\n"
                              "        pspec  '%s'", 
                 gsName.ustr(), this->fman[r].tnf.fSpec ) ;
               ofsdbg << gsdbg << endl ;
            }
            #endif   // DEBUG_PS
            }
         }
         else                             // if an encoded filename, delete the file
         {
            if ( ((psi = gsReal.findlast( L'.' )) > ZERO) &&
                  (gsReal.gstr()[psi - 1] == L'x') )
            {
               #if DEBUG_PS!= 0
               if ( ofsdbg.is_open() )
               {
                  gsdbg.compose( "       delete  '%s'\n", gsReal.ustr() ) ;
                  ofsdbg << gsdbg << endl ;
               }
               #endif   // DEBUG_PS

               tStamp = this->fman[r].tnf.modTime.epoch ; // current mod time
               if ( retain )              // get source file mod date
               {
                  if ( ((psi = gsReal.findlast( L'.' ) - 1) > ZERO) &&
                       (gsReal.gstr()[psi] == L'x') )
                  {
                     gs = gsReal ;
                     gs.replace( L"x.", L".", psi ) ;
                     this->GetFileStats( tnf, gs ) ;
                     tStamp = tnf.modTime.epoch ;
                  }
               }
               if ( ! retain || (tStamp == this->fman[r].tnf.modTime.epoch) )
                  this->DeleteFile ( gsReal.ustr() ) ;
               --updates ;
            }
         }
      }
   }
   if ( ! set && (updates != ZERO) )
      this->win->ttyWrite ( "** psrc error **\n" ) ; // report problem

   return updates ;

   #undef DEBUG_PS
}  //* End psrc() *
#endif   // DEBUG_ACE && DEBUG_PSRC

#endif   // DEBUG_ACE - For use during development only.

#if 0    // REFERENCE - Undo/Redo
// Track undo/redo
// See Wikipedia: "Command Pattern" and "Memento Pattern"
// a) This is a LIFO (stack) queue so it needs an index for last in, 
//    and a next-free. wrap-around.
//     i) data in 0 <= item index < item max
//    ii) if queue is full and we get an item push, then it would be stored at item 0.
//   iii) This queue could be exercised statically using nominal tokens.
// b) A redo removes the record from the queue. A 1-level redo is probably OK.
// c) Cursor key move to different row OR outside the current undo token 
//    invokes undo record.
// d) A single-character undo is not worth saving. What is the minimum?
// e) A cut/paste is the obvious definition of undo.
// f) If x single characters pushed, they could be combined.
// e) If contiguous add/subtract, then
// f) we need bracketing IP values for creating an undo record.
//    Then the beginning IP becomes the anchor for a cut recent
// Track text and cursor offsets.
// Track auto save, and source update timestamps.
enum undoTYPE : short
{
   undoADD = ZERO,
   undoDEL,
} ;
short UNDO_MAX = 15 ;
class EditManager
{
   public:
   char*    undoData[UNDO_MAX] ;          // pointer to undo data
   WinPos    undoPos[UNDO_MAX] ;          // row/column of buffer for undo data
   undoTYPE undoType[UNDO_MAX] ;          // type of operation to undo
   int      unindx ;                      // LIFO index
   time_t   wkSave ;                      // time of most recent working-copy update
   time_t   srcSave ;                     // time of most recent source update
} ;

// From: https://www.geeksforgeeks.org/projects/implement-undo-and-redo-features-of-a-text-editor/
#include <iostream>
#include <stack>
#include <string>
using namespace std;

string doc = "";
stack<char> redost;
// append operation
void append(char X) {
    doc.push_back(X);

    // clear redo stack on new write
    while (!redost.empty()) redost.pop();
}

// undo operation
void undo() {
    if (!doc.empty()) {
        char last = doc.back();
        doc.pop_back();
        redost.push(last);
    }
}

// redo operation
void redo() {
    if (!redost.empty()) {
        char ch = redost.top();
        redost.pop();
        doc.push_back(ch);
    }
}

// read operation
string read() {
   return doc;
}

int main() {
    
    append('A');
    append('B');
    append('C');
    undo();
    cout<<read()<<" ";
    redo();
    cout<<read()<<" ";

    return 0;
}
#endif   // REFERENCE - Undo/Redo

//*************************
//*        Sandbox        *
//*************************
//********************************************************************************
//* For Development Only:                                                        *
//* ---------------------                                                        *
//* Test code constructs and report intermediate data.                           *
//*                                                                              *
//*                                                                              *
//* Input  : op : (optional, zero by default) operation number                   *
//*               0-3 == primary ad-hoc sandbox tests                            *
//*               4-99 == tests which are retained for some indefinite time      *
//*                                                                              *
//* Returns: depends on operation performed                                      *
//*          A) By default the operation number 'op' is returned                 *
//*             a) Values <= 3 indicate that caller should continue to           *
//*                            mainline code. These are the ad-hoc test numbers. *
//*                            Other tests can manually adjust the return value  *
//*                            (set to -1) to override the default action.       *
//*             b) Values >  3 indicate that caller should immediately exit      *
//*                            the application on return.                        *
//********************************************************************************

short AcEdit::Sandbox ( short op )
{
   #if DEBUG_ACE != 0
   gString gs ;
   short status = op ;

   if ( op < ZERO ) { return status ; }   // no option from command line

   //*--------------------------------------------------*
   //* Main Sandbox is invoked with default 'op' value. *
   //*--------------------------------------------------*
   if ( op == ZERO )
   {
      gs = "Play In the Sandbox." ;
      this->updateGutter ( false, gs.gstr(), true, 0 ) ;
   }

   else if ( op == 1 )
   {
   }

   else if ( op == 2 )
   {
   }

   else if ( op == 3 )
   {
   }

   #if DEBUG_EXT_TESTS != 0
   else if ( op == 11 )
   {
      //* Exercise the updateFocus() and updateAuxData() methods. *
      //* The fiTEXTfw field should be visible initially.         *
      //* Open a new empty file. This initializes the FileMan     *
      //* object for the field with focus, which in turn          *
      //* initializes the FileMan object(s) for subsequent fields.*
      this->newFile2Edit () ;
      this->win->nsleep ( 10 ) ;

      //* Fill fiTEXTfw EXACTLY with text and update the display. *
      gString gsFill ; short r = ZERO ;
      gs.clear() ;
      gs.padCols( (this->wInfo.skf->fld[fiTEXTfw].wid - 1), L'x' ) ;
      gs.append( L".\n" ) ;
      do
      { gsFill.append( gs.gstr() ) ;
      } while ( ++r < this->wInfo.skf->fld[fiTEXTfw].hgt ) ;
      gsFill.replace( L"x.", L"=.", (gsFill.gschars() - 4) ) ;
      this->win->SetFieldText ( fiTEXTfw, gsFill ) ;
      this->updateAuxData () ;
      this->win->nsleep ( 25 ) ;

      //* Move focus to Dual-Vertical Mode: fiTEXTvl  *
      //* Initialize the data and update the display. *
      this->updateFocus ( fiFIELDS, wcsALT_3 ) ;
      this->win->nsleep ( 10 ) ;

      //* Clear both visible fields *
      gs.clear() ; gsFill.clear() ; r = ZERO ;
      this->win->SetFieldText ( fiTEXTvr, gs ) ;
      this->win->SetFieldText ( fiTEXTvl, gs ) ;
      this->updateAuxData () ;
      this->win->nsleep ( 5 ) ;

      //* Fill each visible field EXACTLY and update the display. *
      gs.padCols( (this->wInfo.skf->fld[fiTEXTvl].wid - 1), L'y' ) ;
      gs.append( L".\n" ) ;
      do
      { gsFill.append( gs.gstr() ) ;
      } while ( ++r < this->wInfo.skf->fld[fiTEXTvl].hgt ) ;
      gsFill.replace( L"y.", L"=.", (gsFill.gschars() - 4) ) ;
      this->win->SetFieldText ( fiTEXTvl, gsFill ) ;
      this->updateAuxData () ;
      this->win->nsleep ( 10 ) ;
      
      gs.clear() ; gsFill.clear() ; r = ZERO ;
      this->updateFocus ( fiFIELDS, wcsAPGDN ) ;
      gs.padCols( (this->wInfo.skf->fld[fiTEXTvr].wid - 1), L'z' ) ;
      gs.append( L".\n" ) ;
      do
      { gsFill.append( gs.gstr() ) ;
      } while ( ++r < this->wInfo.skf->fld[fiTEXTvr].hgt ) ;
      gsFill.replace( L"z.", L"=.", (gsFill.gschars() - 4) ) ;
      this->win->SetFieldText ( fiTEXTvr, gsFill ) ;
      this->updateAuxData () ;
      this->win->nsleep ( 15 ) ;

      //* Move focus to Dual-Horizontal Mode:fiTEXThu *
      this->updateFocus ( fiFIELDS, wcsALT_2 ) ;
      this->win->nsleep ( 5 ) ;

      //* Clear both visible fields *
      gs.clear() ; gsFill.clear() ; r = ZERO ;
      this->win->SetFieldText ( fiTEXThl, gs ) ;
      this->win->SetFieldText ( fiTEXThu, gs ) ;
      this->updateAuxData () ;
      this->win->nsleep ( 5 ) ;

      //* Initialize text for Dual-Horizontal fields *
      gs.padCols( (this->wInfo.skf->fld[fiTEXThu].wid - 1), L'a' ) ;
      gs.append( L".\n" ) ;
      do
      { gsFill.append( gs.gstr() ) ;
      } while ( ++r < this->wInfo.skf->fld[fiTEXThu].hgt ) ;
      gsFill.replace( L"a.", L"=.", (gsFill.gschars() - 4) ) ;
      this->win->SetFieldText ( fiTEXThu, gsFill ) ;
      this->updateAuxData () ;
      this->win->nsleep ( 15 ) ;

      gs.clear() ; gsFill.clear() ; r = ZERO ;
      this->updateFocus ( fiFIELDS, wcsAPGUP ) ;
      gs.padCols( (this->wInfo.skf->fld[fiTEXThl].wid - 1), L'b' ) ;
      gs.append( L".\n" ) ;
      do
      { gsFill.append( gs.gstr() ) ;
      } while ( ++r < this->wInfo.skf->fld[fiTEXThl].hgt ) ;
      gsFill.replace( L"b.", L"=.", (gsFill.gschars() - 4) ) ;
      this->win->SetFieldText ( fiTEXThl, gsFill ) ;
      this->updateAuxData () ;
      this->win->nsleep ( 15 ) ;

      //* Return focus to Full-Window Mode *
      gs.clear() ; gsFill.clear() ; r = ZERO ;
      this->updateFocus ( fiTEXTfw ) ;
      this->win->nsleep ( 5 ) ;
      do
      {
         gs.append( "abcdefghijklmnopqrstuvwxyz0123456789" ) ;
      }
      while ( (gs.gscols()) < this->wInfo.skf->fld[fiTEXTfw].wid) ;
      gs.limitCols( this->wInfo.skf->fld[fiTEXTfw].wid - 1 ) ;
      gs.append( L".\n" ) ;
      do
      { gsFill.append( gs.gstr() ) ;
      } while ( ++r < this->wInfo.skf->fld[fiTEXTfw].hgt ) ;
      gs = L" - Bye-bye!" ;
      gsFill.limitCols( (gsFill.gscols()) - (gs.gscols()) ) ;
      gsFill.append( gs.gstr() ) ;
      this->win->SetFieldText ( fiTEXTfw, gsFill ) ;
      this->updateAuxData () ;

      // If file(s) are open for edit, this will allow for an orderly shutdown.
      this->win->KeycodePush ( wcsCTL_4 ) ;
      status = -1 ;
   }  // (op == 11)

   else if ( op == 12 )
   {
      // Note that if no files were specified, this->fman will contain only 
      // default values (or garbage). Previously-edited filenames from the 
      // config file will be loaded.

      //* Test formatting of timestamps *
      WinPos wp( 1, 5 ) ;
      gString gsStamp ;
      localTime lt = this->fman[0].tnf.modTime ;

      //* If default values (no filenames listed), substitute today's date. *
      if ( lt.month == ZERO )
         this->GetLocalTime ( lt ) ;

      this->focus = fldIndex(this->win->SetInputFocus ( fiTEXTfw )) ;

      // Friendly - no seconds, no dow
      bool canon = false, secs = false, dow = false ;
      this->formatTimestamp ( lt, gsStamp, canon, secs, dow ) ;
      gs.compose( "\n  %hhd-%hhd-%hhd : '%S'\n", &canon, &secs, &dow, gsStamp.gstr() ) ;

      // Friendly - no seconds, with dow
      canon = false ; secs = false ; dow = true ;
      this->formatTimestamp ( lt, gsStamp, canon, secs, dow ) ;
      gs.append( "  %hhd-%hhd-%hhd : '%S'\n", &canon, &secs, &dow, gsStamp.gstr() ) ;

      // Friendly - with seconds, with dow
      canon = false ; secs = true ; dow = true ;
      this->formatTimestamp ( lt, gsStamp, canon, secs, dow ) ;
      gs.append( "  %hhd-%hhd-%hhd : '%S'\n", &canon, &secs, &dow, gsStamp.gstr() ) ;

      // Friendly - with seconds, with dow
      canon = false ; secs = true ; dow = true ;
      this->formatTimestamp ( lt, gsStamp, canon, secs, dow ) ;
      gs.append( "  %hhd-%hhd-%hhd : '%S'\n\n", &canon, &secs, &dow, gsStamp.gstr() ) ;

      // Canonical - no seconds, no dow
      canon = true ; secs = false ; dow = false ;
      this->formatTimestamp ( lt, gsStamp, canon, secs, dow ) ;
      gs.append( "  %hhd-%hhd-%hhd : '%S'\n", &canon, &secs, &dow, gsStamp.gstr() ) ;

      // Canonical - no seconds, with dow
      canon = true ; secs = false ; dow = true ;
      this->formatTimestamp ( lt, gsStamp, canon, secs, dow ) ;
      gs.append( "  %hhd-%hhd-%hhd : '%S'\n", &canon, &secs, &dow, gsStamp.gstr() ) ;

      // Canonical - with seconds, no dow
      canon = true ; secs = true ; dow = false ;
      this->formatTimestamp ( lt, gsStamp, canon, secs, dow ) ;
      gs.append( "  %hhd-%hhd-%hhd : '%S'\n", &canon, &secs, &dow, gsStamp.gstr() ) ;

      // Canonical - with seconds, with dow
      canon = true ; secs = true ; dow = true ;
      this->formatTimestamp ( lt, gsStamp, canon, secs, dow ) ;
      gs.append( "  %hhd-%hhd-%hhd : '%S'\n", &canon, &secs, &dow, gsStamp.gstr() ) ;

      this->win->SetFieldText ( fiTEXTfw, gs ) ;

      // If file(s) are open for edit, this will allow for an orderly shutdown.
      this->win->KeycodePush ( wcsCTL_4 ) ;
      status = -1 ;
   }  // (op == 12)

   else if ( op == 13 )
   {
      //* Report Terminal Configuration
      this->dumpTermInfo ( 2, 99 ) ;
      this->dumpTermInfo ( 2, 99, &ofsdbg ) ;

      // If file(s) are open for edit, this will allow for an orderly shutdown.
      this->win->KeycodePush ( wcsCTL_4 ) ;
      status = -1 ;
   }  // (op == 13)
   
   //* Test text-editor field access via updateFocus(). *
   //* Adjusts field flow option for each text field.   *
   else if ( op == 14 )
   {
      //* Verify that the field with focus is displaying "LoremIpsum.txt". *
      //* If not, open it for edit. Note that this is kludgy, sorry.       *
      gs = this->fman[AREC].srcSpec ;
      if ( (gs.find( L"LoremIpsum.txt" )) < ZERO )
      {
         gString gserr ;
         short fmTrg = this->fmcnt ;
         gs = "./1_TestData/LoremIpsum.txt" ;
         this->gclaFilespec ( gs, gserr ) ;
         this->openFile4Edit ( fmTrg ) ;
         this->loadField ( fmTrg, this->focus ) ;
      }

      //* Remove all newline characters from the text and *
      //* reinitialize all target fields in sequence.     *
      gString gsStrip ;
      this->win->GetFieldText ( fiTEXTfw, gsStrip ) ;
      gsStrip.erase( NEWLINE, ZERO, false, true ) ;
      this->win->SetFieldText ( fiTEXTfw, gsStrip ) ;
      this->updateAuxData () ;

      //* Enable Footnote messages *
      this->win->EnableFootnotes ( true ) ;
      //* Set a message in the gutter. *
      this->updateGutter ( false, L" Press Any Key To Continue " ) ;

      gs.compose( "Sandbox 15: fiTEXTfw floShift  PAK..." ) ;
      gs.padCols( 38 ) ;
      this->win->Footnote ( gs, ZERO ) ;
      this->win->acPause () ;

      //* Reformat fiTEXTfw field text. *
      this->win->SetFieldFlow ( fiTEXTfw, floChar, true ) ;
      gs.compose( "Sandbox 15: fiTEXTfw floChar  PAK..." ) ;
      gs.padCols( 38 ) ;
      this->win->Footnote ( gs, ZERO ) ;
      this->win->acPause () ;

      //* Reformat fiTEXTfw field text to floToken *
      this->win->ReformatFieldText ( fiTEXTfw, floToken ) ;
      this->updateAuxData () ;
      gs.compose( "Sandbox 15: fiTEXTfw floToken  PAK..." ) ;
      gs.padCols( 38 ) ;
      this->win->Footnote ( gs, ZERO ) ;
      this->win->acPause () ;

      //* Enable Horizontal pair of edit fields: fiTEXThu *
      this->updateFocus ( fiFIELDS, wcsALT_2 ) ;

      //* Initialize data for Horizontal-Pair Mode *
      this->win->GetFieldText ( fiTEXThl, gsStrip ) ;
      gsStrip.erase( NEWLINE, ZERO, false, true ) ;
      this->win->SetFieldFlow ( fiTEXThl, floShift ) ;
      this->win->SetFieldText ( fiTEXThl, gsStrip ) ;
      this->win->SetFieldFlow ( fiTEXThu, floShift ) ;
      this->win->SetFieldText ( fiTEXThu, gsStrip ) ;
      this->updateAuxData () ;
      gs.compose( "Sandbox 15: fiTEXThu floShift  PAK..." ) ;
      gs.padCols( 38 ) ;
      this->win->Footnote ( gs, ZERO ) ;
      this->win->acPause () ;

      //* Reformat fiTEXThu field text. *
      this->win->SetFieldFlow ( fiTEXThu, floChar, true ) ;
      gs.compose( "Sandbox 15: fiTEXThu floChar  PAK..." ) ;
      gs.padCols( 38 ) ;
      this->win->Footnote ( gs, ZERO ) ;
      this->win->acPause () ;

      //* Reformat fiTEXThu field text to floToken *
      this->win->ReformatFieldText ( fiTEXThu, floToken ) ;
      gs.compose( "Sandbox 15: fiTEXThu floToken  PAK..." ) ;
      gs.padCols( 38 ) ;
      this->win->Footnote ( gs, ZERO ) ;
      this->win->acPause () ;

      //* Move focus to fiTEXThl *
      this->updateFocus ( fiFIELDS, wcsAPGDN ) ;
      gs.compose( "Sandbox 15: fiTEXThl floShift  PAK..." ) ;
      gs.padCols( 38 ) ;
      this->win->Footnote ( gs, ZERO ) ;
      this->win->acPause () ;

      //* Reformat fiTEXThl field text. *
      this->win->SetFieldFlow ( fiTEXThl, floChar, true ) ;
      gs.compose( "Sandbox 15: fiTEXThl floChar  PAK..." ) ;
      gs.padCols( 38 ) ;
      this->win->Footnote ( gs, ZERO ) ;
      this->win->acPause () ;

      //* Reformat fiTEXThl field text to floToken *
      this->win->ReformatFieldText ( fiTEXThl, floToken ) ;
      gs.compose( "Sandbox 15: fiTEXThl floToken  PAK..." ) ;
      gs.padCols( 38 ) ;
      this->win->Footnote ( gs, ZERO ) ;
      this->win->acPause () ;

      //* Enable Vertical pair of edit fields: fiTEXTvl *
      this->updateFocus ( fiFIELDS, wcsALT_3 ) ;
      this->win->GetFieldText ( fiTEXTvr, gsStrip ) ;
      gsStrip.erase( NEWLINE, ZERO, false, true ) ;
      this->win->SetFieldFlow ( fiTEXTvr, floShift ) ;
      this->win->SetFieldText ( fiTEXTvr, gsStrip ) ;
      this->win->SetFieldFlow ( fiTEXTvl, floShift ) ;
      this->win->SetFieldText ( fiTEXTvl, gsStrip ) ;
      this->updateAuxData () ;
      gs.compose( "Sandbox 15: fiTEXTvl floShift  PAK..." ) ;
      gs.padCols( 38 ) ;
      this->win->Footnote ( gs, ZERO ) ;
      this->win->acPause () ;

      //* Reformat fiTEXTvl field text. *
      this->win->SetFieldFlow ( fiTEXTvl, floChar, true ) ;
      gs.compose( "Sandbox 15: fiTEXTvl floChar  PAK..." ) ;
      gs.padCols( 38 ) ;
      this->win->Footnote ( gs, ZERO ) ;
      this->win->acPause () ;

      //* Reformat fiTEXTvl field text. *
      this->win->ReformatFieldText ( fiTEXTvl, floToken ) ;
      gs.compose( "Sandbox 15: fiTEXTvl floToken  PAK..." ) ;
      gs.padCols( 38 ) ;
      this->win->Footnote ( gs, ZERO ) ;
      this->win->acPause () ;

      //* Move focus to fiTEXTvr *
      this->updateFocus ( fiFIELDS, wcsAPGDN ) ;
      gs.compose( "Sandbox 15: fiTEXTvr floShift  PAK..." ) ;
      gs.padCols( 38 ) ;
      this->win->Footnote ( gs, ZERO ) ;
      this->win->acPause () ;

      //* Reformat fiTEXTvr field text. *
      this->win->SetFieldFlow ( fiTEXTvr, floChar, true ) ;
      gs.compose( "Sandbox 15: fiTEXTvr floChar  PAK..." ) ;
      gs.padCols( 38 ) ;
      this->win->Footnote ( gs, ZERO ) ;
      this->win->acPause () ;

      //* Reformat fiTEXTvr field text. *
      this->win->ReformatFieldText ( fiTEXTvr, floToken ) ;
      gs.compose( "Sandbox 15: fiTEXTvr floToken  PAK..." ) ;
      gs.padCols( 38 ) ;
      this->win->Footnote ( gs, ZERO ) ;
      this->win->acPause () ;
      this->win->Footnote ( "", ZERO ) ;
      this->win->Footnote ( "Sandbox: Bye-bye! ", ZERO ) ;

      // Programmer's Note: The working copy of the source file will be orphaned 
      // unless we reset the return value. We also push the exit code into the 
      // input stream so the application will do an orderly shutdown on return.
      this->win->KeycodePush ( wcsCTL_4 ) ;
      status = -1 ;
   }  // (test 14)

   else if ( op == 15 )
   {
      this->win->ClearWin () ;

      //* Report the locale settings according to ACWin/AnsiCmd *
      WinPos wp( 1, 5 ) ;
      this->win->WinInfo ( this->wInfo ) ;
      gString gsLoc ( this->wInfo.tcfg.ioLocale->name().c_str() ) ;
      int indx = ZERO ;
      while ( (indx = gsLoc.after( L';', indx )) > ZERO )
         gsLoc.insert( NEWLINE, indx ) ;
      gs.compose( "    ACWin Locale Report:     \n"
                  "-----------------------------\n"
                  "wInfo.ioLocale.tcfg.ioLocale:\n"
                  "%s\n"
                  "   Press Any Key To Exit...", gsLoc.ustr() ) ;
      this->win->Write ( wp, gs ) ;
      this->win->acPause() ;
      this->win->RefreshWin () ;

      // If file(s) are open for edit, this will allow for an orderly shutdown.
      this->win->KeycodePush ( wcsCTL_4 ) ;
      status = -1 ;
   }  // (test 15)

   else if ( op == 16 )
   {
      //* Report the timestamp for the file referenced by    *
      //* the first FileMan object: this->fman[0].           *
      //* Reports both canonical and epoch timestamps.       *
      //* Invoke: ./ace TargetFilename --test=16             *
      //* (This also tests the flashDialog() border option.) *
      const FileMan* fmPtr = &this->fman[0] ;
      gString tStamp ;
      this->formatTimestamp ( fmPtr->tnf.modTime, tStamp ) ;
      gs.compose ( "Filename   : %s\n"
                   "Mod Time   : %s\n"
                   "Epoch Time : 0x%08X\n"
                   " Press any key to exit...",
                   fmPtr->tnf.fName, tStamp.ustr(), &fmPtr->tnf.modTime.epoch ) ;
      this->flashDialog ( gs, 60, this->dlgAttr, true ) ;

      // If file(s) are open for edit, this will allow for an orderly shutdown.
      this->win->KeycodePush ( wcsCTL_4 ) ;
      status = -1 ;
   }  // (test 16)

   //***************************
   //** Display the Test Menu **
   //***************************
   else if ( op > 16 )
   {
      WinPos wp( 1, 5 ) ;
      const char* testMenu =
         "00, 01, 02. 03 - Temporary Tests\n"
         "11 - Test updateFocus/updateText methods\n"
         "12 - Timestamp Formatting\n"
         "13 - Terminal Configuration\n"
         "14 - skField Text Formatting Options\n"
         "15 - Report Locale Settings\n"
         "16 - Report File Timestamp\n"
         "99 - This Menu\n"
         "     Press any key to exit..." ;
      gs = "Development Test Menu" ;
      gs.padCols( 60 ) ;
      gs.append( NEWLINE ) ;

      this->focus = fldIndex(this->win->SetInputFocus ( fiTEXTfw )) ;
      this->win->ClearField ( fiTEXTfw ) ;
      this->win->acSetEchoOption ( noEcho ) ;
      acAttr acaVal = acAttr(this->wInfo.skf->fld[fiTEXTfw].aca.acaVal | acaUNDERLINE) ;
      wp = this->win->Write ( wp, gs, acaVal ) ;
      gs = testMenu ;
      wp = this->win->Write ( wp, gs, this->wInfo.skf->fld[fiTEXTfw].aca.acaVal ) ;
      this->win->acPause () ;
   }

   else     // invalid test option
   { this->win->acBeep ( 2, 2 ) ; }
   #endif   // DEBUG_EXT_TESTS

   return status ;
#else    // Sandbox Disabled
   return -1 ;
#endif   // DEBUG_ACE
}  //* End Sandbox() *

//*************************
//*                       *
//*************************
//********************************************************************************
//*                                                                              *
//*                                                                              *
//*                                                                              *
//* Input  :                                                                     *
//*                                                                              *
//* Returns:                                                                     *
//********************************************************************************

