/**************************************************************************************

   zfuncs.cc   collection of Linux and GDK/GTK utility functions

   Copyright 2007-2026 Michael Cornelison
   source code URL: https://kornelix.net
   contact: mkornelix@gmail.com

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version. See https://www.gnu.org/licenses

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
   See the GNU General Public License for more details.

***************************************************************************************/

#include "zfuncs.h"

#define RELEASE 26.4             //  follows Fotocx release versions                   //  26.4

/**************************************************************************************

   System Utility Functions
   ------------------------
   zmalloc  zfree          wrappers to add add checks and statistics
   zmalloc_test            check if planned allocation is available
   realmemory              get real free memory (incl. file cache) in MB units
   availmemory             get available memory (incl. swap file) in MB units
   zstrdup                 duplicate string with added space
   zstrcpy                 replace string with new string + added cc
   zmalloc_report          memory usage by tag > popup report
   zmalloc_growth          memory growth by tag > popup report
   xmessage                output a popup message not requiring GTK
   zexit                   exit process and kill subprocesses, opt. popup message
   zbacktrace              callable backtrace dump
   zappcrash               abort with traceback dump to desktop file
   catch_signals           trap segfault, crash with zappcrash()
   trace                   implements TRACE macro
   tracedump               dump trace data
   combine_argvs           catenate argv[ii] elements from Nth to last
   get_seconds             get time in real seconds with millisecond resolution
   print_seconds           output seconds since last call
   start_timer             start a named timer
   get_timer               get elapsed time with millisecond resolution
   CPUtime                 get CPU process time for current process, seconds
   memused                 get real memory used by current process, MB
   compact_time            convert time_t type to yyyymmddhhmmss format
   pretty_datetime         convert time_t type to yyyy:mm:dd hh:mm:ss format
   secs_datetime           seconds since 1970 (double) to yymmddhhmmss (int[6])
   datetime_secs           yymmddhhmmss (int[6]) to seconds since 1970 (double)
   parseprocfile           read and parse /proc records formatted "parmname value"
   parseprocrec            read and parse /proc records with fixed series of values
   coretemp                get current processor core temperature
   disktemp                get temperature for given disk drive
   zsleep                  sleep for any amount of time (e.g. 0.1 seconds)
   zloop                   loop for any amount of time
   spinlock                simple method to prevent parallel execution of code block
   global_lock             lock/unlock a global resource (all processes/threads)
   resource_lock           lock/unlock a resource within a process and threads
   zget_locked, etc.       safely get/put/increment parameters from multiple threads
   start_detached_thread   start a detached thread
   start_Jthread           start a joinable thread
   wait_Jthread            wait for thread and join
   synch_threads           make threads pause and resume together
   main_thread             return 1 if main() thread, else 0
   zshell                  run shell command with options log, ack
   kill_procname           kill process matching wildcard name
   signalProc              pause, resume, or kill a child process
   fgets_trim              fgets() with trim of trailing \r \n and optionally blanks
   fgets_pend              fgets() non-blocking wrapper
   fgetsB                  fgets() equivalent that reads file in huge blocks
   samefolder              test if two files/folders have the same folder path
   parsefile               parse filespec into folder, file, extension
   renamez                 like rename() but works across file systems
   check_create_dir        check if folder exists, ask to create if not
   cp_copy                 same, using shell "cp -f -p"
   diskspace               get available space on disk of given file, MB
   get_file_extension      use 'file' command to find and append correct extension
   zreaddir                return all files in a folder, sorted
   zreadfile               read file, return array of records
   zwritefile              write array of records to file
   zreadfile_free          free zreadfile() memory

   String Functions
   ----------------
   strmatch                true if strings match, null strings match only null strings
   strmatchN               true if leading N characters match, null strings as above
   strmatchcase            true if strings match, except for case, null strings as above
   strmatchcaseN           true if leading N characters match, null strings as above
   substringR              get delimited substrings from input string
   substring               same, not thread-safe, no zfree() needed
   get_substrings          get all substrings from a delimited input string
   strHash                 hash string to random number in a range
   strncpy0                strncpy() with insured null delimiter
   strnPad                 add blank padding to specified length
   strTrim                 remove trailing blanks
   strTrim2                remove leading and trailing blanks
   strCompress             remove embedded blanks
   strncatv                catenate multiple strings with length limit
   strmatchV               compare 1 string to N strings
   strToUpper              convert string to upper case (ascii only)
   strToLower              convert string to lower case (ascii only)
   repl_1str               replace a substring within a string
   repl_Nstrs              replace multiple substrings within string
   breakup_text            insert newline chars to limit text line lengths
   strncpyx                convert string to hex format
   StripZeros              remove trailing zeros (1.23000E+8 >> 1.23E+8)
   blank_null              test string for null pointer, zero length, and all blanks
   clean_escapes           replace 2-character escapes ("\n") with the escaped characters
   UTF8 functions          deal with UTF8 multibyte character strings
   zsed                    substitute multiple strings in a file
   zstrstr                 zstrstr() and zcasestrstr() work like strstr() and strcasestr()
   zstrcasestr               but the string "" does NOT match with any string
   zstrcpy                 strcpy with overlap allowed
   zstrncpy                strncpy with overlap allowed
   zstrcmp                 like strcmp, but \n as well as null ends the compare
   zstrcmp2                works like strcmp(), but using ch *args instead of cch *
   zstrcasecmp             works like strcasecmp(), but using ch *args instead of cch *
   zescape_quotes          escape quote marks (",') for strings used in shell commands

   Number Conversion and Formatting
   --------------------------------
   convSI            string to integer with optional limits check
   convSD            string to double with optional limits check
   convSF            string to float with optional limits check
   convIS            integer to string with returned length
   convDS            double to string with specified digits of precision
   atofz             atof() accepting both '.' and ',' decimal points
   formatKBMB        format a byte count with specified precision and B/KB/MB/GB units
   Ftruncate         reduce significant bits in a float number mantissa

   Wildcard Functions
   ------------------
   MatchWild         match string to wildcard string (multiple * and ?)
   MatchWildCase     works like MatchWild() but ignores case
   SearchWild        wildcard file search (multiple * and ? in path or file name)
   SearchWildCase    works like SearchWild() but ignores case in file name
   zfind             find files matching a pattern. uses glob()

   Search and Sort Functions
   -------------------------
   bsearch           binary search of sorted list
   HeapSort          sort list of integer / float / double / records / pointers to records
   MemSort           sort records with multiple keys (data types and sequence)
   zmember           test if a value is a member of a set of values
   HashTab           hash table: add, delete, find, step through

   Misc Functions
   --------------
   zlist functions         list processing functions - array of string pointers
   lrandz                  int64 random numbers
   drandz                  double random numbers
   random numbers          int and double random numbers with improved distributions
   spline1/2               cubic spline curve fitting function
   Qtext                   FIFO queue for text strings, dual thread access
   variance                compute variance for a list of numbers

   Application Admin Functions
   ---------------------------
   zinitapp                initialize application folder and data files
   get_zprefix             /usr or /home/<user>
   get_zhomedir            /home/<user>/.appname or custom location
   get_zdatadir            app data files location
   get_zdocdir             app documentation files location
   get_zimagedir           app image files location
   zabout                  popup application 'about' information
   zsetfont                set new application font
   widget_font_metrics     get font width and height for given widget
   get_zfilespec           get filespec for README, changelog, userguide, parameters ...
   showz_logfile           display application log file
   showz_textfile          show application text file (README, changelog, etc.)
   showz_docfile           show a document file topic and associated image
   audit_docfile           audit docfile for missing topics and bad links
   translate               create a translation file for the fotocx GUI                //  26.0
   TX_init                 initialize translations at startup time
   TX                      translate a text string

   GTK Utility Functions
   ---------------------
   zmainloop               do main loop to process menu events, etc.
   zmainsleep              loop zmainloop and zsleep for designated time
   draw_context_create     get cairo drawing context for GDK window
   txwidget                text report, navigation, line editing
   create_menubar          menubar functions
   create_toolbar          toolbar functions
   create_stbar            statusbar functions
   create_popmenu          create popup menu with response functions
   popup_menu              display/execute popup menu
   popup_choose            popup to choose from zlist or text file
   Vmenu                   vertical menu/toolbar in vertical packing box

   splcurve_init           set up a spline curve drawing area
   splcurve_adjust         mouse event function to manipulate curve nodes
   splcurve_addnode        add an anchor point to a curve
   splcurve_resize         resize drawing area if too small
   splcurve_draw           draw curve through nodes
   splcurve_generate       generate x/y table of values from curve
   splcurve_yval           get curve y-value for given x-value
   splcurve_load           load curve data from a saved file
   splcurve_save           save curve data to a file

   zdialog_new                create new zdialog
   zdialog_set_title          change a zdialog title
   zdialog_set_modal          set a zdialog to be modal
   zdialog_set_decorated      set a zdialog to be decorated or not
   zdialog_present            present a zdialog (visible and on top)
   zdialog_set_focus          set focus on zdialog window or window + widget
   zdialog_add_widget         add widget to existing zdialog
   zdialog_valid              return 1/0 if zdialog is valid/invalid
   zdialog_find_widget        return widget from zdialog and widget name
   zdialog_gtkwidget          get GTK widget from zdialog and widget name
   zdialog_set_image          set image widget from GDK pixbuf
   zdialog_add_ttip           add a popup tool tip to a zdialog widget
   zdialog_resize             resize zdialog greater than initial size
   zdialog_put_data           put data into a zdialog widget of any type
   zdialog_get_data           get data from a zialog widget of any type
   zdialog_set_limits         set new limits for numeric data entry widget
   zdialog_get_limits         get limits for numeric data entry widget
   zdialog_rescale            expand the scale around a neutral value
   zdialog_run                run the zdialog and send events to event function
   zdialog_widget_event       respond to zdialog widget events
   zdialog_focus_in_event     response handler for "focus-in-event" signal
   zdialog_activate_event     response handler for "activate" signal ('enter' key)
   zdialog_KB_press           respond to zdialog keyboard inputs
   zdialog_zspin_event        response function for "zspin" widget
   zdialog_copyfunc           copy widget data to clipboard
   zdialog_pastefunc          copy clipboard to widget with KB focus
   zdialog_delete_event       process zdialog delete event ([x] button)
   zdialog_send_event         send an event to an active zdialog
   zdialog_send_response      complete a zdialog and assign status
   zdialog_show               show or hide a zdialog window
   zdialog_destroy            destroy a zdialog (data remains available)
   zdialog_free               free zdialog memory (data is gone)
   zdialog_wait               wait for zdialog completion, get status
   zdialog_goto               put cursor at named widget
   zdialog_set_cursor         set zdialog cursor (e.g. busy)
   zdialog_stuff              stuff data into zdialog widget
   zdialog_labelfont          set label text with font
   zdialog_fetch              fetch data from zdialog widget
   zdialog_combo_clear        clear combo box entries
   zdialog_combo_popup        open combo box pick list
   zdialog_load_widgets       load zdialog widgets and curves from a file
   zdialog_save_widgets       save zdialog widgets and curves to a file
   zdialog_load_prev_widgets  load last-used widgets (for [prev] buttons)
   zdialog_save_last_widgets  save last-used widgets (for [prev] buttons)

   zdialog_geometry           load/save zdialog positions at app start/exit
   zdialog_set_position       set zdialog position: null mouse desktop parent save nn/nn
   zdialog_save_position      remember zdialog position relative to parent
   zdialog_save_inputs        save zdialog inputs when zdialog completed
   zdialog_load_inputs        retrieve prior zdialog input fields

   zdialog_text               popup zdialog to get N lines of text input from user
   zdialog_text1              popup zdialog to get one line of text input from user
   zdialog_password           popup zdialog to get a password entry (invisible)
   zdialog_choose             popup zdialog to show a message, select a button, return choice
   zdialog_edit_textfile      popup zdialog to edit a text file

   popup_report            popup window and scrolling text report
   popup_command           run a shell command with output in a popup window
   monitor_file            show a text file in a popup window, with real-time updates
   zmessageACK             popup message, printf format, wait for user ACK
   zmessageYN              popup message, printf format, wait for user Yes / No
   zmessage_post           popup message, printf format, show until killed
   poptext_screen          popup message at given absolute screen position
   poptext_mouse           popup message at current mouse position + offset
   poptext_widget          popup message at given widget position + offset
   poptext_killnow         kill popup message
   zgetfile                simplified file chooser zdialog
   zgetfolder              file chooser for folder, with create option
   print_image_file        zdialog to print an image file using GTK functions
   drag_drop_source        connect window as drag-drop source
   drag_drop_dest          connect window as drag-drop destination
   get_thumbnail           get thumbnail image for given image file
   zmakecursor             make a cursor from an image file (.png .jpg)
   gdk_pixbuf_stripalpha   remove an alpha channel from a pixbuf
   text_pixbuf             create pixbuf containing text
   move_pointer            move the mouse pointer within a widget/window
   window_to_mouse         move a GtkWindow to the mouse position

***************************************************************************************/

namespace zfuncs
{
   GdkDisplay     *display;                                                            //  workstation (KB, mouse, screen)
   GdkScreen      *screen;                                                             //  screen, N monitors
   GdkDevice      *mouse;                                                              //  pointer device
   GtkSettings    *gtksettings = 0;                                                    //  screen settings
   GtkWidget      *mainwin = 0;                                                        //  main window
   GtkTextView    *curr_textview_widget;                                               //  curr. GtkTextView widget
   ch          zcontact[] = "mkornelix@gmail.com";                                     //  author contact
   ch          *build_date_time = __DATE__ " " __TIME__;                               //  build date and time
   ch          *progexe = 0;                                                           //  executable image file
   int         Floglevel = 1;                                                          //  0/1/2 = errs/infos/dialog inputs
   int         Fescape = 0;                                                            //  zdialog, escape key pressed
   int         Fshutdown;                                                              //  flag, app shutdown underway           25.1
   int         monitor_ww, monitor_hh;                                                 //  monitor dimensions
   int         appfontsize = 10;                                                       //  application font size
   ch          *appfont = "sans 10";                                                   //  application font defaults
   ch          *appboldfont = "sans bold 10";
   ch          *appmonofont = "mono 10";
   ch          *appmonoboldfont = "mono bold 10";
   ch          zappname[40] = "undefined";                                             //  appname without version
   ch          zappvers[40] = "undefined";                                             //  appname-N.N
   ch          zprefix[200], zdatadir[200], zdocdir[200];                              //  app folders
   ch          zimagedir[200], zhomedir[200];
   ch          logfile[200];                                                           //  application log file
   pthread_t   tid_main = 0;                                                           //  main() thread ID
   int         vmenuclickposn;                                                         //  Vmenu image click posn. 0-100
   int         vmenuclickbutton;                                                       //  button: 1/2/3 = L/M/R mouse
   int         vmenustop;                                                              //  setupfunc() stop flag
   zdialog     *zdialog_list[zdialog_max];                                             //  active zdialog list
   int         zdialog_count = 0;                                                      //  total zdialogs (new - free)
   float       splcurve_minx = 5;                                                      //  min. anchor point dist, % scale
   ch          *zappcrash_context1 = 0, *zappcrash_context2 = 0;
}

using namespace zfuncs;


/**************************************************************************************
   system-level utility functions
***************************************************************************************/

/**************************************************************************************

   zmalloc() zfree() zstrdup()
   These are wrappers for malloc() and free() with extra diagnostics.
   zmalloc() crashes with a message to standard output if the allocation fails,
   hence the caller need not check. zmalloc() allocates memory for sentinels
   placed before and after the returned memory space, and zfree() checks the
   sentinels for validity and crashes with a message if they don't match.
   The optional tag argument is also copied into the extra allocated space for
   use by zmalloc_report(). zmalloc() clears the allocated memory to zeros.
   zmalloc_report() reports total allocated memory by tag to standard output.
   Allocation counts and bytes are listed for zmalloc() calls not yet matched
   by zfree() calls.

   The extra cpu time is about 4% (for the fotocx application).

***************************************************************************************/

#define  zmalloc_extra 36
int64    zmalloc_tot = 0;
int      zmalloc_lock = 0;

void * zmalloc(int64 cc, ch *tag)                                                      //  bytes, tag
{
   void  zmalloc_tabulate(ch *tag, int64 cc);

   double      memavail;
   static int  ftf = 1, memcheck = 1;
   double      mcc;

   while (! resource_lock(zmalloc_lock)) zsleep(0.001);

   ch          *OOMmessage = "                   \n"                                   //  big and obvious
                             "  ---------------  \n"
                             "   OUT OF MEMORY   \n"
                             "  ---------------  \n";
   if (ftf) {                                                                          //  first call
      ftf = 0;
      memavail = availmemory();
      if (! memavail) memcheck = 0;                                                    //  memory checking not possible, disable
   }

   if (cc <= 0) zappcrash("zmalloc: %lld bytes",cc);

   if (memcheck && cc > 1000) {                                                        //  large block
      mcc = cc;
      mcc = mcc / 1024 / 1024;                                                         //  requested memory, MB
      memavail = availmemory();                                                        //  avail. memory, MB
      if (memavail - mcc < 500) {
         printf("*** memory request for %.0f MB failed\n",mcc);
         zexit(1,OOMmessage);
         exit(-1);
      }
   }

   if (! tag) tag = "zmalloc notag";
   if (strlen(tag) > 19) zappcrash("zmalloc tag too big: %s",tag);                     //  26.0

   void *   maddr = malloc(cc + zmalloc_extra);           //  0             allocated memory with extra space
   int64    *pcc = (int64 *) maddr;                       //  0..8          caller byte count
   ch       *psen1 = (ch *) maddr + 8;                    //  8..11         sentinel "sen1"
   ch       *ptag = (ch *) maddr + 12;                    //  12..31        tag, < 20 chars.
   ch       *puser =  (ch *) maddr + 32;                  //  32..B+31      user data, B chars.
   ch       *psen2 =  (ch *) puser + cc;                  //  B+32..B+35    sentinel "sen2"

   if (! maddr) {
      zexit(1,OOMmessage);
      exit(-1);
   }

   *pcc = cc;
   strncpy(psen1,"sen1",4);                                                            //  set leading sentinel
   strncpy0(ptag,tag,20);                                                              //  set tag
   strncpy(psen2,"sen2",4);                                                            //  set following sentinel

   memset(puser,0,cc);                                                                 //  clear allocation (force memory commit)

   zmalloc_tot += cc;
   zmalloc_tabulate(ptag,cc);                                                          //  track usage by tag

   resource_unlock(zmalloc_lock);
   return puser;
}


//  free memory allocated by zmalloc(). checks for overflow.

void zfree(void *puser)
{
   void  zmalloc_tabulate(ch *tag, int64 cc);

   if (! puser) {                                                                      //  26.2
      printf("*** zfree() null address \n");
      zbacktrace();
      return;
   }

   void        *maddr = (ch *) puser - 32;
   int64       *pcc = (int64 *) maddr;
   ch          *psen1 = (ch *) maddr + 8;
   ch          *ptag = (ch *) maddr + 12;
   int64       cc = *pcc;
   ch          *psen2 = (ch *) puser + cc;

   while (! resource_lock(zmalloc_lock)) zsleep(0.001);

   if (strncmp("sen1",psen1,4) || strncmp("sen2",psen2,4))                             //  check sentinels
      zappcrash("zfree: sentinels clobbered");
   *psen1 = *psen2 = 0;                                                                //  destroy sentinels

   zmalloc_tot -= cc;
   zmalloc_tabulate(ptag,-cc);                                                         //  track usage by tag

   memset(maddr,-1,cc+32);                                                             //  clear to detect use-after-free        25.1
   free(maddr);                                                                        //  free memory

   resource_unlock(zmalloc_lock);
   return;
}


//  private function. track how much memory is in use, per tag.
//  real tag capacity is about 80% of nominal 'zmhtcap'

#define  zmhtcap 500
HashTab     *zmalloc_hashtab = 0;
int64       zmalloc_count[zmhtcap];
int64       zmalloc_bytes[zmhtcap];

void zmalloc_tabulate(ch *ptag, int64 cc)
{
   int      ii;

   if (! zmalloc_hashtab) {
      zmalloc_hashtab = new HashTab(20,zmhtcap);
      memset(zmalloc_count, 0, zmhtcap * sizeof(int64));
      memset(zmalloc_bytes, 0, zmhtcap * sizeof(int64));
   }

   ii = zmalloc_hashtab->Find(ptag);
   if (ii < 0) ii = zmalloc_hashtab->Add(ptag);
   if (ii < 0) zappcrash("zmalloc hash table full");

   zmalloc_bytes[ii] += cc;
   if (cc > 0) ++zmalloc_count[ii];
   else  --zmalloc_count[ii];

   return;
}


//  test if a given about of free memory is available
//  return 1 if OK, return 0 if NO.

int zmalloc_test(int64 cc)
{
   double      memavail, mb;

   mb = cc / 1024 / 1024;
   memavail = availmemory();                                                           //  avail. memory, MB
   memavail -= mb;
   if (memavail > 300) return 1;                                                       //  > 300 MB remaining, return OK
   printf("*** planned memory allocation of %.0f MB failed \n",mb);
   return 0;                                                                           //  not OK
}


//  get real memory in MB units
//  typical < 0.1 milliseconds

double realmemory()
{
   FILE     *fid;
   ch       buff[100], *pp;
   double   rmem = 0;

   fid = fopen("/proc/meminfo","r");
   if (! fid) return 0;

   while (true)
   {
      pp = fgets(buff,100,fid);
      if (! pp) break;
      if (strmatchN(pp,"MemAvailable:",13)) {                                          //  free + file cache
         rmem = atof(pp+13) / 1024;
         break;
      }
   }

   fclose(fid);
   return rmem;
}


//  get available memory in MB units (includes swap space)
//  typical < 0.1 milliseconds

double availmemory()
{
   FILE     *fid;
   ch       buff[100], *pp;
   double   avmem = 0;
   int      Ngot = 0;

   fid = fopen("/proc/meminfo","r");
   if (! fid) return 0;

   while (true)
   {
      pp = fgets(buff,100,fid);
      if (! pp) break;
      if (strmatchN(pp,"MemAvailable:",13)) {                                          //  free + file cache
         avmem += atof(pp+13) / 1024;
         if (++Ngot == 2) break;
      }

      if (strmatchN(pp,"SwapFree:",9)) {                                               //  swapfile free
         avmem += atof(pp+9) / 1024;
         if (++Ngot == 2) break;
      }
   }

   fclose(fid);
   return avmem;
}


//  duplicate string in allocated memory, with additional space at end

ch * zstrdup(ch *zstring, ch *tag, int addcc)
{
   if (! zstring) zstring = "null";                                                    //  26.0
   if (! tag) tag = "zstrdup notag";
   ch   *pp = (ch *) zmalloc(strlen(zstring) + 2 + addcc, tag);                        //  add additional chars, clear
   strcpy(pp,zstring);
   return pp;
}


//  replace zstring with string + added cc

int zstrcopy(ch *&zstring, ch *string, ch *tag, int addcc)
{
   if (! tag) tag = "zstrcopy notag";
   if (zstring == string) zstring = 0;                                                 //  if same string, make a duplicate
   if (zstring) zfree(zstring);
   int cc = strlen(string) + 2 + addcc;
   zstring = (ch *) zmalloc(cc,tag);
   strcpy(zstring,string);
   return cc;
}


/**************************************************************************************/

//  popup report of zmalloc() memory allocation per tag

void zmalloc_report(GtkWidget *parent)                                                 //  25.1
{
   int zmalloc_report_callback(GtkWidget *,int line, int posn, ch *input);

   static zdialog  *zdpop = 0;

   int         count, ii, first = 1;
   int64       cc;
   ch          tag[20];

   if (! zdpop || ! zdialog_valid(zdpop,"zmalloc"))                                    //  open new popup report
      zdpop = popup_report_open("zmalloc",parent,500,400,0,0,
                                 zmalloc_report_callback,"apply","clear","X",null);

   popup_report_write(zdpop,0,"zmalloc total memory: %lld \n",zmalloc_tot);

   while (true)
   {
      ii = zmalloc_hashtab->GetNext(first,tag);
      if (ii < 0) break;
      ii = zmalloc_hashtab->Find(tag);
      if (ii < 0) zappcrash("zmalloc hash table bug: %s",tag);
      cc = zmalloc_bytes[ii];
      count = zmalloc_count[ii];
      if (cc == 0) continue;
      popup_report_write(zdpop,0,"  %-20s  %8d  %lld \n",tag,count,cc);
   }

   popup_report_write(zdpop,0,"\n");
   popup_report_bottom(zdpop);

   return;
}

//  zmalloc_report() callback function for completion buttons

int zmalloc_report_callback(GtkWidget *txwidget, int line, int posn, ch *input)
{
   if (strmatch(input,"apply")) zmalloc_report(0);
   if (strmatch(input,"clear")) txwidget_clear(txwidget);
   return 1;
}


/**************************************************************************************/

//  report total memory allocated per tag - leak detection utility
//  report only tags with increased memory consumption since prior report

void zmalloc_growth(GtkWidget *parent)                                                 //  25.1
{
   int zmalloc_growth_callback(GtkWidget *,int line, int posn, ch *input);

   static zdialog    *zdpop = 0;

   int            count, ii, first = 1;
   int64          cc;
   ch             tag[20];
   static int     pne = 0;                                                             //  table of prior tag and cc values
   static ch      *ptag[1000];
   static int64   pcc[1000];

   if (! zdpop || ! zdialog_valid(zdpop,"zmalloc growth"))                             //  open new popup report
      zdpop = popup_report_open("zmalloc growth",parent,500,400,0,0,
                                 zmalloc_growth_callback,"apply","clear","X",null);

   popup_report_write(zdpop,0,"zmalloc total memory: %lld \n",zmalloc_tot);

   while (true)                                                                        //  loop all tags in table
   {
      ii = zmalloc_hashtab->GetNext(first,tag);
      if (ii < 0) break;
      ii = zmalloc_hashtab->Find(tag);
      if (ii < 0) zappcrash("zmalloc hash table bug: %s",tag);
      cc = zmalloc_bytes[ii];                                                          //  memory allocation for tag
      count = zmalloc_count[ii];                                                       //  zmalloc/zfree calls for tag
      if (cc == 0) continue;                                                           //  net memory = 0, skip

      for (ii = 0; ii < pne; ii++)                                                     //  find prior allocation for tag
         if (strmatch(tag,ptag[ii])) break;
      if (ii == pne) {
         ptag[ii] = strdup(tag);                                                       //  new tag, add to table
         pcc[ii] = cc;
         pne++;
      }

      if (cc <= pcc[ii]) continue;                                                     //  current <= prior allocation, skip

      popup_report_write(zdpop,0,"  %-20s  %8d  %lld \n",tag,count,cc);                //  report increased allocation
      pcc[ii] = cc;                                                                    //  new high-water allocation
   }

   popup_report_write(zdpop,0,"\n");
   popup_report_bottom(zdpop);

   return;
}


//  zmalloc_growth() callback function for completion buttons

int zmalloc_growth_callback(GtkWidget *txwidget, int line, int posn, ch *input)
{
   if (strmatch(input,"apply")) zmalloc_growth(0);
   if (strmatch(input,"clear")) txwidget_clear(txwidget);
   return 1;
}


/**************************************************************************************/

//  output a popup message not requiring GTK - callable from a thread.
//  application is stalled - "not responding" until [ok] button pressed.

void xmessage(ch *message)
{
   ch       command[400];
   ch       *font = "-*-bookman-*-r-*--0-140-0-0-p-*-*-*";                             //  big font                              25.1

   printf("%s \n",message);
   snprintf(command,400,"xmessage -fn %s -center \"%s\" ",font,message);
   int err = system(command);                                                          //  do not use zshell
   if (err) return;                                                                    //  avoid gcc warning
   return;
}


/**************************************************************************************/

//  Output a status or error message and kill all processes in the process group.
//  killpg(0,SIGKILL) kills all processes, including the caller.
//  if 'popup' true, popup an xmessage window with error message.

void zexit(int popup, ch *errmess, ...)
{
   va_list  arglist;
   ch       mess[1000];

   if (errmess) {                                                                      //  output error message
      va_start(arglist,errmess);
      vsnprintf(mess,1000,errmess,arglist);
      printf("zexit: %s\n",mess);
      if (popup) xmessage(mess);                                                       //  popup message
   }
   else printf("zexit\n");

   killpg(0,SIGKILL);                                                                  //  kill all processes in group
   sleep(1);                                                                           //  wait here to die
   exit(-1);
}


/**************************************************************************************/

//  produce a backtrace dump to stdout

void zbacktrace()
{
   int      nstack = 100;
   void     *stacklist[100];

   nstack = backtrace(stacklist,nstack);                                               //  get backtrace data
   if (nstack > 100) nstack = 100;
   backtrace_symbols_fd(stacklist,nstack,STDOUT_FILENO);                               //  backtrace records to STDOUT
   return;
}


/**************************************************************************************/

//  Write an error message and backtrace dump to a file and to a popup window.
//  Error message works like printf().
//  Depends on program addr2line() in binutils package.

void zappcrash(ch *format, ... )
{
   static int     crash = 0;
   struct utsname unbuff;
   va_list        arglist;
   FILE           *fid1, *fid2, *fid3;
   int            fd, ii, err, cc, nstack = 100;
   int            Flinenos = 1;
   void           *stacklist[100];
   ch             OS1[60] = "?", OS2[60] = "?", OS3[60] = "?";
   ch             message[300], progexe[300];
   ch             buff1[300], buff2[300], hexaddr[20];
   ch             *arch, *pp1, *pp2, dlim, *pfunc;

   if (crash++) return;                                                                //  re-entry or multiple threads crash

   va_start(arglist,format);
   vsnprintf(message,300,format,arglist);
   va_end(arglist);

   uname(&unbuff);                                                                     //  get cpu arch. 32/64 bit
   arch = unbuff.machine;
   fid1 = popen("lsb_release -d","r");                                                 //  get Linux flavor and release
   if (fid1) {
      ii = fscanf(fid1,"%s %s %s",OS1,OS2,OS3);
      pclose(fid1);
   }

   printf("\n*** zappcrash: %s %s %s %s %s %s \n",
               arch, OS2, OS3, zappvers, build_date_time, message);
   printf("*** zappcrash context: %s | %s \n",zappcrash_context1, zappcrash_context2);

   nstack = backtrace(stacklist,nstack);                                               //  get backtrace data
   if (nstack <= 0) zexit(0,"zappcrash backtrace() failure");
   if (nstack > 100) nstack = 100;

   fid1 = fopen("zbacktrace","w");                                                     //  open backtrace data output file
   if (! fid1) zexit(0,"zappcrash fopen() failure");

   fd = fileno(fid1);
   backtrace_symbols_fd(stacklist,nstack,fd);                                          //  write backtrace data
   fclose(fid1);                                                                       //  (use of malloc() is avoided)

   fid1 = fopen("zbacktrace","r");                                                     //  open backtrace data file
   if (! fid1) zexit(0,"zappcrash fopen() failure");

   fid2 = fopen("zappcrash","w");                                                      //  open zappcrash output file
   if (! fid2) zexit(0,"zappcrash fopen() failure");

   fprintf(fid2,"\n*** zappcrash: %s %s %s %s %s %s \n",
                     arch, OS2, OS3, zappvers, build_date_time, message);
   fprintf(fid2,"*** zappcrash context: %s | %s \n",zappcrash_context1, zappcrash_context2);
   fprintf(fid2,"*** please send this crash report to mkornelix@gmail.com *** \n"
                "*** if possible, please explain how to repeat this problem *** \n");

   cc = readlink("/proc/self/exe",progexe,300);                                        //  get own program path
   if (cc > 0) progexe[cc] = 0;                                                        //  readlink() quirk
   else {
      fprintf(fid2,"progexe not available \n");
      Flinenos = 0;
   }

   err = zshell(0,"which addr2line >/dev/null");                                       //  check if addr2line() available
   if (err) Flinenos = 0;

   for (ii = 0; ii < nstack; ii++)                                                     //  loop backtrace records
   {
      pp1 = pp2 = 0;
      fgets_trim(buff1,300,fid1);                                                      //  read backtrace line
      if (! Flinenos) goto output;
      pfunc = 0;
      pp1 = strstr(buff1,"+0x");                                                       //  new format (+0x12345...)
      if (pp1) pp2 = strchr(pp1,')');
      else {
         pp1 = strstr(buff1,"[0x");                                                    //  old format [0x12345...]
         if (pp1) pp2 = strchr(pp1,']');
      }
      if (! pp1 || ! pp2) goto output;                                                 //  cannot parse
      dlim = *pp2;
      *pp2 = 0;
      strncpy0(hexaddr,pp1+1,20);
      *pp2 = dlim;
      snprintf(buff2,300,"addr2line -i -e %s %s",progexe,hexaddr);                     //  convert to source program
      fid3 = popen(buff2,"r");                                                         //    and line number
      if (! fid3) goto output;
      pfunc = fgets(buff2,300,fid3);
      pclose(fid3);
      if (! pfunc) goto output;
      cc = strlen(pfunc);
      if (cc < 10) goto output;
      if (pfunc[cc-1] < ' ') pfunc[cc-1] = 0;                                          //  remove tailing \n if present
      strncatv(buff1,300,"\n--- ",pfunc,null);
   output:
      fprintf(fid2,"%s \n",buff1);                                                     //  output
   }

   fclose(fid1);
   fclose(fid2);

   zshell(0,"rm zbacktrace");                                                          //  remove backtrace file
   zshell(0,"cat zappcrash");                                                          //  add zappcrash to log file
   if (*logfile == '/') zshell(0,"cp -f %s $HOME/zappcrash",logfile);                  //  copy log file to $HOME/zappcrash

   snprintf(message,300,"A fatal error has occurred. \n"                               //  do this last                          25.1
                        "See zappcrash file at $HOME/zappcrash");
   xmessage(message);

   zexit(0,"Zexit zappcrash");                                                         //  quit
}


/**************************************************************************************/

//  application initialization function to catch some bad news signals
//  the signal handler calls zappcrash() to output a backtrace dump and exit

void catch_signals()
{
   void sighandler(int signal);
   struct sigaction  sigact;

   sigact.sa_handler = sighandler;
   sigemptyset(&sigact.sa_mask);
   sigact.sa_flags = 0;

   sigaction(SIGTERM,&sigact,0);
   sigaction(SIGSEGV,&sigact,0);
   sigaction(SIGILL,&sigact,0);                                                        //  man page says cannot be caught
   sigaction(SIGFPE,&sigact,0);
   sigaction(SIGBUS,&sigact,0);
   sigaction(SIGABRT,&sigact,0);                                                       //  heap or stack corruption
   return;
}


//  catch fatal signals and produce backtrace dumps on-screen

void sighandler(int signal)
{
   cch  *signame = "unknown";

   if (signal == SIGTERM) zexit(0,"TERMINATED");
   if (signal == SIGKILL) zexit(0,"KILLED");
   if (signal == SIGSEGV) signame = "segment fault";
   if (signal == SIGILL) signame = "illegal operation";
   if (signal == SIGFPE) signame = "arithmetic exception";
   if (signal == SIGBUS) signame = "bus error (bad memory)";
   if (signal == SIGABRT) signame = "abort";

   zappcrash("fatal signal: %s",signame);
   exit(0);
}


/**************************************************************************************/

//  Implement the TRACE macro.
//  Trace program execution by function and source code line number.
//  tracedump() dumps last 50 uses of TRACE macro, latest first.

namespace tracenames
{
   ch    filebuff[50][100];                                                            //  last 50 TRACE calls
   ch    funcbuff[50][60];
   int   linebuff[50];
   void  *addrbuff[50];
   int   ii, ftf = 1;
};


//  Args are source file, source function name, source code line number,
//  caller address. These all come from the GCC compiler and TRACE macro.

void trace(ch *file, ch *func, int line, void *addr)
{
   using namespace tracenames;

   if (ftf) {
      ftf = 0;
      for (ii = 0; ii < 50; ii++) {
         filebuff[ii][99] = 0;
         funcbuff[ii][39] = 0;
         linebuff[ii] = 0;
         addrbuff[ii] = 0;
      }
      ii = 0;
   }

   if (line == linebuff[ii] &&
      strmatch(func,funcbuff[ii])) return;                                             //  same as last call, don't duplicate

   if (++ii > 49) ii = 0;                                                              //  add data to list
   strncpy(&filebuff[ii][0],file,99);
   strncpy(&funcbuff[ii][0],func,39);
   linebuff[ii] = line;
   addrbuff[ii] = addr;
   return;
}


//  dump trace records to STDOUT

void tracedump()
{
   using namespace tracenames;

   FILE     *fid;
   int      kk;

   printf(" *** tracedump *** \n");

   kk = ii;
   while (linebuff[kk]) {
      printf("TRACE %s %s %d %p \n",&filebuff[kk][0],
              &funcbuff[kk][0],linebuff[kk],addrbuff[kk]);
      if (--kk == ii) break;
   }

   fid = fopen("tracedump","w");
   if (! fid) {
      perror("tracedump fopen() failure \n");
      return;
   }

   fprintf(fid, " *** tracedump *** \n");

   kk = ii;
   while (linebuff[kk]) {
      fprintf(fid, "TRACE %s %s %d %p \n",&filebuff[kk][0],
                    &funcbuff[kk][0],linebuff[kk],addrbuff[kk]);
      if (--kk == ii) break;
   }

   fclose(fid);
   return;
}


/**************************************************************************************/

//  ch * combine_argvs(int argc, ch *argv[], Nth)
//    combine argv[ii] elements from Nth to last
//    returned char string has one blank between each element

ch * combine_argvs(int argc, ch *argv[], int Nth)
{
   int          ii, ccv, outcc = 0;
   static ch    output[XFCC];

   for (ii = Nth; ii < argc; ii++)
   {
      ccv = strlen(argv[ii]);
      if (outcc + ccv > XFCC - 2) return 0;
      strcpy(output+outcc,argv[ii]);
      outcc += ccv;
      output[outcc] = ' ';
      outcc++;
   }

   outcc--;
   output[outcc] = 0;
   return output;
}


/**************************************************************************************/

//  get elapsed seconds
//  if init = 0 (default if missing), return seconds since boot.
//  if init = 1, returns seconds since last call.
//  execution time is 0.013 microseconds on 5 GHz computer

double get_seconds(int init)
{
   timespec       time1;
   static double  secs1 = 0, secs2, secs3;

   if (init == 0) {
      clock_gettime(CLOCK_MONOTONIC_RAW,&time1);
      secs1 = time1.tv_sec;
      secs1 += time1.tv_nsec * 0.000000001;
      return secs1;
   }
   else {
      clock_gettime(CLOCK_MONOTONIC_RAW,&time1);
      secs2 = time1.tv_sec;
      secs2 += time1.tv_nsec * 0.000000001;
      secs3 = secs2 - secs1;
      secs1 = secs2;
      return secs3;
   }
}


//  print elapsed seconds since last call, with label

void print_seconds(ch *label)
{
   double secs = get_seconds(1);
   printf("%s  seconds: %.3f \n",label,secs);
   return;
}


//  get time in real seconds from timespec struct

double get_seconds(timespec *ts)
{
   double   secs;

   secs = ts->tv_sec;
   secs += ts->tv_nsec * 0.000000001;
   return secs;
}


/**************************************************************************************/

//  start a timer or get elapsed time with millisecond resolution.

void start_timer(double &time0)
{
   timeval  timev;

   gettimeofday(&timev,0);
   time0 = timev.tv_sec + 0.000001 * timev.tv_usec;
   return;
}

double get_timer(double &time0)
{
   timeval  timev;
   double   time;

   gettimeofday(&timev,0);
   time = timev.tv_sec + 0.000001 * timev.tv_usec;
   return time - time0;
}


/**************************************************************************************/

//  get elapsed CPU time used by current process
//  returns seconds with millisecond resolution

double CPUtime()
{
   clock_t ctime = clock();
   double dtime = ctime / 1000000.0;
   return dtime;
}


/**************************************************************************************/

//  get real memory used by current process, MB

int memused()
{
   ch       buff1[100], buff2[1000];
   ch       *pp = 0;
   FILE     *fid;
   double   mem;
   int      MB, pagesize;
   int      mega = 1024 * 1024;

   snprintf(buff1,100,"/proc/self/stat");                                              //  read file /proc/self/stat
   fid = fopen(buff1,"r");
   if (fid) {
      pp = fgets(buff2,1000,fid);
      fclose(fid);
   }

   MB = 0;

   if (pp) {
      pp = strchr(pp,')');                                                             //  closing ')' after (short) filename
      if (pp) {
         parseprocrec(pp+1,22,&mem,null);                                              //  get real memory
         pagesize = sysconf(_SC_PAGESIZE);                                             //  system page size
         MB = mem * pagesize / mega + 0.5;
      }
   }

   return MB;
}


/**************************************************************************************/

//  convert a time_t date/time (e.g. st_mtime from stat() call)
//    into a compact date/time format "yyyymmddhhmmss"

void compact_time(time_t DT, ch *compactDT)
{
   struct tm   *fdt;
   int         year, mon, day, hour, min, sec;

   fdt = localtime(&DT);

   year = fdt->tm_year + 1900;
   mon = fdt->tm_mon + 1;
   day = fdt->tm_mday;
   hour = fdt->tm_hour;
   min = fdt->tm_min;
   sec = fdt->tm_sec;

   compactDT[0] = year / 1000 + '0';
   compactDT[1] = (year % 1000) / 100 + '0';
   compactDT[2] = (year % 100) / 10 + '0';
   compactDT[3] = year % 10 + '0';
   compactDT[4] = mon / 10 + '0';
   compactDT[5] = mon % 10 + '0';
   compactDT[6] = day / 10 + '0';
   compactDT[7] = day % 10 + '0';
   compactDT[8] = hour / 10 + '0';
   compactDT[9] = hour % 10 + '0';
   compactDT[10] = min / 10 + '0';
   compactDT[11] = min % 10 + '0';
   compactDT[12] = sec / 10 + '0';
   compactDT[13] = sec % 10 + '0';
   compactDT[14] = 0;

   return;
}


/**************************************************************************************/

//  convert a time_t date/time (e.g. st_mtime from stat() call)
//    into a pretty date/time format "yyyy:mm:dd hh:mm:ss" (19 chars. + null)

void pretty_datetime(time_t DT, ch *prettyDT)                                          //  25.1
{
   struct tm   *fdt;
   int         year, mon, day, hour, min, sec;

   fdt = localtime(&DT);

   year = fdt->tm_year + 1900;
   mon = fdt->tm_mon + 1;
   day = fdt->tm_mday;
   hour = fdt->tm_hour;
   min = fdt->tm_min;
   sec = fdt->tm_sec;

   prettyDT[0] = year / 1000 + '0';
   prettyDT[1] = (year % 1000) / 100 + '0';
   prettyDT[2] = (year % 100) / 10 + '0';
   prettyDT[3] = year % 10 + '0';
   prettyDT[4] = ':';
   prettyDT[5] = mon / 10 + '0';
   prettyDT[6] = mon % 10 + '0';
   prettyDT[7] = ':';
   prettyDT[8] = day / 10 + '0';
   prettyDT[9] = day % 10 + '0';
   prettyDT[10] = ' ';
   prettyDT[11] = hour / 10 + '0';
   prettyDT[12] = hour % 10 + '0';
   prettyDT[13] = ':';
   prettyDT[14] = min / 10 + '0';
   prettyDT[15] = min % 10 + '0';
   prettyDT[16] = ':';
   prettyDT[17] = sec / 10 + '0';
   prettyDT[18] = sec % 10 + '0';
   prettyDT[19] = 0;

   return;
}


/**************************************************************************************/

//  seconds since 1970 (double) <--> year/mon/day/hour/min/secs (int[6])

void secs_datetime(double secs, int datetime[6])
{
   time_t   tsecs = secs;
   tm       *tmx;

   tmx = localtime(&tsecs);
   datetime[0] = tmx->tm_year + 1900;
   datetime[1] = tmx->tm_mon + 1;
   datetime[2] = tmx->tm_mday;
   datetime[3] = tmx->tm_hour;
   datetime[4] = tmx->tm_min;
   datetime[5] = tmx->tm_sec;
   return;
}


void datetime_secs(int datetime[6], double *secs)
{
   time_t   tsecs;
   tm       tmx;

   tmx.tm_year = datetime[0] - 1900;
   tmx.tm_mon  = datetime[1] - 1;
   tmx.tm_mday = datetime[2];
   tmx.tm_hour = datetime[3];
   tmx.tm_min  = datetime[4];
   tmx.tm_sec  = datetime[5];
   tmx.tm_isdst = -1;

   tsecs = mktime(&tmx);
   *secs = tsecs;
   return;
}


/**************************************************************************************/

//  Read and parse /proc file with records formatted "parmname xxxxxxx"
//  Find all requested parameters and return their numeric values

int parseprocfile(ch *pfile, ch *pname, double *value, ...)                            //  EOL = 0
{
   FILE        *fid;
   va_list     arglist;
   ch          buff[1000];
   ch          *pnames[20];
   double      *values[20];
   int         ii, fcc, wanted, found;

   pnames[0] = pname;                                                                  //  1st parameter
   values[0] = value;
   *value = 0;

   va_start(arglist,value);

   for (ii = 1; ii < 20; ii++)                                                         //  get all parameters
   {
      pnames[ii] = va_arg(arglist,ch *);
      if (! pnames[ii]) break;
      values[ii] = va_arg(arglist,double *);
      *values[ii] = 0;                                                                 //  initialize to zero
   }

   va_end(arglist);

   if (ii == 20) zappcrash("parseProcFile, too many fields");
   wanted = ii;
   found = 0;

   fid = fopen(pfile,"r");                                                             //  open /proc/xxx file
   if (! fid) return 0;

   while ((fgets(buff,999,fid)))                                                       //  read record, "parmname nnnnn"
   {
      for (ii = 0; ii < wanted; ii++)
      {                                                                                //  look for my fields
         fcc = strlen(pnames[ii]);
         if (strmatchN(buff,pnames[ii],fcc)) {
            *values[ii] = atof(buff+fcc);                                              //  return value
            found++;
            break;
         }
      }

      if (found == wanted) break;                                                      //  stop when all found
   }

   fclose(fid);
   return found;
}


//  Parse /proc record of the type  "xxx xxxxx xxxxx xxxxxxxx xxx"
//  Return numeric values for requested fields (starting with 1)

int parseprocrec(ch *prec, int field, double *value, ...)                              //  EOL = 0
{
   va_list     arglist;
   int         xfield = 1, found = 0;

   va_start(arglist,value);

   while (*prec == ' ') prec++;                                                        //  skip leading blanks

   while (field > 0)
   {
      while (xfield < field)                                                           //  skip to next wanted field
      {
         prec = strchr(prec,' ');                                                      //  find next blank
         if (! prec) break;
         while (*prec == ' ') prec++;                                                  //  skip multiple blanks
         xfield++;
      }

      if (! prec) break;
      *value = atof(prec);                                                             //  convert, return double
      found++;

      field = va_arg(arglist,int);                                                     //  next field number
      if (! field) break;
      value = va_arg(arglist,double *);                                                //  next output double *
   }

   while (field > 0)
   {
      *value = 0;                                                                      //  zero values not found
      field = va_arg(arglist,int);
      value = va_arg(arglist,double *);
   }

   va_end(arglist);
   return found;
}


/**************************************************************************************/

//  get current CPU temperature
//  returns 0 if cannot find

int coretemp()                                                                         //  use package temp
{
   FILE           *fid;
   static int     ftf = 1, zone, temp;
   static ch      Tfile[200];
   ch             buff[200], *pp;

   if (ftf)                                                                            //  first call, setup
   {                                                                                   //  dump files ".../thermal_zone*/type"
      ftf = 0;
      fid = popen("cat /sys/class/thermal/thermal_zone*/type","r");                    //  find file containing "pkg_temp"
      if (! fid) return 0;
      for (zone = 0; ; zone++) {
         pp = fgets(buff,200,fid);
         if (! pp) break;
         pp = strstr(pp,"pkg_temp");
         if (pp) break;
      }
      pclose(fid);                                                                     //  ignore 'broken pipe' status

      if (! pp) {                                                                      //  failed
         zone = -1;
         return 0;
      }
      snprintf(Tfile,200,"cat /sys/class/thermal/thermal_zone%d/temp",zone);           //  corresp. file ".../thermal_zone*/temp"
   }

   if (zone < 0) return 0;                                                             //  setup call failed

   fid = popen(Tfile,"r");                                                             //  read temp file
   if (! fid) return 0;
   pp = fgets(buff,200,fid);
   pclose(fid);
   if (! pp) return 0;
   temp = atoi(pp) / 1000;                                                             //  get temp, deg. C x 1000
   return temp;
}


/**************************************************************************************/

//  get current temperature for given disk, e.g. "/dev/sda"
//  depends on "smartctl" command from package smartmontools

int disktemp(ch *disk)
{
   int         id, temp;
   ch          *pp, *pp2;
   ch          buff[200], command[100];
   FILE        *ffid;

   temp = 0;
   pp2 = 0;
   snprintf(command,100,"smartctl -A %s",disk);
   ffid = popen(command,"r");
   if (! ffid) return 0;

   while (true) {
      pp = fgets(buff,200,ffid);                                                       //  revised for smartctl report
      if (! pp) break;                                                                 //    format changes
      if (strmatchN(pp,"ID#",3)) pp2 = strstr(pp,"RAW_VALUE");
      id = atoi(pp);
      if (id != 190 && id != 194) continue;                                            //  Airflow Temp. or Temp.
      if (! pp2) continue;
      temp = atoi(pp2);
      if (temp < 10 || temp > 99) temp = 0;
      break;
   }

   pclose(ffid);
   return temp;
}


/**************************************************************************************/

//  sleep for specified time in seconds (double)
//  signals can cause early return

void zsleep(double dsecs)
{
   unsigned    isecs, nsecs;
   timespec    tsecs;

   if (dsecs <= 0) return;
   isecs = unsigned(dsecs);
   nsecs = unsigned(1000000000.0 * (dsecs - isecs));
   tsecs.tv_sec = isecs;
   tsecs.tv_nsec = nsecs;
   nanosleep(&tsecs,null);
   return;
}


/**************************************************************************************/

//  loop for specified time in seconds (double)

void zloop(double dsecs)
{
   double  time0, time1;

   if (dsecs <= 0) return;
   time0 = get_seconds();
   time1 = time0 + dsecs;
   while (get_seconds() < time1) continue;
   return;
}


/**************************************************************************************/

//  spinlock() is a simply way for a process to protect a code block from
//  concurrent execution by more than one thread, including the main() thread.
//  CANNOT BE USED for coroutines within one thread, e.g. GTK main loop.
//
//  spinlock(1);
//    ...  protected code                 //  only one thread at a time can be in here
//  spinlock(0);
//
//  will deadlock if already locked by same thread

pthread_mutex_t spinmutex = PTHREAD_MUTEX_INITIALIZER;

void spinlock(int lock)
{
   if (lock) mutex_lock(&spinmutex);
   else mutex_unlock(&spinmutex);
   return;
}


/**************************************************************************************/

//  Lock or unlock a multi-process multi-thread resource.
//  Only one process/thread may possess a given lock.
//  A reboot or process exit or crash releases the lock.
//  lockfile is typically "/tmp/filename" and does not have to exist
//
//  fd = global_lock(lockfile);
//    ...   protected code                   //  only one process/thread at a time
//  global_unlock(fd,lockfile);


int global_lock(ch *lockfile)
{
   int       err, fd;

   while (true)                                                                        //  loop until success
   {
      fd = open(lockfile,O_RDWR|O_CREAT,0666);                                         //  open the lock file
      if (fd < 0) zappcrash("global_lock() %s",strerror(errno));
      err = flock(fd,LOCK_EX);                                                         //  request exclusive lock
      if (! err) return fd + 1;                                                        //  return value >= 1
      close(fd);                                                                       //  failed
      zsleep(0.001);                                                                   //  wait a bit and try again
   }
}

void global_unlock(int fd, ch *lockfile)
{
   int err = close(fd-1);
   if (err < 0) zappcrash("global_unlock() %s",strerror(errno));
   return;
}


/**************************************************************************************/

//  lock or unlock a resource
//  does not spin or wait for resource.
//  usable within or across threads in one process.
//  CANNOT BE USED for coroutines within one thread, e.g. GTK main loop.
//  return 0 if already locked, otherwise lock and return 1.

mutex_t resource_lock_lock = PTHREAD_MUTEX_INITIALIZER;

int resource_lock(int &resource)
{
   if (resource) return 0;                                                             //  locked

   mutex_lock(&resource_lock_lock);
   if (resource) {
      mutex_unlock(&resource_lock_lock);                                               //  locked
      return 0;
   }
   resource = 1;
   mutex_unlock(&resource_lock_lock);
   return 1;                                                                           //  locked OK
}

//  unlock a locked resource

void resource_unlock(int &resource)
{
   mutex_lock(&resource_lock_lock);
   if (resource != 1) zappcrash("resource not locked");                                //  not locked
   resource = 0;                                                                       //  unlock
   mutex_unlock(&resource_lock_lock);
   return;
}


/**************************************************************************************/

//  Safely access and update parameters from multiple threads.
//  A mutex lock is used to insure one thread at a time has access to the parameter.
//  Many parameters can be used but there is only one mutex lock.
//  CANNOT BE USED for coroutines within one thread, e.g. GTK main loop.

mutex_t zget_lock = PTHREAD_MUTEX_INITIALIZER;

int zget_locked(int &param)                                                            //  lock and return parameter
{                                                                                      //  (wait if locked)
   mutex_lock(&zget_lock);
   return param;
}

void zput_locked(int &param, int value)                                                //  set and unlock parameter
{
   param = value;
   mutex_unlock(&zget_lock);
   return;
}

int zadd_locked(int &param, int incr)                                                  //  lock, increment, unlock, return
{
   int      retval;

   mutex_lock(&zget_lock);
   retval = param + incr;
   param = retval;
   mutex_unlock(&zget_lock);
   return retval;
}


/**************************************************************************************/

//  Start a detached thread using a simplified protocol.
//  Will not make a zombie if caller exits without checking thread status.

pthread_t start_detached_thread(void * threadfunc(void *), void * arg)
{
   pthread_attr_t pthattr;
   pthread_t      pthtid;
   int            ii, err;

   pthread_attr_init(&pthattr);
   pthread_attr_setdetachstate(&pthattr,PTHREAD_CREATE_DETACHED);

   for (ii = 0; ii < 1000; ii++)
   {
      err = pthread_create(&pthtid,&pthattr,threadfunc,arg);
      if (! err) return pthtid;
      zsleep(0.001);
      if (err == EAGAIN) continue;                                                     //  this shit happens
      break;
   }

   zexit(1,"pthread_create() failure: %s",strerror(err));
   return 0;                                                                           //  avoid compiler warning
}


/**************************************************************************************/

//  Start a thread using a simplified protocol.
//  Caller must call wait_Jthread() to avoid creating a zombie process.

pthread_t start_Jthread(void * threadfunc(void *), void * arg)
{
   pthread_t   tid;
   int         err;

   err = pthread_create(&tid, null, threadfunc, arg);                                  //  retry EAGAIN failure removed
   if (! err) return tid;                                                              //  (does not work)
   zexit(1,"pthread_create() failure: %s",strerror(err));                              //  fail totally
   return 0;
}


//  wait for thread to exit.

int wait_Jthread(pthread_t tid)
{
   int      err;

   err = pthread_join(tid, null);
   if (! err) return 0;
   zexit(1,"pthread_join() failure: %s",strerror(err));
   return 0;
}


/**************************************************************************************/

//  Synchronize execution of multiple threads.
//  Simultaneously resume NT calling threads.
//  from main():        synch_threads(NT)    /* setup to synch NT threads */
//  from each thread:   synch_threads(0)     /* suspend, resume simultaneously */
//
//  Each calling thread will suspend execution until all threads have suspended,
//  then they will all resume execution at the same time. If NT is greater than
//  the number of calling threads, the threads will never resume.

void synch_threads(int NT)
{
   static pthread_barrier_t   barrier;
   static int                 bflag = 0;

   if (NT) {                                                                           //  main(), initialize
      if (bflag) pthread_barrier_destroy(&barrier);
      pthread_barrier_init(&barrier,null,NT);
      bflag = 1;
      return;
   }

   pthread_barrier_wait(&barrier);                                                     //  thread(), wait for NT threads
   return;                                                                             //  unblock
}


/**************************************************************************************/

//  Test if caller is from the main() thread or from a created thread.
//  return 1 if main(), else 0

int main_thread()
{
   if (pthread_equal(pthread_self(),zfuncs::tid_main)) return 1;
   return 0;
}


/**************************************************************************************

   int err = zshell(ch *options, ch *command, ...)

   Format and perform a shell command, wait for completion, return status.
   Shell command is performed in a thread with a GTK

   options: may be null or may contain any of the following substrings:
             "log"      write command to log file, stdout
             "ack"      popup user ACK message if the shell command has an error
             "noerr"    do not log error status
             "thread"   run shell command in a thread with GTK main loop wait          //  25.1

   command:  shell command with optional '%' printf formats
     ...  :  optional arguments to stuff into printf formats

   returns: status of the shell command

***************************************************************************************/

int   zshell_stat;

int zshell(ch *options, ch *command, ...)
{
   void * zshell_thread(void *command);

   int         Flog, Fack, Fnoerr, Fthread;
   va_list     arglist;
   int         err, cc, ccmax = 9999;                                                  //  25.1
   ch          command2[10000];
   pthread_t   tid;

   Flog = Fack = Fnoerr = Fthread = 0;

   if (options) {
      if (strstr(options,"log")) Flog = 1;                                             //  set options
      if (strstr(options,"ack")) Fack = 1;
      if (strstr(options,"noerr")) Fnoerr = 1;
      if (strstr(options,"thread")) Fthread = 1;                                       //  25.1
   }

   va_start(arglist,command);                                                          //  format command
   cc = vsnprintf(command2,ccmax,command,arglist);
   va_end(arglist);
   if (cc >= ccmax) zappcrash("zshell: buffer overflow: %d",cc);

   if (Fnoerr && ccmax-cc > 17) strcat(command2," >/dev/null 2>&1");                   //  suppress error output if not wanted   25.0

   if (Flog) printf("zshell: %s \n",command2);                                         //  command > log file if wanted

   if (Fthread)                                                                        //  do shell command in a thread          25.1
   {
      zshell_stat = -1;
      tid = start_Jthread(zshell_thread,command2);                                     //  start thread
      while (zshell_stat < 0) zmainsleep(0.01);                                        //  wait for command status
      wait_Jthread(tid);                                                               //  join thread
   }

   else zshell_stat = system(command2);

   err = WEXITSTATUS(zshell_stat);                                                     //  get thread status
   if (err && ! Fnoerr) {                                                              //  suppress log of command and error
      if (! Flog) printf("zshell: %s \n",command2);                                    //  log command if not already
      printf("*** zshell error: %s \n",strerror(err));                                 //  log error
      if (Fack) zmessageACK(mainwin,"command: %s \n error: %s",
                                       command2, strerror(err));                       //  popup error to user if wanted
   }

   return err;                                                                         //  return completion status
}


void * zshell_thread(void *command)
{
   zshell_stat = system((ch *) command);
   return 0;
}


/**************************************************************************************/

//  kill processes matching a given process name, which may have '*' wildcards
//    int NP = kill_procname("process*name*")
//  status returned: N = processes found (0 if none found)

int kill_procname(ch *wildname)
{
   DIR            *procdir;
   struct dirent  *procent;
   int            cc, found = 0;
   ch             *pp, *pid, *filename;
   ch             buff1[100], buff2[XFCC];

   procdir = opendir("/proc");
   if (! procdir) return 1;

   while (true)
   {
      procent = readdir(procdir);
      if (! procent) break;
      pid = procent->d_name;
      snprintf(buff1,100,"/proc/%s/exe",pid);
      cc = readlink(buff1,buff2,XFCC);
      if (cc <= 0) continue;
      buff2[cc] = 0;
      pp = strrchr(buff2,'/');
      if (! pp) continue;
      filename = pp + 1;
      if (MatchWild(wildname,filename) != 0) continue;
      zshell("log","kill %s",pid);
      found++;
   }

   closedir(procdir);
   return found;
}


/**************************************************************************************/

//  Signal a running subprocess by name (name of executable or shell command).
//  Signal is "pause", "resume" or "kill". If process is paused, kill may not work,
//  so issue resume first if process is paused.

int signalProc(ch *pname, ch *signal)
{
   pid_t       pid;
   FILE        *fid;
   ch          buff[100], *pp;
   int         err, nsignal = 0;

   snprintf(buff,100,"ps -C %s h o pid",pname);
   fid = popen(buff,"r");                                                              //  popen() instead of system()
   if (! fid) return 2;
   pp = fgets(buff,100,fid);
   pclose(fid);
   if (! pp) return 4;

   pid = atoi(buff);
   if (! pid) return 5;

   if (strmatch(signal,"pause")) nsignal = SIGSTOP;
   if (strmatch(signal,"resume")) nsignal = SIGCONT;
   if (strmatch(signal,"kill")) nsignal = SIGKILL;

   err = kill(pid,nsignal);
   return err;
}


/**************************************************************************************/

//  fgets() with additional feature: trailing \n \r are removed.
//  optional bf flag: true if trailing blanks are to be removed.
//  trailing null character is assured.

ch * fgets_trim(ch *buff, int maxcc, FILE *fid, int bf)
{
   int      cc;
   ch       *pp;

   pp = fgets(buff,maxcc,fid);
   if (! pp) return pp;
   cc = strlen(buff);
   if (bf) while (cc && buff[cc-1] > 0 && buff[cc-1] <= ' ') --cc;
   else    while (cc && buff[cc-1] > 0 && buff[cc-1] < ' ') --cc;
   buff[cc] = 0;
   return pp;
}


/**************************************************************************************/

//  get next record from open file steam
//  do not block if next record is not yet available
//
//    buff: buffer for reading file
//     bcc: length of buffer
//     fid: open file stream
//
//  returned ch * pointer:
//       null: EOF or error - file can be closed
//       "fgets_pend\n": next record not yet available, try again later
//       other: pointer to record in buffer, \n\0 terminated

ch * fgets_pend(ch *buff, int bcc, FILE *fid)                                          //  25.1
{
   int            fd, flags;
   static FILE    *pfid = 0;
   ch             *pp;

   if (fid != pfid) {
      pfid = fid;
      fd = fileno(fid);                                                                //  stop fgets() from blocking
      flags = fcntl(fd,F_GETFL,0);
      flags |= O_NONBLOCK;
      fcntl(fd,F_SETFL,flags);
   }

   pp = fgets(buff,500,fid);                                                           //  read next record in file
   if (pp) return pp;                                                                  //  success
   if (feof(fid)) return 0;                                                            //  EOF, no more data
   return "fgets_pend\n";                                                              //  next record not available yet
}


/**************************************************************************************

   This function is logically equivalent to fgets().
   It uses a huge buffer to read many logical records at once,
      replacing many calls to fgets() with few calls to fread(). 

   Does it improve Windows WSL2 speed to read huge file with small records?  [ NO ]

   initial call:      ch * rec = fgetsB(ch *file)   get first logical record
   subsequent calls:       rec = fgetsB(0)          get next logical record (null = EOF) 

   fgets() difference: fgetsB() does not return the \n logical record delimiters, 
                         null is returned at end of each record instead of \n + null.
                       trailing blanks in returned record are set to null.

***************************************************************************************/

ch * fgetsB(cch *file, int &ftf)                                                       //  26.0
{
   #define bcc 10000000
   static ch      buff[bcc+1];                                                         //  10 MB buffer + 1
   static FILE    *fid;
   static ch      *pp1, *pp2, *pp3;
   static int     bcc2, pcc, Fempty, Feof;

   if (! ftf) goto nextrec;                                                            //  get next logical record
   ftf = 0;
   
   fid = fopen(file,"r");                                                              //  initial call
   if (! fid) {
      printf("failed to open file: %s %s \n",file,strerror(errno));
      return 0;
   }

   Feof = 0;                                                                           //  not EOF
   Fempty = 1;                                                                         //  buffer is empty
   pcc = 0;                                                                            //  no partial record at buffer start
   
nextrec:

   if (Feof) return 0;                                                                 //  EOF, nothing left

   if (Fempty)                                                                         //  replenish buffer
   {
      bcc2 = bcc - pcc;                                                                //  buff cc - partial record at buffer start
      bcc2 = fread(buff+pcc,1,bcc2,fid);                                               //  fill buffer, after partial record
      if (! bcc2) {                                                                    //  nothing left to read, EOF
         Feof = 1;
         fclose(fid);
         if (pcc) {                                                                    //  partial record at buffer start
            buff[pcc+1] = 0;                                                           //  mark record end
            return buff;                                                               //  return actually complete record
         }
         else return 0;
      }
      buff[bcc2+pcc] = 0;                                                              //  mark end of buffer
      Fempty = 0;
      pp1 = buff + pcc;                                                                //  resume scan from here
   }

   pp2 = pp1;                                                                          //  look for logical record end, \n or null
   while (*pp2 && *pp2 != '\n') pp2++;

   if (*pp2 == 0)                                                                      //  found null, ran off end of buffer
   {
      if (pcc) {
         printf("*** fgetsB(), logical record too long \n");                           //  logical record > buffer size
         return 0;
      }
      pcc = pp2 - pp1;                                                                 //  move partial logical record from
      memmove(buff,pp1,pcc);                                                           //    buffer end to buffer start
      Fempty = 1;
      goto nextrec;                                                                    //  continue reading from file
   }

   *pp2 = 0;                                                                           //  found \n, end of logical record --> null
   pp3 = pp1;                                                                          //  save start of logical record
   pp1 = pp2 + 1;                                                                      //  pp1 = next logical record start after \n
   if (pcc) pp3 = buff;                                                                //  if partial record, start is buff start
   pcc = 0;                                                                            //  reset, no partial record
   return pp3;                                                                         //  return logical record, null terminated
}


/**************************************************************************************/

//  Return 1 if both filespecs have the same folder, else return 0.
//  Both folders must be specified, at least one with ending '/'
//  (true if a file name is present)

int samefolder(ch *file1, ch *file2)
{
   ch       *p1, *p2;
   int      cc1, cc2, cc;

   p1 = strrchr(file1,'/');                                                            //  /dir1/dir2
   p2 = strrchr(file2,'/');                                                            //  /dir1/dir2/file
   cc1 = cc2 = 0;
   if (p1) cc1 = p1 - file1;                                                           //  /dir1/dir2/file
   if (p2) cc2 = p2 - file2;                                                           //  |         |
   if (cc2 > cc1) cc = cc2;                                                            //  0         cc
   else cc = cc1;
   if (cc == 0) return 0;
   if (strmatchN(file1,file2,cc)) return 1;
   return 0;
}


/**************************************************************************************/

//   Parse a pathname (filespec) and return its components.
//   Returned strings are subject to zfree() unless they are null.
//   folder is returned without trailing '/' but with two nulls on end,
//      to allow appending '/' if needed.
//   file is returned without .ext
//   ext is returned with leading '.'
//   returns 0 if OK, 1 if error

int parsefile(ch *path, ch *&folder, ch *&file, ch *&ext)                              //  make reentrant      25.1
{
   ch       *pp1, *pp2;
   int      cc;

   folder = file = ext = 0;

   if (! path || ! *path) return 1;                                                    //  null or empty string
   pp1 = strrchr(path,'/');
   if (! pp1) return 1;                                                                //  /folders/.../file.ext
   cc = pp1 - path;                                                                    //  |           |    |
   if (cc < 2) return 1;                                                               //  path        pp1  pp2
   *pp1 = 0;
   folder = zstrdup(path,"parsefile",2);
   *pp1 = '/';

   pp1++;                                                                              //  /folders/.../file.ext
   if (! *pp1) return 0;                                                               //               |   |
                                                                                       //              pp1  pp2
   pp2 = strrchr(pp1,'.');
   if (! pp2) {
      file = zstrdup(pp1,"parsefile");
      return 0;
   }

   *pp2 = 0;
   file = zstrdup(pp1,"parsefile");
   *pp2 = '.';
   ext = zstrdup(pp2,"parsefile");
   return 0;
}


//  free file name components allocated by parsefile()

void parsefile_free(ch *&folder, ch *&file, ch *&ext)                                  //  25.1
{
   if (folder) zfree(folder);
   if (file) zfree(file);
   if (ext) zfree(ext);
   folder = file = ext = 0;
   return;
}


/**************************************************************************************/

//  Move a source file to a destination file and delete the source file.
//  Equivalent to rename(), but the two files MAY be on different file systems.
//  Pathnames must be absolute (start with '/').
//  Returns 0 if OK, +N if not.

int renamez(ch *file1, ch *file2)
{
   ch       *pp1, *pp2;
   int      err, Frename = 0;

   if (*file1 != '/' || *file2 != '/') return 1;                                       //  not absolute pathnames

   pp1 = strchr((ch *) file1+1,'/');
   pp2 = strchr((ch *) file2+1,'/');
   if (! pp1 || ! pp2) return 2;

   *pp1 = *pp2 = 0;
   if (strmatch(file1,file2)) Frename = 1;
   *pp1 = *pp2 = '/';

   if (Frename) {                                                                      //  same top folder
      err = rename(file1,file2);
      if (err) return errno;
      return 0;
   }

   pp1 = zescape_quotes(file1);
   pp2 = zescape_quotes(file2);
   err = zshell(0,"mv -f \"%s\" \"%s\" ",pp1,pp2);
   zfree(pp1);
   zfree(pp2);
   return err;
}


/**************************************************************************************/

//  Check if a folder exists. If not, ask user if it should be created.
//  Returns 0 if OK or +N if error or user refused to create.
//  The user is notified of failure, no other message needed.
//  path may not have embedded quotes.

int check_create_dir(ch *path)
{
   int      err, yn;
   STATB    statB;

   err = stat(path,&statB);                                                            //  check status
   if (! err) {
      if (S_ISDIR(statB.st_mode)) return 0;                                            //  exists, folder, OK
      else {
         zmessageACK(mainwin,"%s \n %s",path,strerror(ENOTDIR));                       //  exists, not a folder
         return ENOTDIR;
      }
   }

   if (errno != ENOENT) {
      zmessageACK(mainwin,"%s \n %s",path,strerror(errno));                            //  error other than missing
      return errno;
   }

   yn = zmessageYN(0,TX("create folder %s"),path);                                     //  ask to create
   if (! yn) return ENOENT;

   err = zshell("ack","mkdir -p -m 0750 \"%s\" ",path);                                //  create
   if (! err) return 0;

   zmessageACK(mainwin,"%s \n %s",path,strerror(errno));                               //  failed to create
   return errno;
}


/**************************************************************************************/

//  copy a file using shell 'cp' command
//  file owner, permissions, and timestamps are copied
//  returns 0 if OK, else errno

int cp_copy(ch *sfile, ch *dfile)
{
   ch    *pps, *ppd;

   if (strmatch(sfile,dfile)) {
      printf("ignore copy file to self: %s \n",sfile);
      return 0;
   }

   pps = zescape_quotes(sfile);
   ppd = zescape_quotes(dfile);

   int err = zshell(0,"cp -f -p  \"%s\" \"%s\" ",pps,ppd);

   zfree(pps);
   zfree(ppd);

   return err;
}


/**************************************************************************************/

//  get the available space on disk of the given file
//  returns disk space in MB (limit 4 billion MB)

uint diskspace(ch *file)
{
   ch       command[200], buff[200];
   ch       *pp;
   uint     avail;
   FILE     *fid;

   pp = zescape_quotes(file);
   snprintf(command,200,"df --output=avail \"%s\" ",pp);
   zfree(pp);

   fid = popen(command,"r");
   if (! fid) return 0;
   pp = fgets(buff,200,fid);                                                           //  "Avail" header
   pp = fgets(buff,200,fid);                                                           //  decimal number, KB space
   pclose(fid);
   if (! pp) return 0;
   avail = 0.001 * atoll(pp);                                                          //  MB space
   return avail;
}


/**************************************************************************************/

//  if a file has an incorrect .ext, return the correct .ext
//  return null if file is OK or cannot be determined
//  returned .ext is in a static buffer  NOT THREAD SAFE

ch * get_file_extension(ch *file)
{
   ch       *fext1;
   ch       *extlist, *fext2, *pp;
   ch       *buff;
   int      cc;
   FILE     *fid;

   static ch fext3[20];

   errno = 0;
   cc = strlen(file) + 20;
   buff = (ch *) zmalloc(cc,0);
   pp = zescape_quotes(file);
   snprintf(buff,cc,"file --extension %s",pp);                                         //  'file' command - get correct extensions
   zfree(pp);
   fid = popen(buff,"r");
   if (! fid) goto ret0;
   extlist = fgets(buff,cc,fid);                                                       //  /.../filename.ext: ext1/ext2/...
   pclose(fid);
   if (! extlist) goto ret0;
   extlist = strrchr(extlist,':');                                                     //  extlist = : ext1/ext2/...
   if (! extlist) goto ret0;
   extlist += 2;                                                                       //  extlist = ext1/ext2/...

   fext1 = strrchr(file,'.');                                                          //  fext1 = file current .ext
   if (! fext1) fext1 = ".xxx";
   if (strcasestr(extlist,fext1+1)) goto ret0;                                         //  fext1 found in extlist

   fext2 = extlist;
   pp = strchr(fext2,'/');                                                             //  fext2 = first in extlist
   if (pp) *pp = 0;

   strncpy0(fext3,fext2,20);                                                           //  return correct .ext in static buffer
   zfree(buff);
   return fext3;

ret0:
   if (errno) printf("*** %s\n",strerror(errno));
   zfree(buff);
   return 0;
}


/**************************************************************************************/

//  Return all the file names in a folder, sorted in alphabetic order.
//  Subfolders are not included.
//  The 'files' argument is allocated and filled with pointers to file names.
//  (the names in the folder, not the full path names)
//  The number of files found is returned.
//  -1 is returned if the folder is invalid or other error.
//  If 'files' is returned non-null, it is subject to zfree()

int zreaddir(ch *folder, ch **&files)
{
   struct dirent  *dirent1;

   int      Nfiles = 0, maxfiles = 100;
   DIR      *direc;
   ch       **ufiles, **ufiles2;

   files = 0;                                                                          //  nothing returned yet

   ufiles = (ch **) zmalloc(maxfiles * sizeof(ch *),"zreaddir");                       //  starting space

   direc = opendir(folder);                                                            //  open caller's folder
   if (! direc) return -1;

   while (true)
   {
      if (Nfiles == maxfiles)                                                          //  out of space
      {
         ufiles2 = (ch **) zmalloc(2 * maxfiles * sizeof(ch *),"zreaddir");            //  allocate new space = 2x old space
         memcpy(ufiles2,ufiles, maxfiles * sizeof(ch *));                              //  copy data to new space
         zfree(ufiles);                                                                //  free old space
         ufiles = ufiles2;                                                             //  set new space
         maxfiles *= 2;                                                                //  new capacity
      }

      dirent1 = readdir(direc);                                                        //  get next file in folder
      if (! dirent1) break;
      if (dirent1->d_type != DT_REG) continue;                                         //  skip subfolders
      ufiles[Nfiles] = zstrdup(dirent1->d_name,"zreaddir");                            //  add to file list
      Nfiles++;
      continue;
   }

   closedir(direc);

   if (Nfiles > 1) HeapSort(ufiles,Nfiles);                                            //  sort file list

   files = ufiles;                                                                     //  return allocated file list
   return Nfiles;                                                                      //  return file count
}


/**************************************************************************************/

//  int NR = zreadfile(ch *filename, ch &**rrecs)
//
//  Read a text file into a list of ch * strings, 1 record per string.
//  The strings are allocated as needed. The number of records is returned.
//  Returned: -1  error (errno is set)
//             0  empty file
//            NR  records read, > 0
//  Returned record N: rrecs[N]  (ch *)
//  Trailing blanks and '\n' characters are removed.
//  The maximum record length is 1000 chars, including terminating null.
//  The maximum record count is 1000 records.
//  Null records ("" or "\n") are not included in output.
//  rrecs[NR] (last + 1) is a null pointer.

int zreadfile(ch *filename, ch **&rrecs)
{
   FILE           *fid;
   ch             *recs[1001];
   ch             buff[1001], *pp;
   int            cc, NR = 0;

   rrecs = 0;                                                                          //  initz. no data

   fid = fopen(filename,"r");                                                          //  open file
   if (! fid) return -1;

   while (true)
   {
      pp = fgets(buff,1001,fid);                                                       //  read record
      if (! pp) break;                                                                 //  EOF
      cc = strlen(pp);
      if (cc > 999) {
         zmessageACK(mainwin,"zreadfile() record too long %s",filename);
         errno = EFBIG;
         return -1;
      }

      while (cc && pp[cc-1] > 0 && pp[cc-1] <= ' ') --cc;                              //  remove trailing \n, \r, blanks, etc.
      pp[cc] = 0;                                                                      //  terminating null
      if (cc == 0) continue;                                                           //  discard null recs

      recs[NR] = (ch *) zmalloc(cc+1,"zreadfile");                                     //  allocate memory
      memcpy(recs[NR],pp,cc+1);                                                        //  copy record
      NR++;
      if (NR == 1000) {
         zmessageACK(mainwin,"zreadfile() too many records %s",filename);
         errno = EFBIG;
         return -1;
      }
   }

   fclose(fid);
   recs[NR] = 0;                                                                       //  last record + 1 = null

   cc = (NR + 1) * sizeof(ch *);                                                       //  allocate caller rrecs list
   rrecs = (ch **) zmalloc(cc,"zreadfile");
   memcpy(rrecs,recs,cc);                                                              //  copy record pointers + null

   return NR;
}


//  int NR = zwritefile(ch *filename, ch **rrecs)
//  write array of records to a file, each with trailing \n character.
//  EOF is signalled with a null pointer: rrecs[last] = null
//  returns no. records written (>= 0) or -1 if file error.

int zwritefile(ch *filename, ch **rrecs)
{
   FILE           *fid;
   int            nr, nn;

   fid = fopen(filename,"w");                                                          //  open file
   if (! fid) return -1;

   for (nr = 0; nr < 1000; nr++)
   {
      if (! rrecs[nr]) break;
      nn = fprintf(fid,"%s\n",rrecs[nr]);
      if (nn <= 0) break;
   }

   fclose(fid);
   return nr;
}


//  free allocated records and their pointer list

void zreadfile_free(ch **&rrecs)
{
   for (int ii = 0; rrecs[ii]; ii++)                                                   //  loop until null pointer
      zfree(rrecs[ii]);
   zfree(rrecs);
   rrecs = 0;                                                                          //  set no data
   return;
}


/**************************************************************************************/

//  these functions are a replacement for the strcmp functions
//  null strings are allowed and compare false to non-null strings
//  null strings compare true to null strings

int strmatch(cch *str1, cch *str2)
{
   if (str1 && str2) return ! strcmp(str1,str2);
   if (! str1 && ! str2) return 1;
   else return 0;
}

int strmatchN(cch *str1, cch *str2, int cc)
{
   if (str1 && str2) return ! strncmp(str1,str2,cc);
   else return 0;
}

int strmatchcase(cch *str1, cch *str2)
{
   if (str1 && str2) return ! strcasecmp(str1,str2);
   if (! str1 && ! str2) return 1;
   else return 0;
}

int strmatchcaseN(cch *str1, cch *str2, int cc)
{
   if (str1 && str2) return ! strncasecmp(str1,str2,cc);
   else return 0;
}


/**************************************************************************************

    substringR()

    ch * substringR(ch *string, ch *delims, int Nth)

    Get the Nth substring in an input string, which contains at least N
    substrings delimited by the character(s) in delim (e.g. blank, comma).
    Nth >= 1.

    Returns a pointer to the found substring (actually a pointer to a
    copy of the found substring, with a null terminator appended).
    The returned pointer is a subject for zfree().

    If a delimiter is immediately followed by another delimiter, it is
    considered a substring with zero length, and the string "" is returned.

    Leading blanks in a substring are omitted from the returned substring.
    A substring with only blanks is returned as "".

    The last substring may be terminated by null or a delimiter.

    Characters within quotes (") are treated as data within a substring,
    i.e. blanks and delimiters are not processed as such.
    The quotes are removed from the returned substring.

    If there are less than Nth substrings, a null pointer is returned.

    This function is thread-safe.
    See below for simpler non-thread-safe version.

    Example: input string: ,a,bb,  cc,   ,dd"ee,ff"ggg,
             (first and last characters are comma)
             delimiter: comma
             Nth   returned string
              1:   (null)
              2:   a
              3:   bb
              4:   cc
              5:   (one blank)
              6:   ddee,ffggg
              7:   (null)        last+1 substring

***************************************************************************************/

ch * substringR(ch *string, ch *delims, int Nth)
{
   ch       *pf1, pf2[2000];                                                           //  2000 char. limit
   ch       quote = '"';
   int      nf, fcc = 0;

   if (! string || ! *string) return 0;                                                //  bad call
   if (Nth < 1) return 0;

   pf1 = (ch *) string - 1;                                                            //  start parse
   nf = 0;

   while (nf < Nth)
   {
      pf1++;                                                                           //  start substring
      nf++;
      fcc = 0;

      while (*pf1 == ' ') pf1++;                                                       //  skip leading blanks

      while (true)
      {
         if (*pf1 == quote) {                                                          //  pass chars between quotes
            pf1++;                                                                     //  (but without the quotes)
            while (*pf1 && *pf1 != quote) pf2[fcc++] = *pf1++;
            if (*pf1 == quote) pf1++;
         }
         else if (strchr(delims,*pf1) || *pf1 == 0) break;                             //  found delimiter or null
         else pf2[fcc++] = *pf1++;                                                     //  pass normal character
         if (fcc > 1999) zappcrash("substringR() too long");
      }

      if (*pf1 == 0) break;                                                            //  end of input string
   }

   if (nf < Nth) return 0;                                                             //  no Nth substring
   if (fcc == 0 && *pf1 == 0) return 0;                                                //  empty substring

   pf2[fcc] = 0;
   return zstrdup(pf2,"substringR");                                                   //  returned string (needs zfree())
}


//  alternative with one delimiter

ch * substringR(ch *string, ch delim, int Nth)
{
   ch     delims[2] = "x";
   *delims = delim;
   return substringR(string,delims,Nth);
}


//  non-thread-safe versions without zfree() requirement
//  returned substring is in static memory
//  only the last 1000 results are kept       <----- NOTE limitation

ch * substring(ch *string, ch *delims, int Nth)
{
   ch           *s1;
   static ch    *s2[1000];
   static int   kk = -1;

   if (kk == -1) memset(s2,0,1000*sizeof(ch *));

   s1 = substringR(string,delims,Nth);
   if (! s1) return 0;

   kk++;
   if (kk == 1000) kk = 0;
   if (s2[kk]) zfree(s2[kk]);
   s2[kk] = s1;
   return s1;
}


ch * substring(ch *string, ch delim, int Nth)
{
   ch     delims[2] = "x";
   *delims = delim;
   return substring(string,delims,Nth);
}


/**************************************************************************************

   decode a string with embedded delimiters and return the substrings
   input:   string1^ string2^ ... stringN     where '^' is the delimiter
   output:  output[0], output[1] ... output[N]
   delim: single delimiter character
   count: max. number of output strings
   maxcc: max. output string length (rest truncated) 
   the output strings must be pre-allocated with at least maxcc chars.
   returns: no. substrings found
   output string = single null if two delimiters are separated by nothing or blanks

****/

int get_substrings(ch *input, ch delim, int count, int maxcc, ch **output)             //  25.1
{
   int      Nth = 0, ii, cc;
   ch       *pp1 = input, *pp2;

   for (ii = 0; ii < count; ii++)                              //  clear outputs
      output[ii][0] = 0;

   for (Nth = 0; Nth < count; Nth++)                           //  aaaaaaaa^  bbbbbbbbbbbbb^ ... ^  ccccccc
   {                                                           //  |       |  |            |     |  |      |
      while (*pp1 == ' ') pp1++;                               //  pp1    pp2 pp1         pp2   pp2 pp1    pp2
      if (! *pp1) break;
      pp2 = strchr(pp1,delim);
      if (! pp2) pp2 = pp1 + strlen(pp1);
      cc = pp2 - pp1;
      if (cc >= maxcc) cc = maxcc - 1;
      if (cc) strncpy(output[Nth],pp1,cc);
      output[Nth][cc] = 0;
      if (*pp2 == delim) pp1 = pp2 + 1;
      else pp1 = pp2;
   }

   return Nth;
}


/**************************************************************************************/

//  Produce random value from hashed input string.
//  Output range is 0 to max-1.
//  Benchmark: 0.2 usec for 99 char. string, 3 GHz Core i5

int strHash(ch *string, uint max)
{
   uint     hash = 1357;
   uch      byte;

   while ((byte = *string++))
   {
      hash = hash * (byte + 111);
      hash = hash ^ (hash >> 9);
      hash = hash ^ (byte << 9);
   }

   hash = hash % max;
   return hash;
}


/**************************************************************************************/

//  Copy string with specified max. length (including null terminator).
//  truncate if needed. null terminator is always supplied.
//  Returns 0 if no truncation, 1 if input string was truncated to fit.

int strncpy0(ch *dest, ch *source, uint cc)
{
   strncpy(dest,source,cc);
   dest[cc-1] = 0;
   if (strlen(source) >= cc) return 1;                                                 //  truncated
   else return 0;
}


/**************************************************************************************/

//  Copy string with blank pad to specified length.  No null is added.

void strnPad(ch *dest, ch *source, int cc)
{
   strncpy(dest,source,cc);
   int ii = strlen(source);
   for (int jj = ii; jj < cc; jj++) dest[jj] = ' ';
}


/**************************************************************************************/

//  Remove trailing blanks from a string. Returns remaining length.

int strTrim(ch *dest, ch *source)
{
   if (dest != source) strcpy(dest,source);
   return strTrim(dest);
}

int strTrim(ch *dest)
{
   int  ii = strlen(dest);
   while (ii && (dest[ii-1] == ' ')) dest[--ii] = 0;
   return ii;
}


/**************************************************************************************/

//  Remove leading and trailing blanks from a string.
//  Returns remaining length, possibly zero.

int strTrim2(ch *dest, ch *source)
{
   ch         *pp1, *pp2;
   int         cc;

   pp1 = source;
   pp2 = source + strlen(source) - 1;
   while (*pp1 == ' ') pp1++;
   if (*pp1 == 0) {
      strcpy(dest,"");                                                                 //  null or blank input
      return 0;
   }
   while (*pp2 == ' ' && pp2 > pp1) pp2--;
   cc = pp2 - pp1 + 1;
   memmove(dest,pp1,cc);
   dest[cc] = 0;
   return cc;
}

int strTrim2(ch *string)
{
   return strTrim2(string,string);
}


/**************************************************************************************/

//  Remove all blanks from a string. Returns remaining length.

int strCompress(ch *dest, ch *source)
{
   if (dest != source) strcpy(dest,source);
   return strCompress(dest);
}

int strCompress(ch *string)
{
   int   ii, jj;

   for (ii = jj = 0; string[ii]; ii++)
   {
      if (string[ii] != ' ')
      {
         string[jj] = string[ii];
         jj++;
      }
   }
   string[jj] = 0;
   return jj;
}


/**************************************************************************************/

//  Concatenate multiple strings, staying within a specified overall length.
//  The destination string is also the first source string.
//  Null marks the end of the source strings (omission --> crash).
//  Output is truncated to fit within the specified length.
//  A final null is assured and is included in the length.
//  Returns 0 if OK, 1 if truncation was needed.

int strncatv(ch *dest, int maxcc, ch *source, ...)
{
   ch         *ps;
   va_list     arglist;

   maxcc = maxcc - strlen(dest) - 1;
   if (maxcc < 0) return 1;
   va_start(arglist,source);
   ps = source;

   while (ps)
   {
      strncat(dest,ps,maxcc);
      maxcc = maxcc - strlen(ps);
      if (maxcc < 0) break;
      ps = va_arg(arglist,ch *);
      if (! ps) break;
   }

   va_end(arglist);
   if (maxcc < 0) return 1;
   return 0;
}


/**************************************************************************************/

//  Match 1st string to N additional strings.
//  Return matching string number 1 to N or 0 if no match.
//  Supply a null argument for end of list.

int strmatchV(ch *string, ...)
{
   int         match = 0;
   ch          *stringN;
   va_list     arglist;

   va_start(arglist,string);

   while (true)
   {
      stringN = va_arg(arglist, ch *);
      if (! stringN) {
         va_end(arglist);
         return 0;
      }

      match++;
      if (strmatch(string,stringN))
      {
         va_end(arglist);
         return match;
      }
   }
}


/**************************************************************************************/

//  convert string to upper case (ascii only)

void strToUpper(ch *string)
{
   int   ii;
   ch    jj;
   int   delta = 'A' - 'a';

   for (ii = 0; (jj = string[ii]); ii++)
        if ((jj >= 'a') && (jj <= 'z')) string[ii] += delta;
}

void strToUpper(ch *dest, ch *source)
{
   strcpy(dest,source);
   strToUpper(dest);
}


/**************************************************************************************/

//  convert string to lower case (ascii only) 

void strToLower(ch *string)
{
   int   ii;
   ch    jj;
   int   delta = 'a' - 'A';

   for (ii = 0; (jj = string[ii]); ii++)
        if ((jj >= 'A') && (jj <= 'Z')) string[ii] += delta;
}

void strToLower(ch *dest, ch *source)
{
   strcpy(dest,source);
   strToLower(dest);
}


/**************************************************************************************/

//  Copy string strin to strout, replacing every occurrence
//  of the substring ssin with the substring ssout.
//  maxcc is the limit for strout. The actual cc is returned.
//  Replacement strings may be longer or shorter or have zero length.

int repl_1str(ch *strin, ch *strout, int maxcc, ch *ssin, ch *ssout)
{
   int     cc, cc1, cc2, ccout, nfound;
   ch      *pp, *pp1, *pp2;

   pp1 = strin;
   pp2 = strout;
   cc1 = strlen(ssin);
   cc2 = strlen(ssout);
   nfound = 0;
   ccout = 0;

   while ((pp = strstr(pp1,ssin)))
   {
      nfound++;
      cc = pp - pp1;
      if (ccout + cc >= maxcc) break;
      memcpy(pp2,pp1,cc);
      pp2 += cc;
      pp1 += cc;
      ccout += cc;
      if (ccout + cc2 >= maxcc) break;
      memcpy(pp2,ssout,cc2);
      pp1 += cc1;
      pp2 += cc2;
      ccout += cc2;
   }

   cc = strlen(pp1);
   if (ccout + cc >= maxcc) cc = maxcc - ccout - 1;                                    //  overflow, keep as much as poss.       25.1
   memcpy(pp2,pp1,cc);
   ccout += cc;
   strout[ccout] = 0;

   return ccout;
}


/**************************************************************************************/

//  Copy string strin to strout, replacing multiple substrings with replacement strings.
//  Multiple pairs of string arguments follow maxcc, a substring and a replacement string.
//  Last pair of string arguments must be followed by a null argument.
//  maxcc is the limit for strout. The actual cc is returned.
//  Replacement strings may be longer or shorter or have zero length.

int repl_Nstrs(ch *strin, ch *strout, int maxcc, ...)
{
   va_list     arglist;
   ch          *ssin, *ssout;
   ch          ftemp[2000];
   int         ftf, ccout;

   ftf = 1;
   ccout = 0;
   va_start(arglist,maxcc);

   while (true)
   {
      ssin = va_arg(arglist, ch *);
      if (! ssin) break;
      ssout = va_arg(arglist, ch *);

      if (ftf) {
         ftf = 0;
         ccout = repl_1str(strin,strout,maxcc,ssin,ssout);
      }

      else {
         if (ccout >= 2000) goto stop;
         strcpy(ftemp,strout);
         ccout = repl_1str(ftemp,strout,maxcc,ssin,ssout);
      }
   }

   stop:
   va_end(arglist);
   return ccout;
}


/**************************************************************************************/

//  Break up a long text string into lines no longer than cc2 chars.
//  If fake newlines ("\n") are found, replace them with real newlines.
//  Break unconditionally where newlines are found and remove them.
//  Break at last blank ch between cc1 and cc2 if present.
//  Break at last delimiter ch between cc1 and cc2 if present.
//  Break unconditionally at cc2 if none of the above.
//  Returns text lines in txout[*] with count as returned function value.
//  txout[*] are subjects for zfree().

int breakup_text(ch *txin0, ch **&txout, ch *delims, int cc1, int cc2)
{
   ch       *txin;
   uch      ch1;
   int      ccmax;
   int      p1, p2, cc3, Nout;
   int      Np, Bp, Sp;

   txin = zstrdup(txin0,"breakup_text");
   ccmax = strlen(txin);
   txout = (ch **) zmalloc(100 * sizeof(ch *),"breakup_text");                         //  100 line limit

   if (strstr(txin0,"\\n"))                                                            //  replace "\n" with real newline chars
      repl_1str(txin0,txin,ccmax,"\\n","\n");

   Nout = p1 = 0;

   while (true)
   {
      p2 = p1;                                                                         //  input line position
      cc3 = 0;                                                                         //  output line cc

      Np = Bp = Sp = 0;

      while (txin[p2])                                                                 //  scan further up to cc2 chars
      {
         ch1 = txin[p2];
         if (ch1 == '\n') { Np = p2; break; }                                          //  break out if newline found
         if (cc3 >= cc1) {
            if (ch1 == ' ') Bp = p2;                                                   //  remember last ' ' found after cc1 chars
            if (delims && strchr(delims,ch1)) Sp = p2;                                 //  remember last delimiter found after cc1
         }
         if (ch1 < 0)                                                                  //  UTF8 wide character
            while ((ch1 = txin[p2+1]) < 0xC0) p2++;
         p2++;
         cc3++;
         if (cc3 == cc2) break;
      }

      if (! cc3 && ! Np) break;                                                        //  nothing left
      if (Np) cc3 = Np - p1;                                                           //  newline found
      else {
         if (cc3 < cc2) Bp = Sp = 0;                                                   //  line fits cc2 limit
         if (Bp) cc3 = Bp - p1 + 1;                                                    //  break at previous ' '
         else if (Sp) cc3 = Sp - p1 + 1;                                               //  break at previous delimiter
         else cc3 = p2 - p1;
      }
      if (txin[p1] == ' ' && cc3) { p1++; cc3--; }                                     //  remove leading blank
      if (cc3 > 0) {                                                                   //  avoid blank line
         txout[Nout] = (ch *) zmalloc(cc3+1,"breakup_text");
         strncpy0(txout[Nout],txin+p1,cc3+1);
         if (++Nout == 100) break;
      }
      p2 = p1 + cc3;
      if (Np) p2++;
      p1 = p2;
   }

   zfree(txin);
   return Nout;
}


/**************************************************************************************/

//  Copy and convert string to hex string.
//  Each input character 'A' >> 3 output characters "41 "

void strncpyx(ch *out, ch *in, int ccin)
{
   int      ii, jj, c1, c2;
   ch       cx[] = "0123456789ABCDEF";

   if (! ccin) ccin = strlen(in);

   for (ii = 0, jj = 0; ii < ccin; ii++, jj += 3)
   {
      c1 = (uch) in[ii] >> 4;
      c2 = in[ii] & 15;
      out[jj] = cx[c1];
      out[jj+1] = cx[c2];
      out[jj+2] = ' ';
   }
   out[jj] = 0;
   return;
}


/**************************************************************************************/

//  Strip trailing zeros from ascii floating numbers
//    (e.g. 1.230000e+02  -->  1.23e+02)

void StripZeros(ch *pNum)
{
   int     ii, cc;
   int     pp, k1, k2;
   ch      work[20];

   cc = strlen(pNum);
   if (cc >= 20) return;

   for (ii = 0; ii < cc; ii++)
   {
      if (pNum[ii] == '.')
      {
         pp = ii;
         k1 = k2 = 0;
         for (++ii; ii < cc; ii++)
         {
            if (pNum[ii] == '0')
            {
               if (! k1) k1 = k2 = ii;
               else k2 = ii;
               continue;
            }

            if ((pNum[ii] >= '1') && (pNum[ii] <= '9'))
            {
               k1 = 0;
               continue;
            }

            break;
         }

         if (! k1) return;

         if (k1 == pp + 1) k1++;
         if (k2 < k1) return;
         strcpy(work,pNum);
         strcpy(work+k1,pNum+k2+1);
         strcpy(pNum,work);
         return;
      }
   }
}


/**************************************************************************************/

//  test for blank/null string
//  Returns status depending on input string:
//    0 not a blank or null string
//    1 argument string is NULL
//    2 string has zero length (*string == 0)
//    3 string is all blanks

int blank_null(ch *string)
{
   if (! string) return 1;                                                             //  null string
   if (! *string) return 2;                                                            //  zero length string
   int cc = strlen(string);
   for (int ii = 0; ii < cc; ii++)
      if (string[ii] != ' ') return 0;                                                 //  non-blank string
   return 3;                                                                           //  blank string
}


/**************************************************************************************/

//  clean \x escape sequences and replace them with the escaped character
//    \n >> newline  \" >> doublequote  \\ >> backslash   etc.
//  see  $ man ascii  for the complete list

int clean_escapes(ch *string)
{
   ch       *pp1 = string, *pp2 = string, *pp;
   ch       char1;
   ch       escapes[] = "abtnvfr";
   int      count = 0;

   while (true)
   {
      char1 = *pp1++;

      if (char1 == 0) {
         *pp2 = 0;
         return count;
      }

      else if (char1 == '\\')  {
         char1 = *pp1++;
         pp = strchr(escapes,char1);
         if (pp) char1 = pp - escapes + 7;
         count++;
      }

      *pp2++ = char1;
   }
}


/**************************************************************************************/

//  Compute the graphic character count for a UTF8 character string.
//  Depends on UTF8 rules:
//    - ascii characters are positive (0x00 to 0x7F)
//    - 1st ch of multich sequence is negative (0xC0 to 0xFD)
//    - subsequent multichars are in the range 0x80 to 0xBF

int utf8len(ch *utf8string)
{
   int      ii, cc;
   ch       xlimit = 0xC0;

   for (ii = cc = 0; utf8string[ii]; ii++)
   {
      if (utf8string[ii] < 0)                                                          //  multibyte character
         while (utf8string[ii+1] < xlimit) ii++;                                       //  skip extra bytes
      cc++;
   }

   return cc;
}


/**************************************************************************************/

//  Extract a UTF8 substring with a specified count of graphic characters.
//    utf8in     input UTF8 string
//    utf8out    output UTF8 string, which must be long enough
//    pos        initial graphic character position to get (0 = first)
//    cc         max. count of graphic characters to get
//    returns    number of graphic characters extracted, <= cc
//  Output string is null terminated after last extracted character.

int utf8substring(ch *utf8out, ch *utf8in, int pos, int cc)
{
   int      ii, jj, kk, posx, ccx;
   ch       xlimit = 0xC0;

   for (ii = posx = 0; posx < pos && utf8in[ii]; ii++)
   {
      if (utf8in[ii] < 0)
         while (utf8in[ii+1] < xlimit) ii++;
      posx++;
   }

   jj = ii;

   for (ccx = 0; ccx < cc && utf8in[jj]; jj++)
   {
      if (utf8in[jj] < 0)
         while (utf8in[jj+1] < xlimit) jj++;
      ccx++;
   }

   kk = jj - ii;

   strncpy(utf8out,utf8in+ii,kk);
   utf8out[kk] = 0;

   return   ccx;
}


/**************************************************************************************/

//  check a character string for valid utf8 encoding
//  returns:  0 = OK,  1 = bad string

int utf8_check(ch *string)
{
   ch       *pp;
   uch      ch1, ch2, nch;

   for (pp = string; *pp; pp++)
   {
      ch1 = *pp;
      if (ch1 < 0x7F) continue;
      if (ch1 > 0xBF && ch1 < 0xE0) nch = 1;
      else if (ch1 < 0xF0) nch = 2;
      else if (ch1 < 0xF8) nch = 3;
      else if (ch1 < 0xFC) nch = 4;
      else if (ch1 < 0xFE) nch = 5;
      else return 1;
      while (nch) {
         pp++;
         ch2 = *pp;
         if (ch2 < 0x80 || ch2 > 0xBF) return 1;
         nch--;
      }
   }

   return 0;
}


/**************************************************************************************/

//  check a character string for valid utf8 encoding
//  invalid characters are replaced with '?'
//  returns no. of characters replaced (0 if no errors found)

int utf8_clean(ch *string)
{
   int      badbytes = 0;
   ch       *pp1, *pp2;
   uch      ch1, ch2, nch;

   for (pp1 = string; *pp1; pp1++)
   {
      ch1 = *pp1;
      if (ch1 < 0x7F) continue;                                                        //  ascii, single byte

      if (ch1 > 0xBF && ch1 < 0xE0) nch = 1;                                           //  extra bytes
      else if (ch1 < 0xF0) nch = 2;
      else if (ch1 < 0xF8) nch = 3;
      else if (ch1 < 0xFC) nch = 4;
      else if (ch1 < 0xFE) nch = 5;
      else goto fix1;                                                                  //  >5 invalid

      pp2 = pp1;
      while (nch) {                                                                    //  check extra bytes
         pp1++;
         ch2 = *pp1;
         if (ch2 < 0x80 || ch2 > 0xBF) goto fix2;                                      //  invalid
         nch--;
      }
      continue;

   fix1:                                                                               //  replace bad byte with '?'
      *pp1 = '?';
      badbytes++;
      continue;

   fix2:
      pp1 = pp2;                                                                       //  back to initial byte
      *pp1 = '?';                                                                      //  replace with '?'
      badbytes++;
      continue;
   }

   return badbytes;
}


/**************************************************************************************/

//  Find the Nth graphic character position within a UTF8 string
//    utf8in      input UTF8 string
//    Nth         graphic character position, zero based
//  returns starting character (byte) position of Nth graphic character
//  returns -1 if Nth is beyond the string length

int utf8_position(ch *utf8in, int Nth)
{
   int      ii, posx;
   ch       xlimit = 0xC0;

   for (ii = posx = 0; posx < Nth && utf8in[ii]; ii++)
   {
      if (utf8in[ii] < 0)                                                              //  multi-byte character
         while (utf8in[ii+1] && utf8in[ii+1] < xlimit) ii++;                           //  traverse member bytes
      posx++;
   }

   if (utf8in[ii]) return ii;
   return -1;
}


/**************************************************************************************/

//  err = zsed(file, string1, string2 ... null)
//
//  replace string1/3/5... with string2/4/6... in designated file
//  returns    N  lines changed
//            -1  file not found
//            -2  other error (with message)

int zsed(ch *infile ...)
{
   int         err, ftf, nn;
   FILE        *fid1, *fid2;
   ch          *outfile, *pp;
   ch          buffin[1000], buffout[1000], buffxx[1000];
   ch          *stringin, *stringout;
   va_list     arglist;

   fid1 = fopen(infile,"r");
   if (! fid1) return -1;

   outfile = zstrdup(infile,"zsed",8);
   strcat(outfile,"-temp");
   fid2 = fopen(outfile,"w");
   if (! fid2) {
      printf("*** %s \n",strerror(errno));
      zfree(outfile);
      return -2;
   }

   nn = 0;

   while (true)
   {
      pp = fgets(buffin,500,fid1);
      if (! pp) break;

      va_start(arglist,infile);

      ftf = 1;

      while (true)
      {
         stringin = va_arg(arglist, ch *);
         if (! stringin) break;
         stringout = va_arg(arglist, ch *);
         if (! stringout) break;

         if (ftf) {
            ftf = 0;
            nn += repl_1str(buffin,buffout,1000,stringin,stringout);
         }
         else {
            strcpy(buffxx,buffout);
            nn += repl_1str(buffxx,buffout,1000,stringin,stringout);
         }
      }

      va_end(arglist);

      fputs(buffout,fid2);
   }

   fclose(fid1);
   err = fclose(fid2);
   if (err) {
      printf("*** %s \n",strerror(errno));
      zfree(outfile);
      return -2;
   }

   rename(outfile,infile);

   zfree(outfile);
   return nn;
}


/**************************************************************************************/

//  zstrstr() and zstrcasestr() work like strstr() and strcasestr()
//  but the needle string "" does NOT match any haystack string.

ch * zstrstr(ch *haystack, ch *needle)
{
   if (! needle || ! *needle) return 0;
   return strstr(haystack,needle);
}

ch * zstrcasestr(ch *haystack, ch *needle)
{
   if (! needle || ! *needle) return 0;
   return strcasestr(haystack,needle);
}


/**************************************************************************************/

//  strcpy() with overlap allowed

ch * zstrcpy(ch *dest, ch *source)
{
   int cc = strlen(source);
   memmove(dest,source,cc);
   dest[cc] = 0;
   return dest;
}


//  strncpy() with overlap allowed

ch * zstrncpy(ch *dest, ch *source, int cc)
{
   memmove(dest,source,cc);
   return dest;
}


/**************************************************************************************/

//  works like strcmp(), but compare is terminated by \n as well as null

int zstrcmp(ch *s1, ch *s2)
{
   ch       *p1, *p2;
   int      nn;

   p1 = (ch *) strchr(s1,'\n');
   p2 = (ch *) strchr(s2,'\n');
   if (p1) *p1 = 0;
   if (p2) *p2 = 0;
   nn = strcmp(s1,s2);
   if (p1) *p1 = '\n';
   if (p2) *p2 = '\n';
   return nn;
}


/**************************************************************************************/

//  works like strcmp(), but using char *args instead of const char*

int  zstrcmp2(ch *s1, ch *s2)
{
   return strcmp((cch *) s1, (cch *) s2);
}


/**************************************************************************************/

//  works like strcasecmp(), but using char *args instead of const char*

int  zstrcasecmp(ch *s1, ch *s2)
{
   return strcasecmp((cch *) s1, (cch *) s2);
}


/**************************************************************************************/

//  escape quote marks (" and ') in a string, for use in shell commands
//  returned file is subject for zfree()

ch * zescape_quotes(ch *str1)
{
   ch     *str2 = 0;
   int    cc;

   if (strchr(str1,'"') == 0) {
      str2 = zstrdup(str1,"zescape_quotes");
      return str2;
   }

   cc = strlen(str1);
   str2 = (ch *) zmalloc(cc+40,"zescape_quotes");
   repl_Nstrs(str1,str2,cc+40,"\"","\\\"","\'","\\\'",null);
   return str2;
}


/**************************************************************************************

   Conversion Utilities

   convSI(string, inum, delim)                     string to int
   convSI(string, inum, low, high, delim)          string to int with range check

   convSD(string, dnum, delim)                     string to double
   convSD(string, dnum, low, high, delim)          string to double with range check

   convSF(string, fnum, delim)                     string to float
   convSF(string, fnum, low, high, delim)          string to float with range check

   convIS(inum, string, cc)                        int to string with returned cc

   convDS(fnum, digits, string, cc)                double to string with specified
                                                     digits of precision and returned cc

   string      input (ch *) or output (ch *)
   inum        input (int) or output (int &)
   dnum        input (double) or output (double &)
   delim       optional returned delimiter (null or ch **)
   low, high   input range check (int or double)
   cc          output string length (int &)
   digits      input digits of precision (int) to be used for output string

   NOTE: decimal point may be comma or period.
         1000's separators must NOT be present.

   convIS and convDS also return the length cc of the string output.
   convDS accepts same formats as atof. Decimal point can be comma or period.
   convDS will use whatever format (f/e) gives the shortest result.
   Outputs like "e03" or "e+03" will be shortened to "e3".

   function status returned:
       0    normal conversion, no invalid digits, blank/null termination
       1    successful conversion, but trailing non-numeric found
       2    conversion OK, but outside specified limits
       3    null or blank string, converted to zero       (obsolete, now status 4)
       4    conversion error, invalid data in string
   overlapping statuses have following precedence: 4 3 2 1 0

***************************************************************************************/

//  Convert string to integer

int convSI(ch *string, int &inum, ch **delim)                                          //  use glib function
{
   ch       *ddelim = 0;
   int      err;

   inum = strtol(string,&ddelim,10);                                                   //  convert next characters
   if (delim) *delim = ddelim;
   if (ddelim == string) err = 4;                                                      //  no valid digits
   else if (*ddelim == '\0') err = 0;                                                  //  null delimiter
   else if (*ddelim == ' ') err = 0;                                                   //  blank delimiter
   else err = 1;                                                                       //  other delimiter
   return err;
}


int convSI(ch *string, int &inum, int lolim, int hilim, ch **delim)
{
   int stat = convSI(string,inum,delim);

   if (stat > 2) return stat;                                                          //  invalid or null/blank
   if (inum < lolim) return 2;                                                         //  return 2 if out of limits
   if (inum > hilim) return 2;                                                         //  (has precedence over status 1)
   return stat;                                                                        //  limits OK, return 0 or 1
}


//  Convert string to double   ***  status 3 --> status 4  ***

int convSD(ch *string, double &dnum, ch **delim)                                       //  use glib function
{
   ch       *ddelim = 0;
   int      err;

   dnum = strtod(string,&ddelim);
   if (delim) *delim = ddelim;
   if (ddelim == string) err = 4;                                                      //  no valid digits
   else if (*ddelim == '\0') err = 0;                                                  //  OK, null delimiter
   else if (*ddelim == ' ') err = 0;                                                   //  OK, blank delimiter
   else err = 1;                                                                       //  OK, other delimiter
   return err;
}


int convSD(ch *string, double &dnum, double lolim, double hilim, ch **delim)
{
   int stat = convSD(string,dnum,delim);

   if (stat > 2) return stat;                                                          //  invalid or null/blank
   if (dnum < lolim) return 2;                                                         //  return 2 if out of limits
   if (dnum > hilim) return 2;                                                         //  (has precedence over status 1)
   return stat;                                                                        //  limits OK, return 0 or 1
}


int convSF(ch *string, float &fnum, ch **delim)
{
   double   dnum;
   int      err;
   err = convSD(string,dnum,delim);
   fnum = dnum;
   return err;
}


int convSF(ch *string, float &fnum, float lolim, float hilim, ch **delim)
{
   double   dnum, dlolim = lolim, dhilim = hilim;
   int      err;
   err = convSD(string,dnum,dlolim,dhilim,delim);
   fnum = dnum;
   return err;
}


//  Convert int to string with returned length.
//  (will never exceed 12 characters)

int convIS(int inum, ch *string, int *cc)
{
   int   ccc;

   ccc = snprintf(string,12,"%d",inum);
   if (cc) *cc = ccc;
   return 0;
}


//  Convert double to string with specified digits of precision.
//  Shortest length format (f/e) will be used.
//  Output length is returned in optional argument cc.
//  (will never exceed 20 characters)

int convDS(double dnum, int digits, ch *string, int *cc)                               //  bugfix: use memmove not strcpy
{
   ch     *pstr;

   snprintf(string,20,"%.*g",digits,dnum);

   pstr = strstr(string,"e+");                                                         //  1.23e+12  >  1.23e12
   if (pstr) memmove(pstr+1,pstr+2,strlen(pstr+2)+1);

   pstr = strstr(string,"e0");                                                         //  1.23e02  >  1.23e2
   if (pstr) memmove(pstr+1,pstr+2,strlen(pstr+2)+1);

   pstr = strstr(string,"e0");
   if (pstr) memmove(pstr+1,pstr+2,strlen(pstr+2)+1);

   pstr = strstr(string,"e-0");                                                        //  1.23e-02  >  1.23e-2
   if (pstr) memmove(pstr+2,pstr+3,strlen(pstr+3)+1);

   pstr = strstr(string,"e-0");
   if (pstr) memmove(pstr+2,pstr+3,strlen(pstr+3)+1);

   if (cc) *cc = strlen(string);

   return 0;
}


/**************************************************************************************/

//  convert string to double, accepting either '.' or ',' decimal points.
//  if there is an error, zero is returned.

double atofz(ch *string)
{
   ch  string2[20], *pp;

   strncpy(string2,string,20);
   string2[19] = 0;
   pp = strchr(string2,',');
   if (pp) *pp = '.';
   return atof(string2);
}


//  format a number as "123 B" or "12.3 KB" or "1.23 MB" etc.
//  prec is the desired digits of precision to output.
//  WARNING: only the last 100 conversions remain available in memory.
//  Example formats for 3 digits of precision:
//    123 B,  999 B,  1.23 KB,  98.7 KB,  456 KB,  2.34 MB,  45.6 GB,  1.23 GB

ch * formatKBMB(double fnum, int prec)
{
   #define Bkilo 1024
   #define Bmega (Bkilo*Bkilo)
   #define Bgiga (Bkilo*Bkilo*Bkilo)

   ch            *units;
   static ch     *output[100];
   static int    ftf = 1, ii;
   double        gnum;

   if (ftf) {                                                                          //  keep last 100 conversions
      ftf = 0;
      for (ii = 0; ii < 100; ii++)
         output[ii] = (ch *) zmalloc(20,"formatKBMB");
   }

   gnum = fabs(fnum);

   if (gnum > Bgiga) {
      fnum = fnum / Bgiga;
      units = "GB";
   }
   else if (gnum > Bmega) {
      fnum = fnum / Bmega;
      units = "MB";
   }
   else if (gnum > Bkilo) {
      fnum = fnum / Bkilo;
      units = "KB";
   }
   else units = "B ";

   gnum = fabs(fnum);
   if (prec == 2 && gnum >= 99.5) prec++;                                              //  avoid e+nn formats
   if (prec == 3 && gnum >= 999.5) prec++;
   if (prec == 4 && gnum >= 9999.5) prec++;
   if (prec == 5 && gnum >= 99999.5) prec++;
   if (prec == 6 && gnum >= 999999.5) prec++;

   if (++ii > 99) ii = 0;
   snprintf(output[ii],20,"%.*g %s",prec,fnum,units);
   return output[ii];
}


//  Truncate the mantissa of a float number to a given number of bits.
//  e.g. 10 bits implies a precision of 1/1024 or 0.098 percent.

float Fround(float Fin, int bits)                                                      //  26.0
{
   uint Fin2;
   memcpy((ch *) &Fin2, (ch *) &Fin,4);
   uint Fin3 = Fin2 >> (23-bits);
   Fin3 = Fin3 << (23-bits);
   float Fout;
   memcpy((ch *) &Fout, (ch *) &Fin3,4);
   return Fout;
}
   

/**************************************************************************************

    Wildcard string match

    Match candidate string to wildcard string containing any number of
    '*' or '?' wildcard characters. '*' matches any number of characters,
    including zero characters. '?' matches any one character.
    Returns 0 if match, 1 if no match.                             <<---- WATCH OUT !

    Benchmark: 0.032 usec.       wild = *asdf*qwer?yxc
               3.3 GHz Core i5   match = XXXasdfXXXXqwerXyxc

***************************************************************************************/

int MatchWild(ch *pWild, ch *pString)
{
   int   ii, star;

new_segment:

   star = 0;
   while (pWild[0] == '*')
   {
      star = 1;
      pWild++;
   }

test_match:

   for (ii = 0; pWild[ii] && (pWild[ii] != '*'); ii++)
   {
      if (pWild[ii] != pString[ii])
      {
         if (! pString[ii]) return 1;
         if (pWild[ii] == '?') continue;
         if (! star) return 1;
         pString++;
         goto test_match;
      }
   }

   if (pWild[ii] == '*')
   {
      pString += ii;
      pWild += ii;
      goto new_segment;
   }

   if (! pString[ii]) return 0;
   if (ii && pWild[ii-1] == '*') return 0;
   if (! star) return 1;
   pString++;
   goto test_match;
}


/**************************************************************************************

    Wildcard string match - ignoring case
    Works like MatchWild() above, but case is ignored.

***/

int MatchWildCase(ch *pWild, ch *pString)
{
   int   ii, star;

new_segment:

   star = 0;
   while (pWild[0] == '*')
   {
      star = 1;
      pWild++;
   }

test_match:

   for (ii = 0; pWild[ii] && (pWild[ii] != '*'); ii++)
   {
      if (! strmatchcaseN(pWild+ii,pString+ii,1))                                      //  the only difference
      {
         if (! pString[ii]) return 1;
         if (pWild[ii] == '?') continue;
         if (! star) return 1;
         pString++;
         goto test_match;
      }
   }

   if (pWild[ii] == '*')
   {
      pString += ii;
      pWild += ii;
      goto new_segment;
   }

   if (! pString[ii]) return 0;
   if (ii && pWild[ii-1] == '*') return 0;
   if (! star) return 1;
   pString++;
   goto test_match;
}


/**************************************************************************************

   SearchWild  - wildcard file search

   Find all files with total /pathname/filename matching a pattern,
   which may have any number of the wildcard characters '*' and '?'
   in either or both the pathname and filename.

   ch * SearchWild(ch *wfilespec, int &flag)

   inputs:  flag = 1 to start a new search
            flag = 2 abort a running search
            *** do not modify flag within a search ***

            wfilespec = filespec to search with optional wildcards
               e.g. "/name1/na*me2/nam??e3/name4*.ext?"

   return:  a pointer to one matching file is returned per call,
            or null when there are no more matching files.

   The search may be aborted before completion, but make a final
   call with flag = 2 to clean up temp file. A new search with
   flag = 1 will also finish the cleanup.

   NOT THREAD SAFE - do not use in parallel threads

   '#' is used in place of '*' in comments below to prevent the
       compiler from interpreting /# and #/ as comment delimiters

   GNU find peculiarities:
     find /path/#      omits "." files
     find /path/       includes "." files
     find /path/#      recurses folders under /path/
     find /path/#.txt  does not recurse folders
     find /path/#/     finds all files under /path/
     find /path/#/#    finds files >= 1 folder level under /path/
     find /path/xxx#   never finds anything

   SearchWild uses simpler rules:
     '/' and '.' are treated like all other characters and match '#' and '?'
     no files are excluded except pure folders
     /path/#.txt finds all xxx.txt files under /path/ at all levels
     (because #.txt matches aaa.txt, /aaa/bbb.txt, etc.)

   Benchmark: search path: /usr/#   file: #.html   2 strings: #per prop#
              find 97 files from 209K files in /usr/#
              first time:  4.6 sec.
              second time:  1.5 sec.
              computer: 3.6 GHz core i7 with SSD disk

   Do not use to search files in /proc/# (causes infinite loop).

***/

ch * SearchWild(ch *wpath, int &uflag)
{
   static FILE    *fid = 0;
   static ch      buff[XFCC];
   static ch      wpath2[XFCC];
   static ch      command[XFCC];
   ch             *fcomm = "find \"%s\" -type f  2>/dev/null";
   int            cc, err;
   ch             *pp, *pp1, *pp2;

   if ((uflag == 1) || (uflag == 2)) {                                                 //  first call or stop flag
      if (fid) pclose(fid);                                                            //  if file open, close it
      fid = 0;
      if (uflag == 2) return 0;                                                        //  stop flag, done
   }

   if (uflag == 1)                                                                     //  first call flag
   {
      cc = strlen(wpath);
      if (cc == 0) return 0;
      if (cc > XFCC-20) zappcrash("SearchWild: wpath > XFCC");

      repl_Nstrs(wpath,wpath2,XFCC,"\"","\\\"","$","\\$",null);                        //  escape " and $ chars. in match pattern

      pp1 = strchr(wpath2,'*');                                                        //  find last wildcard in match pattern
      pp2 = strchr(wpath2,'?');
      pp = 0;
      if (pp1) {
         pp = pp1;
         if (pp2 && pp2 < pp1) pp = pp2;
      }
      else if (pp2) pp = pp2;
      if (pp) *pp = 0;                                                                 //  terminate at first wildcard

      pp = strrchr(wpath2,'/');                                                        //  find last '/' in match pattern
      if (pp) pp[1] = 0;                                                               //  terminate after last '/'

      pp = zescape_quotes(wpath2);
      snprintf(command,XFCC,fcomm,pp);                                                 //  result is input to find command
      zfree(pp);

      fid = popen(command,"r");                                                        //  start find command, get matching files
      if (! fid) zappcrash(strerror(errno));

      uflag = 763568954;                                                               //  begin search
   }

   if (uflag != 763568954) zappcrash("SearchWild, uflag invalid");

   while (true)
   {
      pp = fgets(buff,XFCC-2,fid);                                                     //  next matching file
      if (! pp) {
         pclose(fid);                                                                  //  no more
         fid = 0;
         return 0;
      }

      cc = strlen(pp);                                                                 //  get rid of trailing \n
      pp[cc-1] = 0;

      err = MatchWild(wpath,pp);                                                       //  wildcard match?
      if (err) continue;                                                               //  no

      return pp;                                                                       //  return file
   }
}


/**************************************************************************************

    SearchWildCase  - wildcard file search - ignoring case
    Works like SearchWild() above, but case of file name is ignored.

    Actually, the trailing part of the path name is also case-insensitive,
    meaning that it is possible to get more matches than technically correct
    if folders like this are present:
       /AAA/BBB/.../filename
       /AAA/bbb/.../filename

***/

ch * SearchWildCase(ch *wpath, int &uflag)
{
   static FILE    *fid = 0;
   static ch      buff[XFCC];
   static ch      wpath2[XFCC];
   static ch      command[XFCC];
   ch             *fcomm = "find \"%s\" -type f  2>/dev/null";
   int            cc, err;
   ch             *pp, *pp1, *pp2;

   if ((uflag == 1) || (uflag == 2)) {                                                 //  first call or stop flag
      if (fid) pclose(fid);                                                            //  if file open, close it
      fid = 0;
      if (uflag == 2) return 0;                                                        //  stop flag, done
   }

   if (uflag == 1)                                                                     //  first call flag
   {
      cc = strlen(wpath);
      if (cc == 0) return 0;
      if (cc > XFCC-20) zappcrash("SearchWild: wpath > XFCC");

      repl_Nstrs(wpath,wpath2,XFCC,"\"","\\\"","$","\\$",null);                        //  escape " and $ chars. in match pattern

      pp1 = strchr(wpath2,'*');                                                        //  find last wildcard in match pattern
      pp2 = strchr(wpath2,'?');
      pp = 0;
      if (pp1) {
         pp = pp1;
         if (pp2 && pp2 < pp1) pp = pp2;
      }
      else if (pp2) pp = pp2;
      if (pp) *pp = 0;                                                                 //  terminate at first wildcard

      pp = strrchr(wpath2,'/');                                                        //  find last '/' in match pattern
      if (pp) pp[1] = 0;                                                               //  terminate after last '/'

      pp = zescape_quotes(wpath2);
      snprintf(command,XFCC,fcomm,pp);                                                 //  result is input to find command
      zfree(pp);

      fid = popen(command,"r");                                                        //  start find command, get matching files
      if (! fid) zappcrash(strerror(errno));

      uflag = 763568954;                                                               //  begin search
   }

   if (uflag != 763568954) zappcrash("SearchWild, uflag invalid");

   while (true)
   {
      pp = fgets(buff,XFCC-2,fid);                                                     //  next matching file
      if (! pp) {
         pclose(fid);                                                                  //  no more
         fid = 0;
         return 0;
      }

      cc = strlen(pp);                                                                 //  get rid of trailing \n
      pp[cc-1] = 0;

      err = MatchWildCase(wpath,pp);                                                   //  wildcard match? (ignore case)
      if (err) continue;                                                               //  no

      return pp;                                                                       //  return file
   }
}


/**************************************************************************************

   Find all files matching a given pattern (using glob() rules)

   int zfind(ch *pattern, ch **&flist, int &NF)

      pattern     pattern to match, with wildcards
      flist       list of files returned
      NF          count of files returned

   Returns 0 if OK, +N if error (errno is set).
   flist and flist[*] are subjects for zfree().

   zfind() works for files containing quotes (")
   dotfiles (/. and /..) are not included

***************************************************************************************/

int zfind(ch *pattern, ch **&flist, int &NF)
{
   ch       **zfind_filelist = 0;                                                      //  list of filespecs returned
   int      globflags = GLOB_PERIOD;                                                   //  include dotfiles
   int      ii, jj, err, cc;
   glob_t   globdata;
   ch       *pp;

   globdata.gl_pathc = 0;                                                              //  glob() setup
   globdata.gl_offs = 0;
   globdata.gl_pathc = 0;

   NF = 0;                                                                             //  empty output
   flist = 0;

   err = glob(pattern,globflags,null,&globdata);                                       //  find all matching files

   if (err) {
      if (err == GLOB_NOMATCH) err = 0;
      else if (err == GLOB_ABORTED) err = 1;
      else if (err == GLOB_NOSPACE) err = 2;
      else err = 3;
      if (err) printf("*** zfind() error: %d \n",err);
      globfree(&globdata);                                                             //  free glob memory
      return err;
   }

   NF = globdata.gl_pathc;
   if (! NF) {
      globfree(&globdata);
      return 0;
   }

   cc = NF * sizeof(ch *);
   zfind_filelist = (ch **) zmalloc(cc,"zfind");

   for (ii = jj = 0; ii < NF; ii++) {                                                  //  loop found files
      pp = strrchr(globdata.gl_pathv[ii],'/');
      if (! pp) continue;
      if (strmatch(pp,"/.")) continue;                                                 //  skip dotfiles
      if (strmatch(pp,"/..")) continue;
      zfind_filelist[jj++] = zstrdup(globdata.gl_pathv[ii],"zfind");                   //  add file to output list
   }

   flist = zfind_filelist;                                                             //  return file list and count
   NF = jj;

   globfree(&globdata);                                                                //  free glob memory
   return 0;
}


/**************************************************************************************/

//  perform a binary search on sorted list of integers
//  return matching element or -1 if not found
//  Benchmark: search a list of 10 million sorted integers
//             0.35 usecs.  3.3 GHz Core i5

int bsearch(int seekint, int nn, int list[])
{
   int      ii, jj, kk, rkk;

   ii = nn / 2;                                                                        //  next element to search
   jj = (ii + 1) / 2;                                                                  //  next increment
   nn--;                                                                               //  last element
   rkk = 0;

   while (true)
   {
      kk = list[ii] - seekint;                                                         //  check element

      if (kk > 0)
      {
         ii -= jj;                                                                     //  too high, go down
         if (ii < 0) return -1;
      }

      else if (kk < 0)
      {
         ii += jj;                                                                     //  too low, go up
         if (ii > nn) return -1;
      }

      else if (kk == 0) return ii;                                                     //  matched

      jj = jj / 2;                                                                     //  reduce increment

      if (jj == 0)
      {
         jj = 1;                                                                       //  step by 1 element
         if (! rkk) rkk = kk;                                                          //  save direction
         else
         {
            if (rkk > 0) { if (kk < 0) return -1; }                                    //  if change direction, fail
            else if (kk > 0) return -1;
         }
      }
   }
}


//  Perform a binary search on sorted set of records in memory.
//  Return matching record number (0 based) or -1 if not found.
//  Benchmark: search 10 million sorted records of 20 chars.
//             0.61 usecs.  3.3 GHz Core i5

int bsearch(ch *seekrec, ch *allrecs, int recl, int nrecs)
{
   int      ii, jj, kk, rkk;

   ii = nrecs / 2;                                                                     //  next element to search
   jj = (ii + 1) / 2;                                                                  //  next increment
   nrecs--;                                                                            //  last element
   rkk = 0;

   while (true)
   {
      kk = strcmp(allrecs+ii*recl,seekrec);                                            //  compare member rec to seek rec

      if (kk > 0)
      {
         ii -= jj;                                                                     //  too high, go down in set
         if (ii < 0) return -1;
      }

      else if (kk < 0)
      {
         ii += jj;                                                                     //  too low, go up in set
         if (ii > nrecs) return -1;
      }

      else if (kk == 0) return ii;                                                     //  matched

      jj = jj / 2;                                                                     //  reduce increment

      if (jj == 0)
      {
         jj = 1;                                                                       //  step by 1 element
         if (! rkk) rkk = kk;                                                          //  save direction
         else
         {
            if (rkk > 0) { if (kk < 0) return -1; }                                    //  if change direction, fail
            else if (kk > 0) return -1;
         }
      }
   }
}


//  Perform a binary search on sorted set of pointers to records in memory.
//  Return matching record number (0 based) or -1 if not found.
//  The pointers are sorted in the order of the records starting at ch N.
//  The records need not be sorted.
//  The string length of seekrec is compared.

int bsearch(ch *seekrec, ch **allrecs, int N, int nrecs)
{
   int      ii, jj, kk, rkk;

   ii = nrecs / 2;                                                                     //  next element to search
   jj = (ii + 1) / 2;                                                                  //  next increment
   nrecs--;                                                                            //  last element
   rkk = 0;

   while (true)
   {
      kk = strcmp(allrecs[ii]+N,seekrec);                                              //  compare member rec to seek rec

      if (kk > 0)
      {
         ii -= jj;                                                                     //  too high, go down in set
         if (ii < 0) return -1;
      }

      else if (kk < 0)
      {
         ii += jj;                                                                     //  too low, go up in set
         if (ii > nrecs) return -1;
      }

      else if (kk == 0) return ii;                                                     //  matched

      jj = jj / 2;                                                                     //  reduce increment

      if (jj == 0)
      {
         jj = 1;                                                                       //  step by 1 element
         if (! rkk) rkk = kk;                                                          //  save direction
         else
         {
            if (rkk > 0) { if (kk < 0) return -1; }                                    //  if change direction, fail
            else if (kk > 0) return -1;
         }
      }
   }
}


/**************************************************************************************
   heap sort functions

   void HeapSort(int list[], int nn)
   void HeapSort(float flist[], int nn)
   void HeapSort(double dlist[], int nn)
      Sort list of nn integers, floats, or doubles.
      Numbers are sorted in ascending order.
      Uses 4 parallel threads for lists > 1000.

   void HeapSort(ch *plist[], int nn)
      Pointers are sorted in order of the strings they point to.
      The strings are not changed.

   void HeapSort(ch *plist1[], ch *plist2[], int nn)
      Sort two lists of pointers to two sets of strings.
      Both lists are sorted in order of the first set of strings.

   void HeapSort(ch *plist[], int nn, compfunc)
      Sort list of pointers to strings in user-defined order.
      Pointers are sorted, strings are not changed.
      Uses 4 parallel threads for lists > 1000.

   void HeapSort(ch *recs, int RL, int NR, compfunc)
      Sort an array of records in memory using a caller-supplied compare function.
         recs  pointer to 1st record in array
         RL    record length
         NR    no. of records

   int compfunc(ch *rec1, ch *rec2)
      compare rec1 to rec2, return -1 0 +1  if rec1 < = > rec2  in sort order.

   Benchmarks (4 GHz Core i5)
      100 million integers: 3.9 secs
      100 million floats: 5.2 secs
      100 million doubles: 6.1 secs
      52 million pointers to 20 character strings: 18.2 secs
      20 million records of 100 characters: 20.2 secs

***************************************************************************************/

#define SWAP(x,y) (temp = (x), (x) = (y), (y) = temp)

// ------------------------------------------------------------------------------

//  heapsort for array of integers

void adjust(int vv[], int n1, int n2)
{
   int   *bb, jj, kk, temp;

   bb = vv - 1;
   jj = n1;
   kk = n1 * 2;

   while (kk <= n2)
   {
      if (kk < n2 && bb[kk] < bb[kk+1]) kk++;
      if (bb[jj] < bb[kk]) SWAP(bb[jj],bb[kk]);
      jj = kk;
      kk *= 2;
   }
}

void heapsort(int vv[], int nn)                                                        //  single thread version
{
   int   *bb, jj, temp;

   for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn);

   bb = vv - 1;

   for (jj = nn-1; jj > 0; jj--)
   {
      SWAP(bb[1], bb[jj+1]);
      adjust(vv,1,jj);
   }
}

namespace HeapSort_int
{
   int         nn1, nn2, nn3, nn4;
   int         *vv1, *vv2, *vv3, *vv4;
   pthread_t   tid1, tid2, tid3, tid4;
   int         tt1 = 1, tt2 = 2, tt3 = 3, tt4 = 4;
}

void HeapSort(int vv[], int nn)                                                        //  4-thread version
{
   using namespace HeapSort_int;

   void * HeapSort_int_thread(void *arg);

   int   *vv9, *next;

   if (nn < 1000 || get_nprocs() < 2) {                                                //  small list or <2 SMPs
      heapsort(vv,nn);                                                                 //  use one thread
      return;
   }

   nn1 = nn2 = nn3 = nn / 4;                                                           //  1st/2nd/3rd sub-list counts
   nn4 = nn - nn1 - nn2 - nn3;                                                         //  4th sub-list count

   vv1 = vv;                                                                           //  4 sub-list start positions
   vv2 = vv1 + nn1;
   vv3 = vv2 + nn2;
   vv4 = vv3 + nn3;

   tid1 = start_Jthread(HeapSort_int_thread,&tt1);                                     //  sort the 4 sub-lists, parallel
   tid2 = start_Jthread(HeapSort_int_thread,&tt2);
   tid3 = start_Jthread(HeapSort_int_thread,&tt3);
   tid4 = start_Jthread(HeapSort_int_thread,&tt4);

   wait_Jthread(tid1);                                                                 //  wait for 4 thread completions
   wait_Jthread(tid2);
   wait_Jthread(tid3);
   wait_Jthread(tid4);

   vv9 = (int *) malloc(nn * sizeof(int));                                             //  merge list, output list

   while (true)
   {
      next = 0;
      if (vv1) next = vv1;
      if (! next && vv2) next = vv2;
      if (! next && vv3) next = vv3;
      if (! next && vv4) next = vv4;
      if (! next) break;

      if (vv2 && *vv2 < *next) next = vv2;
      if (vv3 && *vv3 < *next) next = vv3;
      if (vv4 && *vv4 < *next) next = vv4;

      if (next == vv1) {
         vv1++;
         nn1--;
         if (! nn1) vv1 = 0;
      }

      else if (next == vv2) {
         vv2++;
         nn2--;
         if (! nn2) vv2 = 0;
      }

      else if (next == vv3) {
         vv3++;
         nn3--;
         if (! nn3) vv3 = 0;
      }

      else {
         vv4++;
         nn4--;
         if (! nn4) vv4 = 0;
      }

      *vv9 = *next;
      vv9++;
   }

   vv9 -= nn;
   memcpy(vv,vv9,nn * sizeof(int));                                                    //  copy output list to input list
   free(vv9);                                                                          //  free output list

   return;
}

void * HeapSort_int_thread(void *arg)                                                  //  thread function
{
   using namespace HeapSort_int;

   int tt = *((int *) arg);
   if (tt == 1) heapsort(vv1,nn1);
   if (tt == 2) heapsort(vv2,nn2);
   if (tt == 3) heapsort(vv3,nn3);
   if (tt == 4) heapsort(vv4,nn4);

   return 0;
}


// ------------------------------------------------------------------------------

//  heapsort for array of floats

void adjust(float vv[], int n1, int n2)
{
   float    *bb, temp;
   int      jj, kk;

   bb = vv - 1;
   jj = n1;
   kk = n1 * 2;

   while (kk <= n2)
   {
      if (kk < n2 && bb[kk] < bb[kk+1]) kk++;
      if (bb[jj] < bb[kk]) SWAP(bb[jj],bb[kk]);
      jj = kk;
      kk *= 2;
   }
}

void heapsort(float vv[], int nn)                                                      //  single thread version
{
   float    *bb, temp;
   int      jj;

   for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn);

   bb = vv - 1;

   for (jj = nn-1; jj > 0; jj--)
   {
      SWAP(bb[1], bb[jj+1]);
      adjust(vv,1,jj);
   }
}

namespace HeapSort_float
{
   int         nn1, nn2, nn3, nn4;
   float       *vv1, *vv2, *vv3, *vv4;
   pthread_t   tid1, tid2, tid3, tid4;
   int         tt1 = 1, tt2 = 2, tt3 = 3, tt4 = 4;
}

void HeapSort(float vv[], int nn)                                                      //  4-thread version
{
   using namespace HeapSort_float;

   void * HeapSort_float_thread(void *arg);

   float    *vv9, *next;

   if (nn < 1000 || get_nprocs() < 2) {                                                //  small list or <2 SMPs
      heapsort(vv,nn);                                                                 //  use one thread
      return;
   }

   nn1 = nn2 = nn3 = nn / 4;                                                           //  1st/2nd/3rd sub-list counts
   nn4 = nn - nn1 - nn2 - nn3;                                                         //  4th sub-list count

   vv1 = vv;                                                                           //  4 sub-list start positions
   vv2 = vv1 + nn1;
   vv3 = vv2 + nn2;
   vv4 = vv3 + nn3;

   tid1 = start_Jthread(HeapSort_float_thread,&tt1);                                   //  sort the 4 sub-lists, parallel
   tid2 = start_Jthread(HeapSort_float_thread,&tt2);
   tid3 = start_Jthread(HeapSort_float_thread,&tt3);
   tid4 = start_Jthread(HeapSort_float_thread,&tt4);

   wait_Jthread(tid1);                                                                 //  wait for 4 thread completions
   wait_Jthread(tid2);
   wait_Jthread(tid3);
   wait_Jthread(tid4);

   vv9 = (float *) malloc(nn * sizeof(float));                                         //  merge list, output list

   while (true)
   {
      next = 0;
      if (vv1) next = vv1;
      if (! next && vv2) next = vv2;
      if (! next && vv3) next = vv3;
      if (! next && vv4) next = vv4;
      if (! next) break;

      if (vv2 && *vv2 < *next) next = vv2;
      if (vv3 && *vv3 < *next) next = vv3;
      if (vv4 && *vv4 < *next) next = vv4;

      if (next == vv1) {
         vv1++;
         nn1--;
         if (! nn1) vv1 = 0;
      }

      else if (next == vv2) {
         vv2++;
         nn2--;
         if (! nn2) vv2 = 0;
      }

      else if (next == vv3) {
         vv3++;
         nn3--;
         if (! nn3) vv3 = 0;
      }

      else {
         vv4++;
         nn4--;
         if (! nn4) vv4 = 0;
      }

      *vv9 = *next;
      vv9++;
   }

   vv9 -= nn;
   memcpy(vv,vv9,nn * sizeof(float));                                                  //  copy output list to input list
   free(vv9);                                                                          //  free output list

   return;
}

void * HeapSort_float_thread(void *arg)                                                //  thread function
{
   using namespace HeapSort_float;

   int tt = *((int *) arg);
   if (tt == 1) heapsort(vv1,nn1);
   if (tt == 2) heapsort(vv2,nn2);
   if (tt == 3) heapsort(vv3,nn3);
   if (tt == 4) heapsort(vv4,nn4);

   return 0;
}


// ------------------------------------------------------------------------------

//  heapsort for array of doubles

void adjust(double vv[], int n1, int n2)
{
   double   *bb, temp;
   int      jj, kk;

   bb = vv - 1;
   jj = n1;
   kk = n1 * 2;

   while (kk <= n2)
   {
      if (kk < n2 && bb[kk] < bb[kk+1]) kk++;
      if (bb[jj] < bb[kk]) SWAP(bb[jj],bb[kk]);
      jj = kk;
      kk *= 2;
   }
}

void heapsort(double vv[], int nn)                                                     //  single thread version
{
   double   *bb, temp;
   int      jj;

   for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn);

   bb = vv - 1;

   for (jj = nn-1; jj > 0; jj--)
   {
      SWAP(bb[1], bb[jj+1]);
      adjust(vv,1,jj);
   }
}

namespace HeapSort_double
{
   int         nn1, nn2, nn3, nn4;
   double      *vv1, *vv2, *vv3, *vv4;
   pthread_t   tid1, tid2, tid3, tid4;
   int         tt1 = 1, tt2 = 2, tt3 = 3, tt4 = 4;
}

void HeapSort(double vv[], int nn)                                                     //  4-thread version
{
   using namespace HeapSort_double;

   void * HeapSort_double_thread(void *arg);

   double   *vv9, *next;

   if (nn < 1000 || get_nprocs() < 2) {                                                //  small list or <2 SMPs
      heapsort(vv,nn);                                                                 //  use one thread
      return;
   }

   nn1 = nn2 = nn3 = nn / 4;                                                           //  1st/2nd/3rd sub-list counts
   nn4 = nn - nn1 - nn2 - nn3;                                                         //  4th sub-list count

   vv1 = vv;                                                                           //  4 sub-list start positions
   vv2 = vv1 + nn1;
   vv3 = vv2 + nn2;
   vv4 = vv3 + nn3;

   tid1 = start_Jthread(HeapSort_double_thread,&tt1);                                  //  sort the 4 sub-lists, parallel
   tid2 = start_Jthread(HeapSort_double_thread,&tt2);
   tid3 = start_Jthread(HeapSort_double_thread,&tt3);
   tid4 = start_Jthread(HeapSort_double_thread,&tt4);

   wait_Jthread(tid1);                                                                 //  wait for 4 thread completions
   wait_Jthread(tid2);
   wait_Jthread(tid3);
   wait_Jthread(tid4);

   vv9 = (double *) malloc(nn * sizeof(double));                                       //  merge list, output list

   while (true)
   {
      next = 0;
      if (vv1) next = vv1;
      if (! next && vv2) next = vv2;
      if (! next && vv3) next = vv3;
      if (! next && vv4) next = vv4;
      if (! next) break;

      if (vv2 && *vv2 < *next) next = vv2;
      if (vv3 && *vv3 < *next) next = vv3;
      if (vv4 && *vv4 < *next) next = vv4;

      if (next == vv1) {
         vv1++;
         nn1--;
         if (! nn1) vv1 = 0;
      }

      else if (next == vv2) {
         vv2++;
         nn2--;
         if (! nn2) vv2 = 0;
      }

      else if (next == vv3) {
         vv3++;
         nn3--;
         if (! nn3) vv3 = 0;
      }

      else {
         vv4++;
         nn4--;
         if (! nn4) vv4 = 0;
      }

      *vv9 = *next;
      vv9++;
   }

   vv9 -= nn;
   memcpy(vv,vv9,nn * sizeof(double));                                                 //  copy output list to input list
   free(vv9);                                                                          //  free output list

   return;
}

void * HeapSort_double_thread(void *arg)                                               //  thread function
{
   using namespace HeapSort_double;

   int tt = *((int *) arg);
   if (tt == 1) heapsort(vv1,nn1);
   if (tt == 2) heapsort(vv2,nn2);
   if (tt == 3) heapsort(vv3,nn3);
   if (tt == 4) heapsort(vv4,nn4);

   return 0;
}


// ------------------------------------------------------------------------------

//  heapsort array of pointers to strings in ascending order of strings
//  pointers are sorted, strings are not changed.

void HeapSort(ch *vv[], int nn)
{
   HeapSort(vv,nn, (HeapSortUcomp *) strcmp);
   return;
}


// ------------------------------------------------------------------------------

//  Heapsort 2 lists of pointers to 2 parallel sets of strings
//    in ascending order of the first set of strings.
//  Both lists of pointers are sorted together in tandem.
//  Pointers are sorted, strings are not changed.

void adjust(ch *vv1[], ch *vv2[], int n1, int n2)
{
   ch       **bb1, **bb2, *temp;
   int      jj, kk;

   bb1 = vv1 - 1;
   bb2 = vv2 - 1;
   jj = n1;
   kk = n1 * 2;

   while (kk <= n2)
   {
      if (kk < n2 && strcmp(bb1[kk],bb1[kk+1]) < 0) kk++;
      if (strcmp(bb1[jj],bb1[kk]) < 0) {
         SWAP(bb1[jj],bb1[kk]);
         SWAP(bb2[jj],bb2[kk]);
      }
      jj = kk;
      kk *= 2;
   }
}

void HeapSort(ch *vv1[], ch *vv2[], int nn)
{
   ch       **bb1, **bb2, *temp;
   int      jj;

   for (jj = nn/2; jj > 0; jj--) adjust(vv1,vv2,jj,nn);

   bb1 = vv1;
   bb2 = vv2;

   for (jj = nn-1; jj > 0; jj--)
   {
      SWAP(bb1[0], bb1[jj]);
      SWAP(bb2[0], bb2[jj]);
      adjust(vv1,vv2,1,jj);
   }
}


// ------------------------------------------------------------------------------

//  heapsort array of pointers to strings in user-defined order.
//  pointers are sorted, strings are not changed.

void adjust(ch *vv[], int n1, int n2, HeapSortUcomp fcomp)
{
   ch       **bb, *temp;
   int      jj, kk;

   bb = vv - 1;
   jj = n1;
   kk = n1 * 2;

   while (kk <= n2)
   {
      if (kk < n2 && fcomp(bb[kk],bb[kk+1]) < 0) kk++;
      if (fcomp(bb[jj],bb[kk]) < 0) SWAP(bb[jj],bb[kk]);
      jj = kk;
      kk *= 2;
   }
}

void heapsort(ch *vv[], int nn, HeapSortUcomp fcomp)                                   //  single thread version
{
   ch       **bb, *temp;
   int      jj;

   for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn,fcomp);

   bb = vv;

   for (jj = nn-1; jj > 0; jj--)
   {
      SWAP(bb[0], bb[jj]);
      adjust(vv,1,jj,fcomp);
   }
}

namespace HeapSort_string
{
   int         nn1, nn2, nn3, nn4;
   ch          **vv1, **vv2, **vv3, **vv4;
   pthread_t   tid1, tid2, tid3, tid4;
   int         tt1 = 1, tt2 = 2, tt3 = 3, tt4 = 4;
   HeapSortUcomp  *ttfcomp;
}

void HeapSort(ch *vv[], int nn, HeapSortUcomp fcomp)                                   //  4-thread version
{
   using namespace HeapSort_string;

   void * HeapSort_string_thread(void *arg);

   ch     **vv9, **next;

   if (nn < 1000 || get_nprocs() < 2) {                                                //  small list or <2 SMPs
      heapsort(vv,nn,fcomp);                                                           //  use one thread
      return;
   }

   nn1 = nn2 = nn3 = nn / 4;                                                           //  1st/2nd/3rd sub-list counts
   nn4 = nn - nn1 - nn2 - nn3;                                                         //  4th sub-list count

   vv1 = vv;                                                                           //  4 sub-list start positions
   vv2 = vv1 + nn1;
   vv3 = vv2 + nn2;
   vv4 = vv3 + nn3;

   ttfcomp = fcomp;

   tid1 = start_Jthread(HeapSort_string_thread,&tt1);                                  //  sort the 4 sub-lists, parallel
   tid2 = start_Jthread(HeapSort_string_thread,&tt2);
   tid3 = start_Jthread(HeapSort_string_thread,&tt3);
   tid4 = start_Jthread(HeapSort_string_thread,&tt4);

   wait_Jthread(tid1);                                                                 //  wait for 4 thread completions
   wait_Jthread(tid2);
   wait_Jthread(tid3);
   wait_Jthread(tid4);

   vv9 = (ch **) malloc(nn * sizeof(ch *));                                            //  merge list, output list

   while (true)
   {
      next = 0;
      if (vv1) next = vv1;
      if (! next && vv2) next = vv2;
      if (! next && vv3) next = vv3;
      if (! next && vv4) next = vv4;
      if (! next) break;

      if (vv2 && ttfcomp(*vv2,*next) < 0) next = vv2;
      if (vv3 && ttfcomp(*vv3,*next) < 0) next = vv3;
      if (vv4 && ttfcomp(*vv4,*next) < 0) next = vv4;

      if (next == vv1) {
         vv1++;
         nn1--;
         if (! nn1) vv1 = 0;
      }

      else if (next == vv2) {
         vv2++;
         nn2--;
         if (! nn2) vv2 = 0;
      }

      else if (next == vv3) {
         vv3++;
         nn3--;
         if (! nn3) vv3 = 0;
      }

      else {
         vv4++;
         nn4--;
         if (! nn4) vv4 = 0;
      }

      *vv9 = *next;
      vv9++;
   }

   vv9 -= nn;
   memcpy(vv,vv9,nn * sizeof(ch *));                                                   //  copy output list to input list
   free(vv9);                                                                          //  free output list

   return;
}

void * HeapSort_string_thread(void *arg)                                               //  thread function
{
   using namespace HeapSort_string;

   int tt = *((int *) arg);
   if (tt == 1) heapsort(vv1,nn1,ttfcomp);
   if (tt == 2) heapsort(vv2,nn2,ttfcomp);
   if (tt == 3) heapsort(vv3,nn3,ttfcomp);
   if (tt == 4) heapsort(vv4,nn4,ttfcomp);

   return 0;
}


// ------------------------------------------------------------------------------

//  heapsort for array of records,
//  using caller-supplied record compare function.
//  HeapSortUcomp returns [ -1 0 +1 ]  for  rec1 [ < = > ] rec2
//  method: build array of pointers and sort these, then
//  use this sorted array to re-order the records at the end.

void adjust(ch *recs, int RL, int n1, int n2, int *vv1, HeapSortUcomp fcomp)
{
   int      *bb, jj, kk, temp;
   ch       *rec1, *rec2;

   bb = vv1 - 1;
   jj = n1;
   kk = n1 * 2;

   while (kk <= n2)
   {
      rec1 = recs + RL * bb[kk];
      rec2 = recs + RL * bb[kk+1];
      if (kk < n2 && fcomp(rec1,rec2) < 0) kk++;
      rec1 = recs + RL * bb[jj];
      rec2 = recs + RL * bb[kk];
      if (fcomp(rec1,rec2) < 0) SWAP(bb[jj],bb[kk]);
      jj = kk;
      kk *= 2;
   }
}

void HeapSort(ch *recs, int RL, int NR, HeapSortUcomp fcomp)
{
   int      *bb, jj, kk, temp, flag;
   int      *vv1, *vv2;                                                                //  make reentrant
   ch       *vvrec;

   vv1 = (int *) malloc((NR+1) * sizeof(int));
   for (jj = 0; jj < NR; jj++) vv1[jj] = jj;

   for (jj = NR/2; jj > 0; jj--) adjust(recs,RL,jj,NR,vv1,fcomp);

   bb = vv1 - 1;

   for (jj = NR-1; jj > 0; jj--)
   {
      SWAP(bb[1], bb[jj+1]);
      adjust(recs,RL,1,jj,vv1,fcomp);
   }

   vv2 = (int *) malloc((NR+1) * sizeof(int));
   for (jj = 0; jj < NR; jj++) vv2[vv1[jj]] = jj;

   vvrec = (ch *) malloc(RL);
   flag = 1;
   while (flag)
   {
      flag = 0;
      for (jj = 0; jj < NR; jj++)
      {
         kk = vv2[jj];
         if (kk == jj) continue;
         memmove(vvrec,recs+jj*RL,RL);
         memmove(recs+jj*RL,recs+kk*RL,RL);
         memmove(recs+kk*RL,vvrec,RL);
         SWAP(vv2[jj],vv2[kk]);
         flag = 1;
      }
   }

   free(vv1);
   free(vv2);
   free(vvrec);
}


/**************************************************************************************

   int MemSort (ch *RECS, int RL, int NR, int KEYS[][3], int NK)

   RECS is an array of records, to be sorted in-place.
   (record length = RL, record count = NR)

   KEYS[NK][3]  is an integer array defined as follows:
        [N][0]    starting position of Nth key field in RECS
        [N][1]    length of Nth key field in RECS
        [N][2]    type of sort for Nth key:
                    1 = ch ascending
                    2 = ch descending
                    3 = int*4 ascending
                    4 = int*4 descending
                    5 = float*4 ascending
                    6 = float*4 descending
                    7 = float*8 ascending (double)
                    8 = float*8 descending
   Benchmark: 10 million recs of 80 bytes with 4 sort keys:
              10.8 secs (4 GHz Core i5).

***/

int MemSortComp(ch *rec1, ch *rec2);
int MemSortKeys[10][3], MemSortNK;

int MemSort(ch *RECS, int RL, int NR, int KEYS[][3], int NK)
{
   int   ii;

   if (NR < 2) return 1;

   if (NK > 10) zappcrash("MemSort, bad NK");
   if (NK < 1) zappcrash("MemSort, bad NK");

   MemSortNK = NK;

   for (ii = 0; ii < NK; ii++)
   {
      MemSortKeys[ii][0] = KEYS[ii][0];
      MemSortKeys[ii][1] = KEYS[ii][1];
      MemSortKeys[ii][2] = KEYS[ii][2];
   }

   HeapSort(RECS,RL,NR,MemSortComp);
   return 1;
}

int MemSortComp(ch *rec1, ch *rec2)
{
   int         ii, stat, kpos, ktype, kleng;
   int         inum1, inum2;
   float       rnum1, rnum2;
   double      dnum1, dnum2;
   ch          *p1, *p2;

   for (ii = 0; ii < MemSortNK; ii++)                                                  //  loop each key
   {
      kpos = MemSortKeys[ii][0];                                                       //  relative position
      kleng = MemSortKeys[ii][1];                                                      //  length
      ktype = MemSortKeys[ii][2];                                                      //  type

      p1 = rec1 + kpos;                                                                //  absolute position
      p2 = rec2 + kpos;

      switch (ktype)
      {
         case 1:                                                                       //  ch ascending
            stat = strncmp(p1,p2,kleng);                                               //  compare 2 key values
            if (stat) return stat;                                                     //  + if rec1 > rec2, - if <
            break;                                                                     //  2 keys are equal, check next key

         case 2:                                                                       //  ch descending
            stat = strncmp(p1,p2,kleng);
            if (stat) return -stat;
            break;

         case 3:                                                                       //  int ascending
            memmove(&inum1,p1,4);
            memmove(&inum2,p2,4);
            if (inum1 > inum2) return 1;
            if (inum1 < inum2) return -1;
            break;

         case 4:                                                                       //  int descending
            memmove(&inum1,p1,4);
            memmove(&inum2,p2,4);
            if (inum1 > inum2) return -1;
            if (inum1 < inum2) return 1;
            break;

         case 5:                                                                       //  float ascending
            memmove(&rnum1,p1,4);
            memmove(&rnum2,p2,4);
            if (rnum1 > rnum2) return 1;
            if (rnum1 < rnum2) return -1;
            break;

         case 6:                                                                       //  float descending
            memmove(&rnum1,p1,4);
            memmove(&rnum2,p2,4);
            if (rnum1 > rnum2) return -1;
            if (rnum1 < rnum2) return 1;
            break;

         case 7:                                                                       //  double ascending
            memmove(&dnum1,p1,8);
            memmove(&dnum2,p2,8);
            if (dnum1 > dnum2) return 1;
            if (dnum1 < dnum2) return -1;
            break;

         case 8:                                                                       //  double descending
            memmove(&dnum1,p1,8);
            memmove(&dnum2,p2,8);
            if (dnum1 > dnum2) return -1;
            if (dnum1 < dnum2) return 1;
            break;

         default:                                                                      //  key type not 1-8
            zappcrash("MemSort, bad KEYS sort type");
      }
   }

   return 0;                                                                           //  records match on all keys
}


/**************************************************************************************/

//  test if an integer value matches any in a list of values
//  returns the matching value or zero if nothing matches
//  list of values must end with zero
//  zero cannot be one of the values to match

int zmember(int testval, int matchval1, ...)
{
   va_list     arglist;
   int         matchval;

   va_start(arglist,matchval1);
   matchval = matchval1;

   while (matchval)
   {
      if (testval == matchval) break;
      matchval = va_arg(arglist,int);
   }

   va_end(arglist);
   return matchval;
}


/**************************************************************************************

   Hash Table class

   HashTab(int cc, int cap);                       constructor
   ~HashTab();                                     destructor
   int Add(ch *string);                            add a new string
   int Del(ch *string);                            delete a string
   int Find(ch *string);                           find a string
   int GetCount() { return count; }                get string count
   int GetNext(int &first, ch *string);            get first/next string
   int Dump();                                     dump hash table to std. output

   constructor: cc = string length of table entries, cap = table capacity
   cap should be set 30% higher than needed to reduce collisions and improve speed

   Benchmark: 0.056 usec. to find 19 ch string in a table of 100,000
              which is 80% full.    3.3 GHz Core i5

***************************************************************************************/

//  static members (robust for tables up to 60% full)

int HashTab::tries1 = 100;                                                             //  Add() tries
int HashTab::tries2 = 200;                                                             //  Find() tries


HashTab::HashTab(int _cc, int _cap)                                                    //  constructor
{
   cc = 4 * (_cc + 4) / 4;                                                             //  + 1 + mod 4 length
   cap = _cap;
   int len = cc * cap;
   table = new ch [len];
   if (! table) zappcrash("HashTab() new %d fail",len,null);
   memset(table,0,len);
}


HashTab::~HashTab()                                                                    //  destructor
{
   delete [] table;
   table = 0;
}


//  Add a new string to table

int HashTab::Add(ch *string)
{
   int   pos, fpos, tries;

   pos = strHash(string,cap);                                                          //  get random position
   pos = pos * cc;

   for (tries = 0, fpos = -1; tries < tries1; tries++, pos += cc)                      //  find next free slot at/after position
   {
      if (pos >= cap * cc) pos = 0;                                                    //  last position wraps to 1st

      if (! table[pos])                                                                //  empty slot: string not found
      {
         if (fpos != -1) pos = fpos;                                                   //  use prior deleted slot if there
         strncpy(table+pos,string,cc);                                                 //  insert new string
         table[pos+cc-1] = 0;                                                          //  insure null terminator
         return (pos/cc);                                                              //  return rel. table entry
      }

      if (table[pos] == -1)                                                            //  deleted slot
      {
         if (fpos == -1) fpos = pos;                                                   //  remember 1st one found
         continue;
      }

      if (strmatch(string,table+pos)) return -2;                                       //  string already present
   }

   return -3;                                                                          //  table full (tries1 exceeded)
}


//  Delete a string from table

int HashTab::Del(ch *string)
{
   int   pos, tries;

   pos = strHash(string,cap);                                                          //  get random position
   pos = pos * cc;

   for (tries = 0; tries < tries2; tries++, pos += cc)                                 //  search for string at/after position
   {
      if (pos >= cap * cc) pos = 0;                                                    //  last position wraps to 1st

      if (! table[pos]) return -1;                                                     //  empty slot, string not found

      if (strmatch(string,table+pos))                                                  //  string found
      {
         table[pos] = -1;                                                              //  delete table entry
         return (pos/cc);                                                              //  return rel. table entry
      }
   }

   zappcrash("HashTab::Del() fail",null);                                              //  exceed tries2, must not happen
   return 0;                                                                           //  (table too full to function)
}


//  Find a table entry.

int HashTab::Find(ch *string)
{
   int   pos, tries;

   pos = strHash(string,cap);                                                          //  get random position
   pos = pos * cc;

   for (tries = 0; tries < tries2; tries++, pos += cc)                                 //  search for string at/after position
   {
      if (pos >= cap * cc) pos = 0;                                                    //  last position wraps to 1st
      if (! table[pos]) return -1;                                                     //  empty slot, string not found
      if (strmatch(string,table+pos)) return (pos/cc);                                 //  string found, return rel. entry
   }

   zappcrash("HashTab::Find() fail",null);                                             //  cannot happen
   return 0;
}


//  return first or next table entry

int HashTab::GetNext(int &ftf, ch *string)
{
   static int    pos;

   if (ftf)                                                                            //  initial call
   {
      pos = 0;
      ftf = 0;
   }

   while (pos < (cap * cc))
   {
      if ((table[pos] == 0) || (table[pos] == -1))                                     //  empty or deleted slot
      {
         pos += cc;
         continue;
      }

      strcpy(string,table+pos);                                                        //  return string
      pos += cc;
      return 1;
   }

   return -4;                                                                          //  EOF
}


int HashTab::Dump()
{
   int   ii, pos;

   for (ii = 0; ii < cap; ii++)
   {
      pos = ii * cc;

      if (table[pos] && table[pos] != -1)
         printf("%d, %s \n", ii, table + pos);

      if (table[pos] == -1)
         printf("%d, deleted \n", pos);
   }

   return 1;
}


/**************************************************************************************

   zlist - list processing functions

   typedef struct {                             list data type
      int      count;                           count of member strings
      ch       **mber;                          member strings, null == no members
   }  zlist_t;

   zlist_t   *zlist;

   zlist = zlist_new(N)                               make new zlist with N null members
   void zlist_free(zlist)                             delete zlist, free memory
   void zlist_dump(zlist)                             dump zlist to stdout
   N = zlist_count(zlist)                             get member count (including null entries)
   string = zlist_get(zlist,Nth)                      get Nth member
   void zlist_put(zlist,string,Nth)                   put Nth member (replace)
   void zlist_insert(zlist,string,Nth)                insert member (count += 1)
   void zlist_remove(zlist,Nth)                       remove member (count -= 1)
   void zlist_clear(zlist_t *zlist, int Nth);         clear zlist from Nth member to end
   void zlist_purge(zlist);                           purge zlist of null members
   err = zlist_add(zlist,string,Funiq)                add member at first null or append (if unique)
   err = zlist_append(zlist,string,Funiq)             append new member (if unique)
   err = zlist_prepend(zlist,string,Funiq)            prepend new member (if unique)
   Nth = zlist_find(zlist,string,posn);               find next matching zlist member at/after posn
   Nth = zlist_findwild(zlist,wstring,posn);          same as above, but wildcard string match
   zlist2 = zlist_copy(zlist1)                        copy zlist
   zlist3 = zlist_insert_zlist(zlist1,zlist2,Nth)     insert zlist2 into zlist1 at Nth posn
   zlist3 = zlist_remove(zlist1,zlist2)               remove all members of zlist2 from zlist1
   void zlist_sort(zlist)                             sort zlist ascending
   void zlist_sort(zlist,ccfunc)                      sort zlist using caller compare function
   err = zlist_to_file(zlist,filename)                make file from zlist
   zlist = zlist_from_file(filename)                  make zlist from file
   zlist = zlist_from_folder(foldername)              make zlist from file names in folder

   benchmark: zlist_find(): search speed 0.0016 usec per member for 20 ch. strings

***************************************************************************************/


//  create zlist with 'count' empty members

zlist_t * zlist_new(int count)
{
   zlist_t *zlist = (zlist_t *) zmalloc(sizeof(zlist_t),"zlist");
   zlist->count = count;
   if (count > 0)
      zlist->mber = (ch **) zmalloc(count * sizeof(ch *),"zlist");
   for (int ii = 0; ii < count; ii++)
      zlist->mber[ii] = null;
   return zlist;
}


//  delete a zlist

void zlist_free(zlist_t *zlist)
{
   for (int ii = 0; ii < zlist->count; ii++)
      if (zlist->mber[ii])
         zfree(zlist->mber[ii]);
   if (zlist->mber) zfree(zlist->mber);
   zlist->count = 0;
   zfree(zlist);
   return;
}


//  dump zlist to stdout

void zlist_dump(zlist_t *zlist)
{
   printf("count: %d \n",zlist->count);
   for (int ii = 0; ii < zlist->count; ii++)
      printf("%5d  %s \n",ii,zlist->mber[ii]);
   printf("\n");
   return;
}


//  get zlist member count

int zlist_count(zlist_t *zlist)
{
   return zlist->count;
}


//  get a zlist member

ch * zlist_get(zlist_t *zlist, int Nth)
{
   if (Nth >= 0 && Nth < zlist->count)
      return zlist->mber[Nth];
   else return null;                                                                   //  26.0
}


//  put a zlist member (replace existing) (null allowed)

void zlist_put(zlist_t *zlist, ch *string, int Nth)
{
   if (Nth < 0 || Nth >= zlist->count) {
      printf("zlist_put() invalid Nth: %d \n",Nth);                                    //  26.0
      return;
   }
   if (zlist->mber[Nth]) zfree(zlist->mber[Nth]);
   if (string) zlist->mber[Nth] = zstrdup(string,"zlist");
   else zlist->mber[Nth] = 0;
   return;
}


//  insert new zlist member (count increases)
//  new member is Nth member, old Nth member is Nth+1
//  if Nth > last + 1, null members are added in-between

void zlist_insert(zlist_t *zlist, ch *string, int Nth)
{
   int      count, newcount;
   int      ii1, ii2, cc;
   ch       **newmber;

   if (Nth < 0) zappcrash("zlist_insert() invalid Nth: %d",Nth);
   count = zlist->count;
   if (Nth < count) newcount = count + 1;
   else newcount = Nth + 1;
   newmber = (ch **) zmalloc(newcount * sizeof(ch *),"zlist");

   if (Nth > 0) {                                                                      //  copy 0 - Nth-1
      ii1 = 0;
      ii2 = Nth;
      if (Nth > count) ii2 = count;
      cc = (ii2 - ii1) * sizeof(ch *);
      memcpy(newmber,zlist->mber,cc);
   }

   newmber[Nth] = zstrdup(string,"zlist");                                             //  insert Nth

   if (Nth < count) {                                                                  //  copy Nth - last
      ii1 = Nth;
      ii2 = count;
      cc = (ii2 - ii1) * sizeof(ch *);
      memcpy(newmber+ii1+1,zlist->mber+ii1,cc);
   }

   if (zlist->mber) zfree(zlist->mber);
   zlist->mber = newmber;
   zlist->count = newcount;
   return;
}


//  remove a zlist member (count -= 1)

void zlist_remove(zlist_t *zlist, int Nth)
{
   int      newcount, cc;
   ch       **newmber;

   if (Nth < 0 || Nth >= zlist->count)
      zappcrash("zlist_remove() invalid Nth: %d",Nth);

   newcount = zlist->count - 1;
   if (newcount)
      newmber = (ch **) zmalloc(newcount * sizeof(ch *),"zlist");
   else newmber = 0;

   if (Nth > 0) {                                                                      //  copy 0 - Nth-1
      cc = Nth * sizeof(ch *);
      memcpy(newmber,zlist->mber,cc);
   }

   if (zlist->mber[Nth]) zfree(zlist->mber[Nth]);                                      //  remove Nth

   if (Nth < newcount) {                                                               //  copy Nth+1 - last
      cc = (newcount - Nth) * sizeof(ch *);
      memcpy(newmber+Nth,zlist->mber+Nth+1,cc);
   }

   zfree(zlist->mber);
   zlist->mber = newmber;
   zlist->count = newcount;
   return;
}


//  clear zlist members from Nth to end

void zlist_clear(zlist_t *zlist, int Nth)
{
   int      ii;
   ch       **mber = 0;

   if (Nth >= zlist_count(zlist)) return;

   if (Nth > 0) mber = (ch **) zmalloc(Nth * sizeof(ch *),"zlist");                    //  remaining members

   for (ii = 0; ii < Nth; ii++)                                                        //  copy remaining members
      mber[ii] = zlist->mber[ii];

   for (ii = Nth; ii < zlist_count(zlist); ii++)                                       //  free deleted members
      zfree(zlist->mber[ii]);

   zfree(zlist->mber);
   zlist->mber = mber;                                                                 //  null if empty list
   zlist->count = Nth;
   return;
}


//  purge zlist of all null members and reset count

void zlist_purge(zlist_t *zlist)
{
   int      ii, jj;
   ch       **mber;

   for (ii = jj = 0; ii < zlist->count; ii++)
      if (zlist->mber[ii]) jj++;

   if (jj) mber = (ch **) zmalloc(jj * sizeof(ch *),"zlist");
   else mber = 0;

   for (ii = jj = 0; ii < zlist->count; ii++) {
      if (zlist->mber[ii]) {
         mber[jj] = zlist->mber[ii];
         jj++;
      }
   }

   zlist->count = jj;
   zfree(zlist->mber);
   zlist->mber = mber;
   return;
}


//  add new member at first null position, or append (if unique)
//  return 0 if OK, 1 if not unique

int zlist_add(zlist_t *zlist, ch *string, int Funiq)
{
   int      ii;

   if (Funiq && zlist_find(zlist,string,0) >= 0) return 1;
   for (ii = 0; ii < zlist->count; ii++)
      if (! zlist->mber[ii]) break;
   if (ii < zlist->count) {
      zlist->mber[ii] = zstrdup(string,"zlist");
      return 0;
   }
   return zlist_append(zlist,string,Funiq);
}


//  append new member at end (if unique)
//  return 0 if OK, 1 if not unique

int zlist_append(zlist_t *zlist, ch *string, int Funiq)
{
   if (Funiq && zlist_find(zlist,string,0) >= 0) return 1;
   zlist_insert(zlist,string,zlist->count);
   return 0;
}


//  prepend new member at posn 0 (if unique)
//  return 0 if OK, 1 if not unique

int zlist_prepend(zlist_t *zlist, ch *string, int Funiq)
{
   if (Funiq && zlist_find(zlist,string,0) >= 0) return 1;
   zlist_insert(zlist,string,0);
   return 0;
}


//  find next matching zlist member at/from given posn

int zlist_find(zlist_t *zlist, ch *string, int posn)
{
   if (posn < 0 || posn >= zlist->count) return -1;
   for (int ii = posn; ii < zlist->count; ii++) {
      if (zlist->mber[ii])
         if (strmatch(string,zlist->mber[ii])) return ii;
   }
   return -1;
}


//  find next matching zlist member at/from given posn (wildcard match)

int zlist_findwild(zlist_t *zlist, ch *wstring, int posn)
{
   if (posn < 0 || posn >= zlist->count) return -1;
   for (int ii = posn; ii < zlist->count; ii++) {
      if (zlist->mber[ii])
         if (MatchWild(wstring,zlist->mber[ii]) == 0) return ii;
   }
   return -1;
}


//  copy a zlist

zlist_t * zlist_copy(zlist_t *zlist1)
{
   zlist_t *zlist2 = zlist_new(zlist1->count);
   for (int ii = 0; ii < zlist2->count; ii++)
      if (zlist1->mber[ii])
         zlist2->mber[ii] = zstrdup(zlist1->mber[ii],"zlist");
   return zlist2;
}


//  insert zlist2 into zlist1 at Nth position
//  use Nth = -1 to insert at the end (append)

zlist_t * zlist_insert_zlist(zlist_t *zlist1, zlist_t *zlist2, int Nth)
{
   int   ii;
   int   nn1 = zlist1->count;                         //  zlist to receive
   int   nn2 = zlist2->count;                         //  zlist to insert
   int   nn3 = nn1 + nn2;                             //  output zlist

   if (Nth < 0) Nth = nn1;                            //  append to end of zlist1
   if (Nth > nn1) nn3 = Nth + nn2;                    //  append with missing members in-between

   zlist_t *zlist3 = zlist_new(nn3);

   for (ii = 0; ii < Nth; ii++)                                                        //  0 to Nth-1
      if (ii < nn1 && zlist1->mber[ii])
         zlist3->mber[ii] = zstrdup(zlist1->mber[ii],"zlist");
   for (ii = Nth; ii < Nth + nn2; ii++)                                                //  Nth to Nth + nn2-1
      if (zlist2->mber[ii-Nth])
         zlist3->mber[ii] = zstrdup(zlist2->mber[ii-Nth],"zlist");
   for (ii = Nth + nn2; ii < nn3; ii++)                                                //  Nth + nn2 to nn3-1
      if (ii-nn2 < nn1 && zlist1->mber[ii-nn2])
         zlist3->mber[ii] = zstrdup(zlist1->mber[ii-nn2],"zlist");

   return zlist3;
}


//  remove all members of zlist2 from zlist1

zlist_t * zlist_remove(zlist_t *zlist1, zlist_t *zlist2)
{
   int   ii, jj;
   int   nn2 = zlist2->count;

   zlist_t *zlist3 = zlist_copy(zlist1);                                               //  copy input zlist

   for (ii = 0; ii < nn2; ii++) {
      jj = zlist_find(zlist3,zlist_get(zlist2,ii),0);                                  //  find zlist2 member in zlist3
      if (jj >= 0) zlist_put(zlist3,null,jj);                                          //  if found, replace with null
   }

   zlist_purge(zlist3);                                                                //  purge null entries
   return zlist3;
}


//  sort zlist ascending

void zlist_sort(zlist_t *zlist)
{
   HeapSort(zlist->mber,zlist->count);
   return;
}


//  sort zlist via caller compare function

void zlist_sort(zlist_t *zlist, int ccfunc(ch *, ch *))
{
   HeapSort(zlist->mber,zlist->count,ccfunc);
   return;
}

//  make file from zlist

int zlist_to_file(zlist_t *zlist, ch *filename)
{
   int      ii, err;

   FILE *fid = fopen(filename,"w");
   if (! fid) return errno;
   for (ii = 0; ii < zlist->count; ii++)
      if (zlist->mber[ii])
         fprintf(fid,"%s\n",zlist->mber[ii]);
   err = fclose(fid);
   if (err) return errno;
   else return 0;
}


//  make zlist from file
//  performance with SSD: over 100 MB/sec.

zlist_t * zlist_from_file(ch *filename)
{
   FILE     *fid;
   zlist_t  *zlist;
   int      ii, count = 0;
   ch       *pp, buff[5000];

   fid = fopen(filename,"r");                                                          //  count recs in file
   if (! fid) return 0;                                                                //  this adds 40% to elapsed time
   while (true) {
      pp = fgets(buff,5000,fid);
      if (! pp) break;
      count++;
   }
   fclose(fid);

   fid = fopen(filename,"r");
   if (! fid) return 0;
   zlist = zlist_new(count);                                                           //  create zlist
   for (ii = 0; ii < count; ii++) {
      pp = fgets_trim(buff,5000,fid);
      if (! pp) break;
      zlist->mber[ii] = zstrdup(buff,"zlist");
   }
   fclose(fid);

   return zlist;
}

//  create a zlist from all file names in a folder
//  list members are file names without folder

zlist_t * zlist_from_folder(ch *folder)                                                //  25.1
{
   ch       findcomm[1000];
   ch       *pp, **flist;
   int      ii, NF;
   zlist_t  *zlist;

   snprintf(findcomm,1000,"%s/*",folder);                                              //  find all files in folder
   zfind(findcomm,flist,NF);
   if (! NF) return 0;

   zlist = zlist_new(NF);

   for (ii = 0; ii < NF; ii++) {                                                       //  make zlist of file root names
      pp = strrchr(flist[ii],'/');
      if (! pp) pp = flist[ii];
      else pp++;
      zlist_put(zlist,pp,ii);
      zfree(flist[ii]);
   }

   zfree(flist);
   zlist_sort(zlist);                                                                  //  sort zlist
   return zlist;
}


/**************************************************************************************/

//  Random number generators with explicit context
//  and improved randomness over a small series.
//  Benchmark: lrandz 0.012 usec  drandz 0.014 usec  3.3 GHz Core i5
//  (srand() % range) is much slower.

int lrandz(int64 *seed)                                                                //  returns 0 to 0x7fffffff
{
   *seed = *seed ^ (*seed << 17);
   *seed = *seed ^ (*seed << 20);
   return nrand48((unsigned int16 *) seed);
}

int lrandz()                                                                           //  implicit seed, repeatable sequence
{
   static int64   seed = 12345678;
   return lrandz(&seed);
}

double drandz(int64 *seed)                                                             //  returns 0.0 to 0.99999...
{
   *seed = *seed ^ (*seed << 17);
   *seed = *seed ^ (*seed << 20);
   return erand48((unsigned int16 *) seed);
}

double drandz()                                                                        //  automatic seed, volatile
{
   static int64 seed = get_seconds();
   return drandz(&seed);
}


/**************************************************************************************

   spline1: define a curve using a set of data points (x and y values)
   spline2: for a given x-value, return a y-value fitting the curve

   For spline1, the no. of curve-defining points must be < 100.
   For spline2, the given x-value must be within the range defined in spline1.

   The algorithm was taken from the book "Numerical Recipes"
   (Cambridge University Press) and converted from Fortran to C++.

***/

namespace splinedata
{
   int      nn;
   float    px1[100], py1[100], py2[100];
}


void spline1(int dnn, float *dx1, float *dy1)
{
   using namespace splinedata;

   float    sig, p, u[100];
   int      ii;

   nn = dnn;
   if (nn > 100) zappcrash("spline1(), > 100 data points");

   for (ii = 0; ii < nn; ii++)
   {
      px1[ii] = dx1[ii];
      py1[ii] = dy1[ii];
      if (ii && px1[ii] <= px1[ii-1])
         zappcrash("spline1(), x-value not increasing");
   }

   py2[0] = u[0] = 0;

   for (ii = 1; ii < nn-1; ii++)
   {
      sig = (px1[ii] - px1[ii-1]) / (px1[ii+1] - px1[ii-1]);
      p = sig * py2[ii-1] + 2;
      py2[ii] = (sig - 1) / p;
      u[ii] = (6 * ((py1[ii+1] - py1[ii]) / (px1[ii+1] - px1[ii]) - (py1[ii] - py1[ii-1])
            / (px1[ii] - px1[ii-1])) / (px1[ii+1] - px1[ii-1]) - sig * u[ii-1]) / p;
   }

   py2[nn-1] = 0;

   for (ii = nn-2; ii >= 0; ii--)
      py2[ii] = py2[ii] * py2[ii+1] + u[ii];

   return;
}


float  spline2(float x)
{
   using namespace splinedata;

   int      kk, klo = 0, khi = nn-1;
   float    h, a, b, y;

   while (khi - klo > 1)
   {
      kk = (khi + klo) / 2;
      if (px1[kk] > x) khi = kk;
      else klo = kk;
   }

   h = px1[khi] - px1[klo];
   a = (px1[khi] - x) / h;
   b = (x - px1[klo]) / h;
   y = a * py1[klo] + b * py1[khi] + ((a*a*a - a) * py2[klo]
                                   + (b*b*b - b) * py2[khi]) * (h*h) / 6;

   return y;
}


/**************************************************************************************/

//  Add text strings to a FIFO queue, retrieve text strings.
//  Can be used by one or two threads.
//  thread 1: open queue, get strings, close queue.
//  thread 2: put strings into queue.


//  create and initialize Qtext queue, empty status

void Qtext_open(Qtext *qtext, int cap)
{
   int      cc;

   qtext->qcap = cap;
   qtext->qnewest = -1;
   qtext->qoldest = -1;
   qtext->qdone = 0;
   cc = cap * sizeof(ch *);
   qtext->qtext = (ch **) zmalloc(cc,"qtext");
   memset(qtext->qtext,0,cc);
   return;
}


//  add new text string to Qtext queue
//  if queue full, sleep until space is available

void Qtext_put(Qtext *qtext, ch *format, ...)
{
   int      qnext;
   va_list  arglist;
   ch       message[200];

   va_start(arglist,format);
   vsnprintf(message,199,format,arglist);
   va_end(arglist);

   qnext = qtext->qnewest + 1;
   if (qnext == qtext->qcap) qnext = 0;
   while (qtext->qtext[qnext]) zsleep(0.01);
   qtext->qtext[qnext] = zstrdup(message,"Qtext");
   qtext->qnewest = qnext;
   return;
}


//  remove oldest text string from Qtext queue
//  if queue empty, return a null string
//  returned string is subject for zfree()

ch * Qtext_get(Qtext *qtext)
{
   int      qnext;
   ch       *text;

   if (qtext->qcap == 0) return 0;
   qnext = qtext->qoldest + 1;
   if (qnext == qtext->qcap) qnext = 0;
   text = qtext->qtext[qnext];
   if (! text) return 0;
   qtext->qtext[qnext] = 0;
   qtext->qoldest = qnext;
   return text;
}


//  close Qtext, zfree() any leftover strings

void Qtext_close(Qtext *qtext)
{
   for (int ii = 0; ii < qtext->qcap; ii++)
      if (qtext->qtext[ii]) zfree(qtext->qtext[ii]);
   zfree(qtext->qtext);
   qtext->qcap = 0;
   return;
}


/**************************************************************************************/

//  compute variance for a list of numbers
//  std. deviation = sqrtf(variance)

float variance(float *vals, int N)
{
   float    mean1, mean2, variance;
   int      ii;

   if (N == 0) return 0;

   mean1 = mean2 = 0;

   for (ii = 0; ii < N; ii++)
   {
      mean1 += vals[ii];
      mean2 += vals[ii] * vals[ii];
   }

   mean1 = mean1 / N;
   mean2 = mean2 / N;

   variance = fabsf(mean2 - mean1 * mean1);
   return variance;
}


/**************************************************************************************

   Initialize application files according to following conventions:                    //  new version
     + binary executable is at:  /prefix/bin/appname                                   //  = PREFIX/bin/appname
     + other application folders are derived as follows:
         /prefix/share/appname/data/            desktop, parameters, userguide  ...
         /prefix/share/doc/appname/             README, changelog, appname.man ...
         /prefix/share/appname/icons/           application icon files, filename.png
         /prefix/share/appname/images/          application image files
         /home/user/.appname/                   some installation files are copied here
         /home/user/.appname/appname-N.N.log    log file with error messages

   zprefix        install location              /usr or /usr/local
   zdatadir       installed data files          /prefix/share/appname/data/
   zimagedir      installed image files         /prefix/share/appname/images/
   zdocdir        documentation files           /prefix/share/doc/appname/
   zhomedir       local app files               /home/<user>/.appname/

   If it does not already exist, an application folder for the current user is
   created at /home/username/.appname (following common Linux convention).
   If this folder was created for the first time, copy specified files
   (following the 1st argument) from the install folder into the newly created
   user-specific folder. The assumption is that all initial data files for the
   application (e.g. parameters) will be in the install data folder, and these are
   copied to the user folder where the user or application can modify them.

   If the running program is not connected to a terminal device, stdout and stderr are
   redirected to the log file at /home/user/.appname/appname-N.N.log

***/

ch * get_zprefix() { return zfuncs::zprefix; }                                         //  /usr or /usr/local
ch * get_zhomedir() { return zfuncs::zhomedir; }                                       //  /home/<user>/.appname
ch * get_zdatadir() { return zfuncs::zdatadir; }                                       //  data files
ch * get_zdocdir()  { return zfuncs::zdocdir;  }                                       //  documentation files
ch * get_zimagedir()  { return zfuncs::zimagedir;  }                                   //  image files


int zinitapp(ch *appvers, int argc, ch *argv[])                                        //  appname-N.N
{
   ch          buff[300];
   ch          LNhomedir[200];
   ch          username[140];
   ch          cssfile[200];
   ch          *pp, *ch_time;
   int         ii, cc, err, size;
   time_t      startime;
   STATB       statB;
   FILE        *fid;

   printf("current directory: %s\n",get_current_dir_name());                           //  do not CD $HOME                       26.4

   startime = time(null);                                                              //  app start time, secs. since 1970

   catch_signals();                                                                    //  catch signals, do backtrace

   printf("command: ");
   for (ii = 0; ii < argc; ii++) printf("%s ",argv[ii]);                               //  log command line
   printf("\n");

   setpgid(0,0);                                                                       //  make a new process group

   pp = setlocale(LC_NUMERIC,"C");                                                     //  stop comma decimal points

   strncpy0(zappvers,appvers,40);                                                      //  appname-N.N
   printf("%s \n",zappvers);

   strncpy0(zappname,appvers,40);                                                      //  appname   without version
   pp = strchr(zappname,'-');
   if (pp && pp[1] > '9') pp = strchr(pp+1,'-');                                       //  bypass '-' in appname
   if (pp) *pp = 0;

   if (argc > 1 && strmatchV(argv[1],"-ver","-v",null)) exit(0);                       //  exit if nothing else wanted

   progexe = 0;
   cc = readlink("/proc/self/exe",buff,300);                                           //  get my executable program path
   if (cc <= 0) zexit(1,"readlink() /proc/self/exe) failed");
   buff[cc] = 0;                                                                       //  readlink() quirk
   progexe = zstrdup(buff,"zinitapp");

   printf("program exe: %s \n",progexe);                                               //  executable path

   strncpy0(zprefix,progexe,200);
   pp = strstr(zprefix,"/bin/");                                                       //  get install prefix (e.g. /usr)
   if (pp) *pp = 0;
   else (strcpy(zprefix,"/usr"));                                                      //  if /xxxxx/bin --> /xxxxx

   strncatv(zdatadir,199,zprefix,"/share/",zappname,"/data",null);                     //  /prefix/share/appname/data
   strncatv(zimagedir,199,zprefix,"/share/",zappname,"/images",null);                  //  /prefix/share/appname/images
   strncatv(zdocdir,199,zprefix,"/share/doc/",zappname,null);                          //  /prefix/share/doc/appname

   ch_time = zstrdup(build_date_time,"zinitapp");
   if (ch_time[4] == ' ') ch_time[4] = '0';                                            //  replace month day ' d' with '0d'
   printf("build date/time: %s \n",ch_time);

   strncpy0(username,getenv("USER"),140);                                              //  get user name
   if (strlen(username) > 138) zexit(1,"username too big");                            //  insure derived folders <200 ch.

   *zhomedir = 0;                                                                      //  app home dir

   if (argc > 2 && strmatch(argv[1],"-home") && *argv[2] == '/')                       //  get appname home folder from caller
      strncpy0(zhomedir,argv[2],200);                                                  //  (any user-owned folder)

   if (! *zhomedir) {
      snprintf(LNhomedir,200,"%s/.%s-home",getenv("HOME"),zappname);                   //  check $HOME/.appname-home
      fid = fopen(LNhomedir,"r");
      if (fid) {
         pp = fgets_trim(LNhomedir,200,fid);                                           //  if found, read pointer to home folder
         if (pp) strncpy0(zhomedir,pp,200);                                            //  (can be any user-owned folder)
         fclose(fid);
      }
   }

   if (! *zhomedir)
      snprintf(zhomedir,200,"%s/.%s",getenv("HOME"),zappname);                         //  use $HOME/.appname/    (default) 

   printf("%s home folder: %s \n",zappname,zhomedir);

   if (strchr(zhomedir,' ')) zexit(1,"home folder name contains a space");             //  forbid space or quote in home folder
   if (strchr(zhomedir,'"')) zexit(1,"home folder name contains a quote");
   if (strchr(zhomedir,'\'')) zexit(1,"home folder name contains a quote");

   err = stat(zhomedir,&statB);                                                        //  home folder exists already?
   if (err) {
      err = mkdir(zhomedir,0750);                                                      //  no, create
      if (err) zexit(1,"cannot create %s: \n %s",zhomedir,strerror(errno));
   }

   snprintf(logfile,199,"%s/%s.log",zhomedir,zappvers);                                //  /home/<user>/appname-N.N.log
   printf("log file: %s \n",logfile);

   err = stat(logfile,&statB);                                                         //  start new log file if >50 kb          25.1
   if (! err) {
      size = statB.st_size;
      if (size > 50000) remove(logfile);
   }

   if (! isatty(fileno(stdin))) {                                                      //  not attached to a terminal
      fid = freopen(logfile,"a",stdout);                                               //  redirect stdout/stderr to log file
      fid = freopen(logfile,"a",stderr);
      if (! fid) printf("*** cannot redirect stdout and stderr \n");
   }

   printf("-------------------------------------------\n");                            //  log file separator

   ch_time = ctime(&startime);                                                         //  start time: Ddd Mmm dd hh:mm:ss.nn
   ch_time[19] = 0;                                                                    //  eliminate hundredths of seconds
   if (ch_time[8] == ' ') ch_time[8] = '0';                                            //  replace ' d' with '0d'
   printf("start %s %s \n",zappname,ch_time);
   fflush(0);

   tid_main = pthread_self();                                                          //  thread ID of main() process

   //  GTK initialization

   setenv("GDK_BACKEND","x11",0);                                                      //  needed by Fedora (11/2023)
   setenv("GTK_THEME","default",0);                                                    //    " "

   if (! gtk_init_check(0,null))
      zexit(1,"gtk_init_check() failed");

   setlocale(LC_NUMERIC,"C");                                                          //  NECESSARY: GTK changes locale

   int v1 = gtk_get_major_version();                                                   //  get GTK release version
   int v2 = gtk_get_minor_version();
   int v3 = gtk_get_micro_version();
   printf("GTK version: %d.%02d.%02d \n",v1,v2,v3);

   display = gdk_display_get_default();                                                //  get hardware info
   screen = gdk_screen_get_default();

   GdkRectangle   rect;
   GdkMonitor     *monitor;

   monitor = gdk_display_get_primary_monitor(display);
   gdk_monitor_get_geometry(monitor,&rect);
   monitor_ww = rect.width;
   monitor_hh = rect.height;

   if (! monitor_ww) zexit(1,"GTK cannot get monitor data");

   GdkSeat    *gdkseat = 0;                                                            //  screen / KB / pointer associations
   if (screen) gdkseat = gdk_display_get_default_seat(display);                        //  Ubuntu 16.10
   if (screen) gtksettings = gtk_settings_get_for_screen(screen);
   if (gdkseat) mouse = gdk_seat_get_pointer(gdkseat);

   if (! mouse) zexit(1,"GTK cannot get pointer device");

   if (gtksettings) {                                                                  //  get default font
      g_object_get(gtksettings,"gtk_font_name",&appfont,null);
      zsetfont(appfont);                                                               //  set mono and bold versions
   }

   //  widget style changes if widgets.css file is present

   snprintf(cssfile,200,"%s/widgets.css",get_zhomedir());
   err = stat(cssfile,&statB);
   if (! err) {
      GtkStyleProvider *provider = (GtkStyleProvider *) gtk_css_provider_new();
      gtk_style_context_add_provider_for_screen(zfuncs::screen,provider,999);
      gtk_css_provider_load_from_path(GTK_CSS_PROVIDER(provider),cssfile,0);
   }

   return 1;
}


//  copy application files from install folder  /.../appname/data/*
//                          to app home folder  /home/<user>/.appname/*

void zinitappfiles(int Fnewvers)                                                       //  new 26.0
{
   STATB       statB;

   ch *zdatadir = get_zdatadir();
   ch *zhomedir = get_zhomedir();

   int err = stat(zdatadir,&statB);
   if (err) return;

   if (Fnewvers) 
      zshell("log","cp -R --backup=simple  %s/* %s",zdatadir,zhomedir);                //  make backups and copy all files
   else zshell("log","cp -R --update=none  %s/* %s",zdatadir,zhomedir);                //  copy only missing files
   
   return;
}


/**************************************************************************************/

//  popup window with application 'about' information

void zabout(GtkWidget *parent)
{
   int zabout_dialog_event(zdialog *zd, ch *event);

   zdialog  *zd;
   int      cc;
   ch       installed_release[80];
   ch       title[40];
   ch       *execfile;

/***
    __________________________________________________
   |               About Appname                      |
   |                                                  |
   |  installed release: appname-N.N  Mon dd yyyy     |                                //  'query release' removed
   |  executable: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx       |
   |  contact: mkornelix@gmail.com                    |
   |__________________________________________________|

***/

   execfile = progexe;

   snprintf(installed_release,80,"%s  %s",zappvers,build_date_time);                   //  appname-N.N  Mon dd yyyy hh:mm:ss

   cc = strlen(installed_release);
   installed_release[cc-9] = 0;                                                        //  remove hh:mm:ss
   if (installed_release[cc-16] == ' ') installed_release[cc-16] = '0';                //  replace "Jan  1" with "Jan 01"

   snprintf(title,40,"About %s",zappname);
   zd = zdialog_new(title,parent,null);

   zdialog_add_widget(zd,"hbox","hbirel","dialog");
   zdialog_add_widget(zd,"label","labir1","hbirel","installed release:","space=3");
   zdialog_add_widget(zd,"label","labir2","hbirel",installed_release);

   zdialog_add_widget(zd,"hbox","hbexe","dialog");
   zdialog_add_widget(zd,"label","labexe1","hbexe","executable:","space=3");
   zdialog_add_widget(zd,"label","labexe2","hbexe",execfile);

   zdialog_add_widget(zd,"hbox","hbcontact","dialog");
   zdialog_add_widget(zd,"label","labcontact","hbcontact","contact:","space=3");
   zdialog_add_widget(zd,"label","contact","hbcontact",zcontact);

   zdialog_run(zd,zabout_dialog_event,"parent");
   return;
}


//  dialog event and completion function

int zabout_dialog_event(zdialog *zd, ch *event)
{
   if (zd->zstat) zdialog_free(zd);
   return 1;
}


/**************************************************************************************/

//  set a new application font via GtkSettings
//  newfont should be something like "free sans 11"
//  use generic monospace font since app font may not have a mono version

void zsetfont(ch *newfont)
{
   ch       font[80], bfont[88], mfont[88], mbfont[100];
   ch       *pp;
   int      size;

   if (! gtksettings) return;

   strncpy0(font,newfont,80);                                                          //  more robuse font parsing              25.1

   pp = font + strlen(font);
   while (*pp < ' ') pp--;                                                             //  skip poss. trailing blanks            26.3
   while (*pp >= '0' && *pp <= '9') pp--;                                              //  skip over font size digits
   size = atoi(pp);                                                                    //  get font size
   if (size < 6 || size > 14) {
      printf("font size %d is not reasonable",size);
      size = 10;
   }
   else *pp = 0;                                                                       //  font name without size

   g_object_set(gtksettings,"gtk-font-name",newfont,null);                             //  set dialog font

   snprintf(bfont,48,"%s bold %d",font,size);                                          //  "free sans 11 bold"
   snprintf(mfont,48,"mono %d",size-1);                                                //  "mono 10"
   snprintf(mbfont,56,"mono bold %d",size-1);                                          //  "mono bold 10"
   appfont = zstrdup(newfont,"zsetfont");
   appboldfont = zstrdup(bfont,"zsetfont");
   appmonofont = zstrdup(mfont,"zsetfont");
   appmonoboldfont = zstrdup(mbfont,"zsetfont");
   appfontsize = size;

   return;
}


/**************************************************************************************/

//  get the font character width and height for a given widget
//  returns 0 if OK, +N if error

int widget_font_metrics(GtkWidget *widget, int &fontwidth, int &fontheight)
{
   PangoContext         *pangocontext;
   PangoFontDescription *pangofontdesc;
   PangoFontMetrics     *pangofontmetrics;
   PangoLanguage        *pangolanguage;

   pangocontext = gtk_widget_get_pango_context(widget);
   pangofontdesc = pango_context_get_font_description(pangocontext);
   pangolanguage = pango_language_get_default();
   pangofontmetrics = pango_context_get_metrics(pangocontext,pangofontdesc,pangolanguage);
   if (! pangofontmetrics) {
      printf("*** widget_font_metrics() failed \n");
      return 1;
   }

   fontwidth = pango_font_metrics_get_approximate_char_width(pangofontmetrics);
   fontheight = pango_font_metrics_get_ascent(pangofontmetrics)
              + pango_font_metrics_get_descent(pangofontmetrics);
   fontwidth /= PANGO_SCALE;
   fontheight /= PANGO_SCALE;

   return 0;
}


/**************************************************************************************/

//  Find installation file or user file.
//    file type: doc, data, user
//    file name: README, changelog, userguide, parameters ...
//  Returns complete file name, e.g. /usr/share/appname/data/userguide
//  Output filespec should be 200 bytes (limit for all installation files).
//  Returns 0 if OK, +N if not found.

int get_zfilespec(ch *filetype, ch *filename, ch *filespec)
{
   int      cc, err;
   STATB    statB;

   filespec[0] = '/';
   strcat(filespec,filetype);                                                          //  leave /type as default

   if (strmatch(filetype,"doc")) strcpy(filespec,zdocdir);                             //  /usr/share/doc/appname
   if (strmatch(filetype,"data")) strcpy(filespec,zdatadir);                           //  /usr/share/appname/data
   if (strmatch(filetype,"user")) strcpy(filespec,zhomedir);                           //  /home/<user>/.appname

   cc = strlen(filespec);
   filespec[cc] = '/';                                                                 //  /folders.../
   strcpy(filespec+cc+1,filename);                                                     //  /folders.../filename
   err = stat(filespec,&statB);
   if (! err) return 0;                                                                //  found

   if (! strmatch(filetype,"doc")) return 1;                                           //  doc files may be in
   strcpy(filespec,zdocdir);                                                           //    /usr/share/doc/appname/extras
   strcat(filespec,"/extras/");                                                        //       due to Linux chaos
   cc = strlen(filespec);
   strcpy(filespec+cc,filename);
   err = stat(filespec,&statB);
   if (! err) return 0;                                                                //  found

   return 1;                                                                           //  not found
}


/**************************************************************************************/

//  display application log file in a popup window
//  The log file is /home/<user>/.appname/logfile

void showz_logfile(GtkWidget *parent)
{
   ch   buff[200];

   fflush(0);
   snprintf(buff,199,"cat %s",logfile);
   popup_command(buff,800,600,parent);
   return;
}


/**************************************************************************************/

//  find and show a text file in /usr/share/doc/appname/
//                            or /usr/share/appname/data
//  the text file may also be a compressed .gz file
//  type is "doc" or "data"

void showz_textfile(ch *type, ch *file, GtkWidget *parent)
{
   ch       filex[40], filespec[200], command[200];
   int      err;

   if (! main_thread()) zappcrash("illegal call from thread");

   strncpy0(filex,file,36);                                                            //  look for gzip file first
   strcat(filex,".gz");

   err = get_zfilespec(type,filex,filespec);
   if (! err) {
      snprintf(command,200,"zcat \"%s\"",filespec);                                    //  use quotes around filename
      popup_command(command,700,500,parent,1);
      return;
   }

   strncpy0(filex,file,35);                                                            //  look also for bzip2 file
   strcat(filex,".bz2");

   err = get_zfilespec(type,filex,filespec);
   if (! err) {
      snprintf(command,200,"bzcat \"%s\"",filespec);
      popup_command(command,700,500,parent,1);
      return;
   }

   strncpy0(filex,file,36);                                                            //  look for uncompressed file

   err = get_zfilespec(type,filex,filespec);
   if (! err) {
      snprintf(command,200,"cat \"%s\"",filespec);
      popup_command(command,700,500,parent,1);
      return;
   }

   zmessageACK(mainwin,TX("file not found: %s %s"),type,file);
   return;
}


/**************************************************************************************

   void showz_docfile(GtkWidget *parent, ch *docfile, ch *topic)

   Show docfile in popup scrolling text window with 'topic' at the top.
   docfile is located in data folder: get_zdatadir()
   images are located in image folder: get_zimagedir()

   docfile format:

   TOPIC: TOPIC 1                                                                topic: bold
   text text text text text text text text text text text text ...               topic text

   TOPIC: TOPIC 2
   +image: file1.png  +image: file2.jpg                                          embedded images

   text text text text text text text text {TOPIC 1} text text ...               link to topic, bold

   ^bold subtopic line^                                                          subtopic, bold
   text text text text text ^bold text bold text^ text text ...                  embedded bold text

   text text text text text  ^http.........^  text text text ...                 web link, bold

***************************************************************************************/

namespace showz_docfile_names
{
   #define TMAX 1000                                                                   //  max. topics and markups
   zdialog     *zd = 0;
   GtkWidget   *txwidget;
   int         cc, ii, jj;
   int         currline;
   ch          *pp;
   ch          *Tname[TMAX];                                                           //  all topic names (link targets)
   int         Tline[TMAX];                                                            //  topic lines
   int         Bline[TMAX], Bpos[TMAX], Bcc[TMAX];                                     //  bold texts: line, posn, cc
   int         TN, BN, BF;                                                             //  counts, flags
   zlist_t     *RTopics = 0;                                                           //  recent topics list
   ch          RTfile[200] = "";                                                       //  recent topics file
   int         RTmax = 10;                                                             //  max. recent topics
}


void showz_docfile(GtkWidget *parent, ch *docfile, ch *utopic)
{
   using namespace showz_docfile_names;

   int showz_docfile_clickfunc(GtkWidget *widget, int line, int pos, ch *input);

   FILE        *fid;
   ch          filespec[200], buff1[10000], buff2[10000];                              //  record cc limit (100 lines x 100 char.)
   ch          topic[100], image[100];                                                 //  topic name, image name                26.3
   ch          *pp1, *pp2;
   int         Fm, line, pos1, pos2, cc;
   GdkPixbuf   *pixbuf;
   GError      *gerror;

   if (! main_thread()) zappcrash("illegal call from thread");

   if (utopic && strmatch(utopic,"quit")) {                                            //  quit - save recent topics list
      if (RTopics && *RTfile) zlist_to_file(RTopics,RTfile);
      if (zdialog_valid2(zd)) popup_report_close(zd,0);                                //  close open report or UG
      return;
   }

   if (utopic && strmatch(utopic,"validate")) {                                        //  check document for errors
      audit_docfile(docfile);                                                          //  (developer tool)
      return;
   }

   if (zd && zdialog_valid2(zd,docfile))                                               //  document active already
      goto initz_done;

   snprintf(RTfile,200,"%s/recent_topics",get_zhomedir());                             //  get recent topics list
   RTopics = zlist_from_file(RTfile);
   if (! RTopics) RTopics = zlist_new(0);

   for (ii = 0; ii < TN; ii++) zfree(Tname[ii]);                                       //  free prior docfile data if any

   TN = BN = 0;

   snprintf(filespec,200,"%s/%s",get_zdatadir(),docfile);                              //  open docfile
   fid = fopen(filespec,"r");
   if (! fid) zexit(1,"%s %s \n",filespec,strerror(errno));

   for (line = 0; ; line++)                                                            //  loop docfile recs/lines
   {
      pp1 = fgets_trim(buff1,10000,fid);                                               //  line with null at end
      if (! pp1) break;                                                                //  EOF
      if (strmatchN(pp1,"EOF",3)) break;                                               //  end of displayed text

      if (! strmatchN(buff1,"TOPIC:",6)) continue;                                     //  not a topic name
      pp1 = buff1 + 6;
      while (*pp1 == ' ') pp1++;
      pp2 = pp1 + strlen(pp1);
      cc = pp2 - pp1;                                                                  //  bugfix (utf8)                         26.3
      if (cc < 1 || cc > 99) continue;
      strncpy0(topic,pp1,cc+1);                                                        //  add topic and line number to topic list
      Tname[TN] = zstrdup(topic,"docfile");
      Tline[TN] = line;
      if (++TN == TMAX) zexit(1,"docfile exceeds %d topics \n",TMAX);
   }

   fclose(fid);

   printf("Docfile topics: %d \n",TN);

   zd = popup_report_open(docfile,parent,1000,800,1,0,showz_docfile_clickfunc,         //  popup window for docfile text display
                      "Recent", "<", ">", "Find", "Save", "X", null);                  //    + buttons
   if (! zd) zexit(1,"cannot open docfile window \n");

   popup_report_font_attributes(zd);                                                   //  use high contrast font
   txwidget = zdialog_gtkwidget(zd,"text");                                            //  text widget in zdialog

   snprintf(filespec,200,"%s/%s",get_zdatadir(),docfile);                              //  open docfile again
   fid = fopen(filespec,"r");
   if (! fid) zexit(1,"%s %s \n",filespec,strerror(errno));

   for (line = 0; ; line++)                                                            //  loop docfile recs/lines
   {
      pp1 = fgets_trim(buff1,10000,fid);                                               //  line with null at end
      if (! pp1) break;                                                                //  EOF
      if (strmatchN(pp1,"EOF",3)) break;                                               //  end of displayed text

      if (strmatchN(buff1,"TOPIC:",6))                                                 //  TOPIC: TOPIC NAME
      {
         pp1 = buff1 + 6;
         while (*pp1 == ' ') pp1++;
         popup_report_write(zd,1,"%s \n",pp1);                                         //  output TOPIC NAME, bold
         continue;
      }

      pp1 = strstr(buff1,"+image:");                                                   //  line has image names
      if (pp1) {
         while (pp1) {
            pp1 += 7;
            while (*pp1 == ' ') pp1++;                                                 //  remove blanks
            strncpy0(image,pp1,100);
            pp2 = strchr(image,' ');
            if (pp2) *pp2 = 0;
            snprintf(filespec,200,"%s/%s",get_zimagedir(),image);                      //  full filespec
            gerror = 0;
            pixbuf = gdk_pixbuf_new_from_file(filespec,&gerror);                       //  convert to pixbuf image
            if (pixbuf) {
               popup_report_write(zd,0,"  ",0);                                        //  leading spaces
               popup_report_insert_pixbuf(zd,line,pixbuf);                             //  write image to output line
               g_object_unref(pixbuf);
            }
            else printf("*** cannot load image file: %s \n",image);
            pp1 = strstr(pp1,"+image:");                                               //  next image file
         }
         popup_report_write(zd,0,"\n",0);                                              //  write image line EOL
         continue;                                                                     //  next input line
      }

      strncpy0(buff2,buff1,10000);                                                     //  line is text
      Fm = 0;                                                                          //  buff1: line with bold markups

      for (pp1 = buff2; (pp1 = strpbrk(pp1,"{}^"));) {                                 //  buff2: line without markups
         cc = strlen(pp1+1);
         memmove(pp1,pp1+1,cc+1);
         Fm++;                                                                         //  markups found in this line
      }

      popup_report_write(zd,0,"%s \n",buff2);                                          //  write line to output, no markups

      if (! Fm) continue;                                                              //  no markups found, done

      BF = 0;                                                                          //  intiz. no markups active

      pos1 = pos2 = 0;                                                                 //  char. posn. with/without markups

      while (buff1[pos1])                                                              //  loop chars. in line
      {
         if (! strchr("{}^",buff1[pos1])) {                                            //  not a markup char.
            if (BF) ++Bcc[BN];                                                         //  count cc for active markups
            pos1++; pos2++;
            continue;
         }

         if (buff1[pos1] == '{') {                                                     //  ... {bold text} ... link to topic
            if (BN == TMAX) zexit(1,"docfile > %d markups \n",TMAX);
            BF = 1;                                                                    //  start bold text
            Bline[BN] = line;
            Bpos[BN] = pos2;
            Bcc[BN] = 0;
         }

         else if (buff1[pos1] == '}') {                                                //  end bold text
            BF = 0;
            BN++;                                                                      //  bold markup count
         }

         else if (buff1[pos1] == '^') {                                                //  ... ^bold text^ ...
            if (! BF) {                                                                //  starting ^
              BF = 1;
              Bline[BN] = line;
              Bpos[BN] = pos2;
              Bcc[BN] = 0;
            }
            else {                                                                     //  ending ^
               BF = 0;
               BN++;
            }
         }

         pos1++;                                                                       //  skip over markup char.
      }

      if (BF) BN++;                                                                    //  EOL without trailing } or ^
   }                                                                                   //  end loop line

   fclose(fid);

   for (ii = 0; ii < BN; ii++)                                                         //  do all text bolds
      popup_report_bold_word(zd,Bline[ii],Bpos[ii],Bcc[ii]);

   zmainloop();                                                                        //  necessary here for some reason

initz_done:

   currline = 0;                                                                       //  docfile line for topic

   if (utopic)                                                                         //  initial topic from caller
   {
      cc = strlen(utopic);

      for (ii = 0; ii < TN; ii++) {                                                    //  search docfile topics
         if (strmatch(utopic,Tname[ii])) {                                             //  no strmatchcase                       26.3
            currline = Tline[ii];                                                      //  get line of matching topic
            break;
         }
      }

      if (ii == TN)
         printf("*** topic not found: %s \n",utopic);
      else {
         zlist_prepend(RTopics,utopic,0);                                              //  add to recent topics, 1st position
         ii = zlist_find(RTopics,utopic,1);
         if (ii > 0) zlist_remove(RTopics,ii);                                         //  if topic present later, remove it
         ii = zlist_count(RTopics);
         if (ii > RTmax) zlist_remove(RTopics,ii-1);                                   //  limit entry count
      }
   }

   popup_report_scroll_top(zd,currline);                                               //  scroll to topic line

   return;
}


//  handle clicks on document window and KB inputs

int showz_docfile_clickfunc(GtkWidget *txwidget, int line, int posn, ch *input)
{
   using namespace showz_docfile_names;

   int         ii, jj, cc;
   int         vtop, vbott, page, posn8;
   ch          *text, *pp1, *pp2;
   ch          *topic;
   ch          text2[10000], weblink[200];
   static int  Ltab[10], maxL = 10, Lpos = 0;                                          //  last 10 links clicked

   gtk_widget_grab_focus(txwidget);                                                    //  necessary for some reason

   txwidget_get_visible_lines(txwidget,vtop,vbott);                                    //  range of lines on screen

   if (strmatch(input,"X")) {                                                          //  [X] kill report                       25.0
      showz_docfile(0,0,"quit");
      return 1;
   }

   if (strmatch(input,"Recent"))                                                       //  [Recent] recent topics
   {
      topic = popup_choose(RTopics);                                                   //  choose a topic                        25.1
      if (! topic) return 0;

      for (ii = 0; ii < TN; ii++)                                                      //  search docfile topics
         if (strmatchcase(topic,Tname[ii])) break;
      if (ii == TN) return 0;                                                          //  not found

      currline = Tline[ii];                                                            //  get line of matching topic
      popup_report_scroll_top(zd,currline);                                            //  scroll to topic line

      zlist_prepend(RTopics,topic,0);                                                  //  add to recent topics, 1st position
      ii = zlist_find(RTopics,topic,1);
      if (ii > 0) zlist_remove(RTopics,ii);                                            //  if topic present later, remove it
      ii = zlist_count(RTopics);
      if (ii > RTmax) zlist_remove(RTopics,ii-1);                                      //  limit entry count
      return 1;
   }

   if (input)                                                                          //  25.1
   {
      if (*input == GDK_KEY_Left || *input == '<') {                                   //  [<] or left arrow, go back
         Ltab[Lpos] = currline;
         if (Lpos > 0) Lpos--;
         currline = Ltab[Lpos];
         txwidget_scroll_top(txwidget,currline);                                       //  scroll line to top of window
         return 1;
      }

      if (*input == GDK_KEY_Right || *input == '>') {                                  //  [>] or right arrow, go forward
         Ltab[Lpos] = currline;
         if (Lpos < maxL-1 && Ltab[Lpos+1] >= 0)
            currline = Ltab[++Lpos];
         txwidget_scroll_top(txwidget,currline);                                       //  scroll line to top of window
         return 1;
      }

      if (strmatch(input,"Find")) {                                                    //  [Find]
         zdialog_send_event(zd,"Find");
         return 1;
      }

      if (*input >= 0xfd00) {                                                          //  navigation key
         page = vbott - vtop - 2;                                                      //  page size, lines
         if (page < 0) page = 0;
         currline = 0;                                                                 //  default
         if (*input == GDK_KEY_Up) currline = vtop - 1;                                //  handle navigation keys
         else if (*input == GDK_KEY_Down) currline = vbott + 1;
         else if (*input == GDK_KEY_Page_Up) currline = vtop - page;
         else if (*input == GDK_KEY_Page_Down) currline = vbott + page;
         else if (*input == GDK_KEY_KP_Page_Up) currline = vtop - page;
         else if (*input == GDK_KEY_KP_Page_Down) currline = vbott + page;
         else if (*input == GDK_KEY_KP_9) currline = vtop - page;
         else if (*input == GDK_KEY_KP_3) currline = vbott + page;
         else if (*input == GDK_KEY_Home) currline = 0;
         else if (*input == GDK_KEY_End) currline = 999999;
         if (currline < 0) currline = 0;
         txwidget_scroll(txwidget,currline);                                           //  put line on screen
         return 1;
      }
   }

   if (line < 0 || posn < 0) return 0;                                                 //  clicked line and position

   text = txwidget_line(txwidget,line,1);                                              //  get line text
   if (! text) return 0;

   strncpy0(text2,text,posn+1);                                                        //  compensate utf8 chars. before posn
   posn8 = posn + strlen(text2) - utf8len(text2);

   for (ii = 0; ii < BN; ii++) {                                                       //  is clicked text bolded text
      if (line == Bline[ii]) {
         if (posn8 >= Bpos[ii] && posn8 <= Bpos[ii] + Bcc[ii]) {                       //  yes
            for (jj = 0; jj < TN; jj++) {
               if (Bcc[ii] == (int) strlen(Tname[jj]) &&                               //  does it match a topic name     bugfix 26.3
                   strmatchN(Tname[jj],text+Bpos[ii],Bcc[ii])) break;
            }
            if (jj < TN) goto topiclink;                                               //  yes
         }
      }
   }
   goto weblink;

topiclink:
   topic = Tname[jj];                                                                  //  clicked topic
   currline = Tline[jj];
   txwidget_scroll_top(txwidget,currline);                                             //  scroll topic to top of window

   zlist_prepend(RTopics,topic,0);                                                     //  add to recent topics, 1st position
   ii = zlist_find(RTopics,topic,1);
   if (ii > 0) zlist_remove(RTopics,ii);                                               //  if topic present later, remove it
   ii = zlist_count(RTopics);
   if (ii > RTmax) zlist_remove(RTopics,ii-1);                                         //  limit entry count

   if (Lpos == maxL-1) {
      for (jj = 0; jj < maxL-1; jj++)                                                  //  back tab table full,
         Ltab[jj] = Ltab[jj+1];                                                        //    discard oldest
      Lpos--;
   }

   Ltab[Lpos] = vtop;                                                                  //  curr. top line >> back tab
   Lpos++;                                                                             //  advance back tab position
   Ltab[Lpos] = currline;                                                              //       >> back tab
   return 1;

weblink:
   for ( ; posn >= 0; posn--)
      if ( *(text+posn) == ' ') break;                                                 //  click position, preceding blank
   if (posn < 0) posn = 0;
   if (text[posn] == ' ') posn += 1;                                                   //  eliminate preceding blank
   pp1 = text + posn;
   pp2 = strchr(pp1,' ');                                                              //  following blank or EOL
   if (pp2) cc = pp2 - pp1;
   else cc = strlen(pp1);
   if (pp1[cc-1] == '.') cc--;                                                         //  remove trailing period
   if (cc > 199) return 1;
   strncpy0(weblink,pp1,cc+1);                                                         //  copy clicked text string
   if (strmatchN(pp1,"http",4))                                                        //  if "http..." assume a web link
      zshell("log ack","xdg-open %s",weblink);
   return 1;
}


//  validate the F1_help_topic links and the internal links in a docfile
//  (developer tool)

void audit_docfile(ch *docfile)
{
   #define LMAX 10000                                                                  //  max. docfile lines/recs
   ch          *textlines[LMAX];
   ch          *Tname[TMAX];
   ch          filespec[200], buff[10000], image[100];                                 //  limits: filename, rec. cc, image name
   ch          topic[100];
   ch          *pp1, *pp2, *pp3;
   FILE        *fid;
   int         Ntext, Ntop, Nerrs;
   int         ii, cc, line;
   GdkPixbuf   *pixbuf;
   GError      *gerror;

   printf("\n*** audit docfile %s *** \n",docfile);

   Ntext = Ntop = Nerrs = 0;

   snprintf(filespec,200,"%s/%s",get_zdatadir(),docfile);                              //  open docfile
   fid = fopen(filespec,"r");
   if (! fid) {
      printf("%s %s",filespec,strerror(errno));
      return;
   }

   for (line = 0; line < LMAX; line++)                                                 //  read docfile text lines
   {
      pp1 = fgets_trim(buff,10000,fid);                                                //  line without \n EOL
      if (! pp1) break;                                                                //  EOF

      textlines[Ntext] = zstrdup(pp1,"docfile");                                       //  copy text line to memory
      if (++Ntext == LMAX) zexit(1,"exceed LMAX text recs");

      if (! strmatchN(pp1,"TOPIC:",6)) continue;                                       //  not a topic line
      pp1 += 6;
      while (*pp1 == ' ') pp1++;
      cc = strlen(pp1);
      if (cc < 2 || cc > 99) continue;
      strncpy0(topic,pp1,cc+1);                                                        //  add topic to list
      Tname[Ntop] = zstrdup(topic,"docfile");
      if (++Ntop == TMAX) zexit(1,"exceed TMAX topics");
//    printf("topic: %s \n",topic);
   }

   fclose(fid);

   printf("text lines: %d  topics: %d \n",Ntext,Ntop);

   for (line = 0; line < Ntext; line++)                                                //  process text lines
   {
      pp1 = textlines[line];

      pp2 = strstr(pp1,"+image:");
      if (pp2) {                                                                       //  line contains images
         while (pp2) {
            pp2 += 7;
            while (*pp2 == ' ') pp2++;                                                 //  remove blanks
            strncpy0(image,pp2,100);
            pp3 = strchr(image,' ');
            if (pp3) *pp3 = 0;
            snprintf(filespec,200,"%s/%s",get_zimagedir(),image);                      //  full filespec
            gerror = 0;
            pixbuf = gdk_pixbuf_new_from_file(filespec,&gerror);                       //  convert to pixbuf image
            if (pixbuf) g_object_unref(pixbuf);
            else {
               printf("cannot load image file: %s \n",image);
               Nerrs++;
            }
            pp2 = strstr(pp2,"+image:");
         }
         continue;                                                                     //  next line
      }

      if (strmatchN(pp1,"TOPIC:",6)) continue;                                         //  skip topic line
      {
         pp1 = strchr(pp1,'{');                                                        //  get topic links in line

         while (pp1)
         {
            pp2 = strchr(pp1+1,'}');                                                   //  ... {topic name} ...
            if (! pp2) break;                                                          //      |          |
            pp1 += 1;                                                                  //      pp1        pp2
            cc = pp2 - pp1;
            if (cc < 2 || cc > 99) {
               printf("topic >99 chars, line %d: %s \n",line,pp1);                     //  topic name > 99 char.                 26.3
               Nerrs++;
               break;
            }

            strncpy0(topic,pp1,cc+1);
            for (ii = 0; ii < Ntop; ii++)
               if (strcmp(topic,Tname[ii]) == 0) break;
            if (ii == Ntop) {                                                          //  topic not found
               printf("topic not found, line %d: %s \n",line,topic);
               Nerrs++;
            }
            pp1 = strchr(pp2+1,'{');
         }

         continue;                                                                     //  next line
      }
   }

   printf(" %d errors \n",Nerrs);

   for (ii = 0; ii < Ntext; ii++)                                                      //  free memory
      zfree(textlines[ii]);

   for (ii = 0; ii < Ntop; ii++)
      zfree(Tname[ii]);

   return;
}


/**************************************************************************************

   TRANSLATE SUSPEND             omit following code from translation scan             //  26.0
                                 contains tokens: TX(  that must be ignored

***************************************************************************************

   en.text     contains all translatable english texts from source programs
               format: NNNN english text (multiple lines via \n characters)
                  NNNN is a sequence number from 0001 to max. 9999

   xx.text     contains translations for language code xx (de, fr, it ...)
               format: pairs of text, english and translation
                  NNNN english text (same as above) 
                  NNNN translated text (\n and other special characters preserved) 

   Translation files are present in the source and binary packages at:
         .../appname/data/translations/en.text   .../de.text  .../fr.text  etc.
             (english only)                         (pairs of text, en and xx)

   The working copies are at:  ~/.appname/translations/en.text  .../de.text   etc.
   These are the files that the translations functions operate on.

   file en.text:
      1234 english text1         multiline text on one line with embedded \n characters
      1235 english text2
      ...

   file xx.text:                 one file per xx translation (de, fr, it ...)
      1234 english text1
      1234 translated text1
      1235 english text2
      1235 translated text2
      ...


**************************************************************************************/

/***
          _____________________________________________________
         |             Translation Functions                   |
         |                                                     |
         | Translation files:                                  |
         | /home/<user>/.appname/translations                  |
         | 2-character language code: [__]                     |
         |                                                     |
         | 1.                                                  |
         | Create translation file from application source     |
         | files. This is needed when the source code is       |
         | modified and contains new texts to translate.       |
         | Output: en.text: all translatable English texts     |
         | [proceed]                                           |
         |                                                     |
         | 2.                                                  |
         | Find new translations needed. Compare xx.text to    |
         | en.text and create en.new.text, containing new      |
         | English texts needing translation. If xx.text does  |
         | not exist, en.new.text will contain all texts.      |
         | [proceed]                                           |
         |                                                     |
         | 3.                                                  |
         | Use an AI translator to translate en.new.text       |
         | into the language xx. The translation output file   |
         | must be named xx.new.text. The text ID numbers      |
         | in en.new.text must be retained in xx.new.text.     |
         | Suggested AI prompt: [AI prompt]                    |
         |                                                     |
         | 4.                                                  |
         | Process the new translation file xx.new.text        |
         | Prior xx.text and xx.new.text --> new xx.text       |
         | [proceed]                                           |
         |                                                     |
         | 5.                                                  |
         | Audit the formats in a translation file             |
         | (errors can cause program error or crash)           |
         |                                                     |
         |                                         [help] [X]  |
         |_____________________________________________________|

***/

namespace translate_names
{
   ch    sourcefolder[500];               //  source files folder:  /.../appname/*.cc
   ch    *transfolder;                    //  translations folder:  ~/.appname/translations
}


void translate(ch *folder)                                                             //  26.0
{
   using namespace translate_names;

   int      zdialog_translate_event(zdialog *zd, ch *event);

   zlist_t  *zlist;
   int      ii;
   ch       *pp;

   transfolder = folder;                                                               //  translations folder from caller
   printf("translations folder: %s \n",transfolder);

   zdialog  *zd = zdialog_new("Translation Functions",mainwin,"help","X",null);        //  start user dialog
   
   zdialog_add_widget(zd,"hbox","hbloc1","dialog");
   zdialog_add_widget(zd,"label","labloc1","hbloc1","Translation files:","space=3");
   zdialog_add_widget(zd,"hbox","hbloc2","dialog");
   zdialog_add_widget(zd,"label","labloc2","hbloc2",transfolder,"space=3");
   zdialog_add_widget(zd,"hbox","hbxx","dialog");
   zdialog_add_widget(zd,"label","labxx","hbxx","2-character language code:","space=3");
   zdialog_add_widget(zd,"zentry","xx","hbxx",0,"size=4");
   zdialog_add_widget(zd,"label","space","dialog");
   
   ch *text1 = "1. \n"
               "Create translation files from application source \n"
               "files. This is needed when the source code is \n"
               "modified and contains new texts to translate. \n"
               "Output: en.text: all translatable English texts"; 

   zdialog_add_widget(zd,"hbox","hb1","dialog");
   zdialog_add_widget(zd,"label","text1","hb1",text1,"space=3");
   zdialog_add_widget(zd,"hbox","hb1p","dialog");
   zdialog_add_widget(zd,"button","proceed1","hb1p","proceed","space=5");
   zdialog_add_widget(zd,"label","space","dialog");
   
   ch *text2 = "2. \n"
               "Find new translations needed. Compare xx.text to \n"
               "en.text and create en.new.text, containing new \n"
               "English texts needing translation. If xx.text does \n"
               "not exist, en.new.text will contain all texts."; 

   zdialog_add_widget(zd,"hbox","hb2","dialog");
   zdialog_add_widget(zd,"label","text2","hb2",text2,"space=3");
   zdialog_add_widget(zd,"hbox","hb2p","dialog");
   zdialog_add_widget(zd,"button","proceed2","hb2p","proceed","space=5");
   zdialog_add_widget(zd,"label","space","dialog");
   
   
   ch *text3 = "3. \n"
               "Use an AI translator to translate en.new.text \n"
               "into the language xx. The translation output file \n"
               "must be named xx.new.text. The text ID numbers \n"
               "in en.new.text must be retained in xx.new.text.";

   zdialog_add_widget(zd,"hbox","hb3","dialog");
   zdialog_add_widget(zd,"label","text3","hb3",text3,"space=3");
   zdialog_add_widget(zd,"hbox","hb3p","dialog");
   zdialog_add_widget(zd,"label","lab3p","hb3p","Suggested AI prompt:","space=3");
   zdialog_add_widget(zd,"button","AI prompt","hb3p","AI prompt","space=5");
   zdialog_add_widget(zd,"label","space","dialog");
   
   ch *text4 = "4. \n"
               "Process the new translation file xx.new.text \n"
               "Prior xx.text + xx.new.text --> new xx.text";

   zdialog_add_widget(zd,"hbox","hb4","dialog");
   zdialog_add_widget(zd,"label","text4","hb4",text4,"space=3");
   zdialog_add_widget(zd,"hbox","hb41","dialog");
   zdialog_add_widget(zd,"button","proceed4","hb41","proceed","space=5");
   zdialog_add_widget(zd,"label","space","dialog");
   
   ch *text5 = "5. \n"
               "Audit the formats in a translation file \n"
               "(errors can cause program error or crash)";

   zdialog_add_widget(zd,"hbox","hb5","dialog");
   zdialog_add_widget(zd,"label","text5","hb5",text5,"space=3");
   zdialog_add_widget(zd,"hbox","hb51","dialog");
   zdialog_add_widget(zd,"button","proceed5","hb51","proceed","space=5");
   zdialog_add_widget(zd,"label","space","dialog");

   zlist = zlist_from_folder(folder);                                                  //  build language code popup list
   for (ii = 0; ii < zlist_count(zlist); ii++) {
      pp = strstr(zlist_get(zlist,ii),".text");                                        //  look for xx.text files
      if (! pp) continue;
      *pp = 0; pp -= 2;                                                                //  pp --> "xx"
      if (strmatch(pp,"en")) continue;
      zdialog_stuff(zd,"xx",pp);                                                       //  add each xx to popup list
   }
   zdialog_stuff(zd,"xx","new");                                                       //  last entry is "new"
   zlist_free(zlist);

   zdialog_load_inputs(zd);
   zdialog_run(zd,zdialog_translate_event,"save");                                     //  run dialog
   zdialog_wait(zd);                                                                   //  wait for completion
   return;
}


//  zdialog event and completion function

int zdialog_translate_event(zdialog *zd, ch *event)                                    //  26.0
{
   using namespace translate_names;

/***
      xx             language code                                                     de, fr, etc.
      files:         en.text    xx.text    en.new.text   xx.new.text                   english texts, old and new translations
      filenames:     en_file    xx_file    en_new_file   xx_new_file                   corresp. file names
      zlists:        en_zlist   xx_zlist   en_new_zlist  xx_new_zlist                  corresp. zlists
***/

   zdialog  *zd2;
   ch       *AIprompt = 0;
   ch       *sourcefile;
   ch       xx[4] = "xx";                                                              //  2-char. language code
   ch       buff500[500];                                                              //  max. source file record cc
   ch       text999[999];                                                              //  max. translatable text cc
   ch       message[500];
   ch       buffx[XFCC], command[XFCC];
   ch       en_file[500], xx_file[500], en_new_file[500], xx_new_file[500];            //  files in ~/.appname/translations
   zlist_t  *en_zlist = 0, *xx_zlist = 0, *en_new_zlist = 0, *xx_new_zlist = 0;        //  corresponding zlists 
   zlist_t  *zltemp;
   FILE     *fid, *fid1, *fid2, *fid3;
   ch       *pp, *pp1, *pp2, *pp3, *pp4;
   ch       *text, *text1, *text2, text4[200], text5[200];
   int      Tnum, cc, ii, jj, nn, yn, ndups, nuniq, zstat;
   int      ii1, ii2, ii3, nn1, nn2, nn3, id1, id2, Nerr;

   if (zd->zstat) {
      if (zd->zstat == 1) {                                                            //  [help]
         zd->zstat = 0;                                                                //  keep open
         showz_docfile(zd->dialog,"userguide-en","TRANSLATIONS");                      //  26.3
      }
      else zdialog_free(zd);                                                           //  [X] or cancel
      return 1;
   }

   if (strstr("proceed2 proceed4 proceed5",event)) {
      zdialog_fetch(zd,"xx",xx,4);
      if (strlen(xx) != 2) {
         zmessageACK(zd->dialog,"language code is 2 characters");
         return 1;
      }
      if (strmatch(xx,"en")) {
         zmessageACK(zd->dialog,"language code 'en' not allowed");
         return 1;
      }
      yn = zmessageYN(zd->dialog,"Proceed with the language code %s?",xx);
      if (! yn) return 1;
   }
   
   if (strmatch(event,"proceed1")) goto step1;                                         //  generate en.text (all english texts)
   if (strmatch(event,"proceed2")) goto step2;                                         //  generate en.new.text (new english texts)
   if (strmatch(event,"AI prompt")) goto step3;                                        //  translate en.new.text --> xx.new.text
   if (strmatch(event,"proceed4")) goto step4;                                         //  xx.text + xx.new.text --> xx.text
   if (strmatch(event,"proceed5")) goto step5;                                         //  validate xx.text formats

   return 1;


step1:  // ---------------------------------------------------------------------------

//  Generate a new translation file en.text, with all translatable English texts.
//  Application source files --> en.text. Sort and eliminate duplicates.

   zd2 = zdialog_new("get source files folder",zd->dialog,"OK","X",null);              //  get source code folder from user
   zdialog_add_widget(zd2,"hbox","hb1","dialog");
   zdialog_add_widget(zd2,"label","lab1","hb1","folder:");
   zdialog_add_widget(zd2,"zentry","sourcefolder","hb1",0,"space=5|expand");
   zdialog_load_inputs(zd2);
   zdialog_resize(zd2,500,0);
   zdialog_run(zd2,0,"save");
   zstat = zdialog_wait(zd2);
   zdialog_fetch(zd2,"sourcefolder",sourcefolder,500);
   zdialog_free(zd2);
   if (zstat != 1) return 1;                                                           //  cancel

   snprintf(en_file,500,"%s/en.text",transfolder);                                     //  get full en.text file name

   snprintf(command,XFCC,"find \"%s\" -maxdepth 1 -name \"*.cc\" ",sourcefolder);      //  find source files, *.cc
   printf("find command: %s \n",command);

   fid1 = popen(command,"r");
   if (! fid1) {
      zmessageACK(zd->dialog,"'find' command failure: %s",sourcefolder);
      return 1;
   }

   fid2 = fopen(en_file,"w");                                                          //  open en.text file for write
   if (! fid2) {                                                                       //  wrong folder?
      zmessageACK(zd->dialog,"cannot open %s output file",en_file);
      return 1;
   }

   Tnum = 0;                                                                           //  count translatable texts found

   while (true)                                                                        //  loop all source files
   {
      sourcefile = fgets_trim(buffx,XFCC,fid1);                                        //  next source file
      if (! sourcefile) {                                                              //  no more
         pclose(fid1);                                                                 //  close sourcefolder read
         fclose(fid2);                                                                 //  close output file
         break;
      }

      printf("processing file %s \n",sourcefile);
      
      fid3 = fopen(sourcefile,"r");                                                    //  open source file
      if (! fid3) {
         pclose(fid1);                                                                 //  close sourcefolder read
         fclose(fid2);                                                                 //  close output file
         zmessageACK(zd->dialog,"cannot read source file: %s",sourcefile);
         return 1;
      }      

      while (true)                                                                     //  loop all source file lines
      {
         pp = fgets(buff500,500,fid3);                                                 //  read source file line
         if (! pp) {
            fclose(fid3);                                                              //  EOF
            break;
         }
         
         if (strstr(pp,"TRANSLATE SUSPEND")) {                                         //  skip this code block with 
            while (true) {                                                             //    misleading TX(" tokens
               pp = fgets(buff500,500,fid3);
               if (strstr(pp,"TRANSLATE " "RESUME")) break;                            //  segmented to prevent detection
            }                                                                          //    by this function
            continue;
         }
         
         pp1 = strstr(pp,"TX(\"");                                                     //  look for text start TX("
         if (! pp1) continue;

      startscan:
         Tnum++;
         pp1 += 4;                                                                     //  skip TX(", start text
         pp2 = pp1;                                                                    //  initial end of text
         
         while (true)                                                                  //  scan text
         {
            if (*pp2 == 0) break;                                                      //  invalid, no text end found
            else if (*pp2 == '\\') pp2 += 2;                                           //  skip possible \"
            else if (strmatchN(pp2,"TX(",3)) break;                                    //  invalid, no text end found
            else if (*pp2 == '"' && *(pp2+1) == ')') {                                 //  found: ")  text end
               cc = pp2 - pp1;
               pp1[cc] = 0;
               fprintf(fid2,"%s\n",pp1);                                               //  output rest of text + \n
               pp = pp2 + 2;
               pp1 = strstr(pp,"TX(\"");                                               //  continue scan of same line
               if (pp1) goto startscan;                                                //  found TX("
               break;                                                                  //  EOL
            }
            else if (*pp2 == '"') {                                                    //  found: "  continuation line follows
               *pp2 = 0;
               fprintf(fid2,"%s",pp1);                                                 //  output text
               pp = fgets(buff500,500,fid3);                                           //  read next line
               if (! pp) break;
               while (*pp == ' ') pp++;                                                //  skip leading blanks
               if (*pp != '"') break;                                                  //  must start with "
               pp1 = pp2 = pp + 1;                                                     //  continue scan
            }
            else pp2++;
         }
      }
   }

   if (! Tnum) {
      zmessageACK(zd->dialog,"no translatable texts found");
      goto cleanup1;
   }

//  sort text strings in en.text and eliminate duplicates

   en_zlist = zlist_from_file(en_file);                                                //  load en_zlist from file en.text
   if (! en_zlist) {                                                                   //  en.text file missing
      zmessageACK(zd->dialog,"%s file is missing",en_file);
      goto cleanup1;
   }

   nn = zlist_count(en_zlist);
   if (! nn) {                                                                         //  en.text file empty
      zmessageACK(zd->dialog,"%s file is empty",en_file);
      goto cleanup1;
   }

   if (nn > 1) zlist_sort(en_zlist);                                                   //  sort en_zlist alphabetically
   
   for (ii = 1, jj = 0; ii < nn; ii++) {                                               //  scan from text[1] to end
      strncpy0(text999,zlist_get(en_zlist,ii),999);
      if (strmatch(text999,zlist_get(en_zlist,jj))) continue;                          //  skip duplicate
      zlist_put(en_zlist,text999,++jj);                                                //  output non-duplicate
   }
               
   ndups = nn - jj - 1;                                                                //  duplicates removed
   nuniq = jj + 1;                                                                     //  remaining = new count
   zmessageACK(zd->dialog,"translatable texts found: %d \n"
                          "duplicates removed: %d \n"
                          "unique translatable texts: %d",Tnum,ndups,nuniq);

//  output en.text file, with sequence numbers

   fid = fopen(en_file,"w");
   if (! fid) {                                                                        //  cannot write file
      zmessageACK(zd->dialog,"cannot open %s output file",en_file);
      goto cleanup1;
   }

   for (ii = 0; ii < nuniq; ii++) {
      fprintf(fid,"%04d ",ii);                                                         //  4 digit sequence number + blank
      fprintf(fid,"%s\n",zlist_get(en_zlist,ii));                                      //    + text string
   }
   
   fclose(fid);

cleanup1:
   if (en_zlist) zlist_free(en_zlist);
   return 1;


step2:  // ---------------------------------------------------------------------------

//  Compare xx.text with en.text and create en.new.text, new english texts needing translation.

   snprintf(en_file,500,     "%s/en.text",transfolder);                                //  get full file names
   snprintf(xx_file,500,     "%s/%s.text",transfolder,xx);
   snprintf(en_new_file,500, "%s/en.new.text",transfolder);

   en_zlist = zlist_from_file(en_file);                                                //  en.text - all texts
   if (! en_zlist) {
      zmessageACK(zd->dialog,"file %s not found - nothing done",en_file);
      return 1;
   }

   xx_zlist = zlist_from_file(xx_file);                                                //  xx.text - existing translations
   if (! xx_zlist) {
      zmessageACK(zd->dialog,"file %s not found, \n %s will contain all texts",        //  none - all translations are new
                                                  xx_file, en_new_file);
      zshell("log","cp %s %s",en_file,en_new_file);
      return 1;
   }

   nn1 = zlist_count(en_zlist);                                                        //  all texts
   nn2 = zlist_count(xx_zlist);                                                        //  old texts and translations

   en_new_zlist = zlist_new(nn1);                                                      //  new texts (slots for nn1 entries)

   for (ii1 = nn = 0; ii1 < nn1; ii1++) {                                              //  loop texts in en.text
      text1 = zlist_get(en_zlist,ii1);
      if (strlen(text1) < 6) continue;                                                 //  ignore short lines
      for (ii2 = 0; ii2 < nn2; ii2 += 2) {
         text2 = zlist_get(xx_zlist,ii2);                                              //  search in xx.text (old texts)
         if (strlen(text2) < 6) continue;
         if (strmatch(text1+5,text2+5)) break;
      }
      if (ii2 < nn2) continue;                                                         //  found - not new text
      zlist_put(en_new_zlist,text1,nn++);                                              //  not found, add to en.new.text
   }
   
   zlist_purge(en_new_zlist);                                                          //  remove unfilled slots
   
   nn = zlist_count(en_new_zlist);                                                     //  new texts found
   zmessageACK(zd->dialog,"new translatable texts found: %d",nn);
   if (! nn) goto cleanup2;                                                            //  none, do nothing

   zlist_to_file(en_new_zlist,en_new_file);                                            //  output to file en.new.text

cleanup2:
   if (en_zlist) zlist_free(en_zlist);
   if (xx_zlist) zlist_free(xx_zlist);
   if (en_new_zlist) zlist_free(en_new_zlist);
   return 1;


step3:  // ---------------------------------------------------------------------------

//  Translate xx.new.text (en texts) and produce a new xx.new.text (both en and xx texts) 

   AIprompt =  
      "Please translate my text file from English into xxxxxx. \n"
      "The context is digital image editing and photography. \n"
      "The text lines are elements of an application GUI. \n"
      "Translate each input line into a single output line. \n"
      "Output lines must have the same format as the input  \n"
      "lines, including special characters and spaces. \n"
      "Do not convert embedded \"\\n\" into a new line character. \n"
      "Do not translate the following technical words: \n"
      "metadata, tag, tags, keyword, keywords, category,  \n"
      "categories, thumb, thumbs, thumbnail, thumbnails, \n"
      "geotag, geotags, GPS, latitude, longitude.";
   
   if (strmatch(event,"AI prompt")) {
      text = zdialog_text(zd->dialog,"prompt",AIprompt);
      printf("AI prompt: \n");
      printf("%s \n",text);
   }

   return 1;


step4:  // ---------------------------------------------------------------------------

/***
         Combine xx.new.text (new translations) with xx.text (old translations).
         Create a new xx.text with all translations.
         
         input files:         format:

         en.text              1234 english text                      1 line per text - all current english text lines

         xx.text              1234 englist text                      2 lines per text - old english texts and translations
                              1234 corresponding translation

         en.new.text          1234 english text                      1 line per text - new english texts
         xx.new.text          1234 corresponding translation         1 line per text - new translations

         processing:

         combine en.new.text + xx.new.text  -->  xx.new.text         2 lines per text - new english texts and translations

         loop lines in xx.text (old translations)
            found in en.new.text: delete xx.text line
            not found in en.text: delete xx.text line
         xx.text + xx.new.text --> xx.text
         remove en.new.text and xx.new.text
***/
   
   zlist_t *xx_new2_zlist;

   snprintf(en_file,500,    "%s/en.text",transfolder);                                 //  en.text
   snprintf(xx_file,500,    "%s/%s.text",transfolder,xx);                              //  xx.text
   snprintf(en_new_file,500,"%s/en.new.text",transfolder);                             //  en.new.text
   snprintf(xx_new_file,500,"%s/%s.new.text",transfolder,xx);                          //  xx.new.text

   en_zlist = zlist_from_file(en_file);                                                //  load en.text - all english texts
   if (! en_zlist) {
      zmessageACK(zd->dialog,"cannot find %s - nothing done",en_file);
      goto cleanup4;
   }

   en_new_zlist = zlist_from_file(en_new_file);                                        //  load en.new.text - new english texts
   if (! en_new_zlist) {
      zmessageACK(zd->dialog,"cannot find %s - nothing done",en_new_file);
      goto cleanup4;
   }

   xx_new_zlist = zlist_from_file(xx_new_file);                                        //  load xx.new.text - new xx translations
   if (! xx_new_zlist) {
      zmessageACK(zd->dialog,"cannot find %s - nothing done",xx_new_file);
      goto cleanup4;
   }
   
//  merge en.new.text and xx.new.text --> xx.new.text
//        (english)      (translation)     (both)

   nn1 = zlist_count(en_new_zlist);                                                    //  new english texts
   nn2 = zlist_count(xx_new_zlist);                                                    //  new translation texts
   
   xx_new2_zlist = zlist_new(nn1+nn1);                                                 //  combined texts
   nn3 = 0;
   id1 = id2 = -1;
   
   for (ii1 = ii2 = 0; ii1 < nn1; ii1++) {                                             //  loop english texts
      id1 = atoi(zlist_get(en_new_zlist,ii1));                                         //  get ID number
      for (ii2 = ii2; ii2 < nn2; ii2++) {                                              //  loop translations from last match
         id2 = atoi(zlist_get(xx_new_zlist,ii2));                                      //  get ID number
         if (id2 == id1) break;                                                        //  match found
      }
      if (id2 != id1)                                                                  //  not found
         for (ii2 = 0; ii2 < nn2; ii2++) {                                             //  loop translations from start
            id2 = atoi(zlist_get(xx_new_zlist,ii2));                                   //  get ID number
            if (id2 == id1) break;                                                     //  match found
         }
      if (id2 == id1) {                                                                //  match found
         zlist_put(xx_new2_zlist,zlist_get(en_new_zlist,ii1),nn3);                     //  english + translation texts
         zlist_put(xx_new2_zlist,zlist_get(xx_new_zlist,ii2),nn3+1);                   //    --> output zlist
         nn3 += 2;
      }      
   }

   zlist_free(xx_new_zlist);                                                           //  replace xx_new_zlist
   xx_new_zlist = xx_new2_zlist;                                                       //  (now 2 lines per text ID)
   xx_new2_zlist = 0;
   
   zlist_purge(xx_new_zlist);                                                          //  purge deleted lines
   nn2 = zlist_count(xx_new_zlist);                                                    //  new count

   zlist_to_file(xx_new_zlist,xx_new_file);                                            //  replace file xx.new.text
   
   xx_zlist = zlist_from_file(xx_file);                                                //  load xx.text - old translations
   if (! xx_zlist) {                                                                   //  not found (first translation?)
      rename(xx_new_file,xx_file);                                                     //  xx.new.text --> xx.text
      zmessageACK(zd->dialog,"added %d new translations",nn2/2);
      goto cleanup4;
   }

   nn1 = zlist_count(xx_zlist);                                                        //  old translations, prior release
   nn2 = zlist_count(xx_new_zlist);                                                    //  new translations, this release
   nn3 = zlist_count(en_zlist);                                                        //  all english texts

   for (ii1 = 0; ii1 < nn1; ii1 += 2) {                                                //  loop old translations
      text1 = zlist_get(xx_zlist,ii1);
      if (strlen(text1) < 6) continue;
      for (ii2 = 0; ii2 < nn2; ii2 += 2) {                                             //  loop new translations
         text2 = zlist_get(xx_new_zlist,ii2);
         if (strlen(text2) < 6) continue;
         if (strmatch(text1+5,text2+5)) break;
      }
      if (ii2 < nn2) {                                                                 //  old translation found in new translations
         zlist_put(xx_zlist,0,ii1);                                                    //  remove old translation
         zlist_put(xx_zlist,0,ii1+1);
      }
      else {
         for (ii3 = 0; ii3 < nn3; ii3++) {                                             //  loop all english texts
            text2 = zlist_get(en_zlist,ii3);
            if (strlen(text2) < 6) continue;
            if (strmatch(text1+5,text2+5)) break;
         }
         if (ii3 == nn3) {                                                             //  old translation not found
            zlist_put(xx_zlist,0,ii1);                                                 //  remove old translation
            zlist_put(xx_zlist,0,ii1+1);
         }
      }
   }

   zlist_purge(xx_zlist);                                                              //  purge deleted old entries

   zltemp = zlist_insert_zlist(xx_zlist,xx_new_zlist,-1);                              //  xx.text + xx.new.text
   zlist_to_file(zltemp,xx_file);                                                      //    --> new xx.text file
   zlist_free(zltemp);

   zmessageACK(zd->dialog,"added %d new translations",nn2/2);

cleanup4:
   if (en_zlist) zlist_free(en_zlist);
   if (xx_zlist) zlist_free(xx_zlist);
   if (xx_new_zlist) zlist_free(xx_new_zlist);
// remove(en_new_file);                                                                //  keep for re-use for another language  26.2
   remove(xx_new_file);
   return 1;


step5:  // ---------------------------------------------------------------------------

//  audit a translation file for format errors (can cause program error or crash)

   snprintf(xx_file,500,"%s/%s.text",transfolder,xx);                                  //  translation file xx.text
   xx_zlist = zlist_from_file(xx_file);
   if (! xx_zlist) {
      zmessageACK(zd->dialog,"translation file not found: %s",xx_file);
      return 1;
   }

   Nerr = 0;
   for (ii = 0; ii < zlist_count(xx_zlist)-1; ii += 2)
   {
      pp1 = zlist_get(xx_zlist,ii);                                                    //  text pair, english and translation
      pp2 = zlist_get(xx_zlist,ii+1);
      strncpy0(text4,pp1,200);                                                         //  limit cc
      strncpy0(text5,pp2,200);
      nn = 0;

      if (! strmatchN(pp1,pp2,5)) {                                                    //  check ID numbers match
         snprintf(message,500,"ID missmatch: \n %s \n %s",text4,text5);
         nn = zdialog_choose(zd->dialog,"parent",message,"next","X",null);
         Nerr++;
         if (nn < 2) continue;
      }

      pp3 = pp1;
      pp4 = pp2;

      while (true) {                                                                   //  check '%' and '\' sequence match
         while (*pp3 && *pp3 != '%' && *pp3 != '\\') pp3++;
         while (*pp4 && *pp4 != '%' && *pp4 != '\\') pp4++;
         if (! *pp3 && ! *pp4) break;                                                  //  done, OK
         if (*pp3 == *pp4) {
            pp3++; pp4++;                                                              //  OK, continue scan
            continue;
         }
         snprintf(message,500,"format missmatch: \n %s \n %s",text4,text5);            //  format missmatch
         nn = zdialog_choose(zd->dialog,"parent",message,"next","X",null);             //  can have utf8 error at end
         Nerr++;
         break;
      }
      if (nn == 2) break;
   }
   
   zmessageACK(zd->dialog,"%d errors found",Nerr);
   
   zlist_free(xx_zlist);
   return 1;
}


/**************************************************************************************/

namespace txinit_names
{
   zlist_t  *zlist_xx = 0;                               //  translated text strings
   int      TXvalid = 0;
}


//  Initialize the application program for GUI translations.
//  "xx" is language code: "de", "fr", etc.
//  load file xx.text into memory

int TX_init(ch *xx, ch *folder)                                                        //  26.0
{
   using namespace txinit_names;

   ch    zlist_xx_file[500];
   int   N, ii, jj, kk;
   ch    *text;

   TXvalid = 0;                                                                        //  translations inactive
   
   if (strmatch(xx,"en")) return 0;                                                    //  language = english
   if (strlen(xx) != 2) return 0;
   
   snprintf(zlist_xx_file,500,"%s/%s.text",folder,xx);                                 //  get translation file xx.text

   zlist_xx = zlist_from_file(zlist_xx_file);
   if (! zlist_xx) {
      zmessageACK(mainwin,TX("cannot load %s.text file"),xx);
      return 0;
   }

   N = zlist_count(zlist_xx);                                                          //  replace "\\n" with real \n 
   for (ii = 0; ii < N; ii++) {                                                        //  <1 ms for 1800 translations, 4 GHz CPU
      text = zlist_get(zlist_xx,ii);
      for (jj = 0; text[jj]; jj++) {
         if (text[jj] == '\\' && text[jj+1] == 'n') {
            text[jj] = '\n';
            for (kk = jj+1; text[kk]; kk++)
               text[kk] = text[kk+1];
         }
      }
   }

   TXvalid = 1;
   return 1;
}


//  Translate an english text string to the language set by TX_init().
//  If no translation is found, the input text is returned.
//  Benchmark: worst case for en.text with 2765 lines: 40-60 µs using 4 GHz CPU. 

ch * TX(ch *text)                                                                      //  26.0
{
   using namespace txinit_names;
   
   int      N, ii;
   
   if (! TXvalid) return text;                                                         //  translations not active

   N = zlist_count(zlist_xx);
   for (ii = 0; ii < N; ii += 2)                                                       //  search en text for input text
      if (strmatch(text,zlist_get(zlist_xx,ii)+5)) break;                              //  (omit leading "NNNN ") 
   if (ii >= N) return text;                                                           //  not found

   return zlist_get(zlist_xx,ii+1) + 5;                                                //  return corresp. xx text
}


//  TRANSLATE RESUME                                                                   //  continue translate scan from here


/**************************************************************************************
   GTK utility functions
***************************************************************************************/

//  Iterate GTK main loop every "skip" calls.
//  If called within the main() thread, does a GTK main loop to process menu events, etc.
//  You must do this periodically within long-running main() thread tasks if you wish to
//  keep menus, buttons, output windows, etc. alive and working. The skip argument will
//  cause the function to do nothing for skip calls, then perform the normal function.
//  This allows it to be embedded in loops with little execution time penalty.
//  If skip = N, zmainloop() will do nothing for N calls, execute normally, etc.
//  If called from a thread, zmainloop() does nothing.

void zmainloop(int skip)
{
   static int  xskip = 0;

   if (skip && ++xskip < skip) return;
   xskip = 0;

   if (! main_thread()) return;                                                        //  thread caller, do nothing

   while (gtk_events_pending())
      gtk_main_iteration_do(0);

   xskip = 0;
   return;
}


//  Iterate the main loop and sleep for designated time

void zmainsleep(float secs)
{
   while (secs > 0) {
      zmainloop();
      zsleep(0.0001);
      secs = secs - 0.0001;
   }

   return;
}


/**************************************************************************************/

//  cairo drawing context for GDK window

cairo_t * draw_context_create(GdkWindow *gdkwin, draw_context_t &context)
{
   if (! main_thread()) zappcrash("illegal call from thread");

   if (context.dcr) {
      printf("*** draw_context_create(): nested call");
      return context.dcr;
   }
   context.win = gdkwin;
   context.rect.x = 0;
   context.rect.y = 0;
   context.rect.width = gdk_window_get_width(gdkwin);
   context.rect.height = gdk_window_get_height(gdkwin);
   context.reg = cairo_region_create_rectangle(&context.rect);
   context.ctx = gdk_window_begin_draw_frame(gdkwin,context.reg);
   context.dcr = gdk_drawing_context_get_cairo_context(context.ctx);
   return context.dcr;
}

void draw_context_destroy(draw_context_t &context)
{
   if (! main_thread()) zappcrash("illegal call from thread");

   if (! context.dcr) {
      printf("*** draw_context_destroy(): not created \n");
      return;
   }
   gdk_window_end_draw_frame(context.win,context.ctx);
   cairo_region_destroy(context.reg);
   /*  cairo_destroy(context.dcr);              this is fatal  */
   context.dcr = 0;
   return;
}


/**************************************************************************************/

//  txwidget functions
//  --------------------
//
//  High-level use of GtkTextView widget for text reports, line editing, text selection
//  In functions below, txwidget = zdialog_gtkwidget(zd,"widgetname"),
//    where "widgetname" is a zdialog "text" widget type.
//  All line numbers and line positions are zero based.


//  clear the text widget to blank

void txwidget_clear(GtkWidget *txwidget)
{
   GtkTextBuffer  *textBuff;

   if (! main_thread()) zappcrash("illegal call from thread");

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget));
   if (! textBuff) return;
   gtk_text_buffer_set_text(textBuff,"",-1);
   return;
}


//  clear the text widget from given line to end

void txwidget_clear(GtkWidget *txwidget, int line)
{
   GtkTextBuffer           *textBuff;
   GtkTextIter             iter1, iter2;

   if (! main_thread()) zappcrash("illegal call from thread");

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget));
   if (! textBuff) return;
   gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line);                             //  iter at line start
   gtk_text_buffer_get_end_iter(textBuff,&iter2);
   gtk_text_buffer_delete(textBuff,&iter1,&iter2);                                     //  delete existing line and rest of buffer
   return;
}


//  get the current line count

int txwidget_linecount(GtkWidget *txwidget)
{
   GtkTextBuffer  *textBuff;
   int            nlines;

   if (! main_thread()) zappcrash("illegal call from thread");

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget));
   if (! textBuff) return 0;
   nlines = gtk_text_buffer_get_line_count(textBuff);
   return nlines;
}


//  append a new line of text to the end of existing text lines
//  line should normally include trailing \n
//  if current last line has no \n, text is appended to this line

void txwidget_append(GtkWidget *txwidget, int bold, ch *format, ...)
{
   va_list        arglist;
   ch             textline[20000];                                                     //  txwidget append cc limit
   GtkTextBuffer  *textBuff;
   GtkTextIter    enditer;
   GtkTextTag     *fontag = 0;
   ch             *normfont = zfuncs::appmonofont;
   ch             *boldfont = zfuncs::appmonoboldfont;

   if (! main_thread()) zappcrash("illegal call from thread");

   va_start(arglist,format);
   vsnprintf(textline,19999,format,arglist);
   va_end(arglist);

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget));
   if (! textBuff) return;

   gtk_text_buffer_get_end_iter(textBuff,&enditer);                                    //  end of text

   if (bold) fontag = gtk_text_buffer_create_tag(textBuff,0,"font",boldfont,0);        //  prepare bold/norm tag
   else fontag = gtk_text_buffer_create_tag(textBuff,0,"font",normfont,0);
   gtk_text_buffer_insert_with_tags(textBuff,&enditer,textline,-1,fontag,null);        //  insert line

   return;
}


//  same as above, with scroll to last line added (slower)

void txwidget_append2(GtkWidget *txwidget, int bold, ch *format, ...)
{
   va_list        arglist;
   ch             textline[20000];                                                     //  txwidget append cc limit
   GtkTextBuffer  *textBuff;
   GtkTextIter    enditer;
   GtkTextTag     *fontag = 0;
   ch             *normfont = zfuncs::appmonofont;
   ch             *boldfont = zfuncs::appmonoboldfont;
   GtkAdjustment  *vadjust;
   double         upperlimit;

   if (! main_thread()) zappcrash("illegal call from thread");

   va_start(arglist,format);
   vsnprintf(textline,19999,format,arglist);
   va_end(arglist);

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget));
   if (! textBuff) return;

   gtk_text_buffer_get_end_iter(textBuff,&enditer);                                    //  end of text

   if (bold) fontag = gtk_text_buffer_create_tag(textBuff,0,"font",boldfont,0);        //  prepare bold/norm tag
   else fontag = gtk_text_buffer_create_tag(textBuff,0,"font",normfont,0);
   gtk_text_buffer_insert_with_tags(textBuff,&enditer,textline,-1,fontag,null);        //  insert line

   vadjust = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(txwidget));
   upperlimit = gtk_adjustment_get_upper(vadjust);
   gtk_adjustment_set_value(vadjust,upperlimit);

   zmainloop();
   return;
}


//  insert a new line of text after designated line
//  use line -1 to insert before line 0
//  line should normally include trailing \n

void txwidget_insert(GtkWidget *txwidget, int bold, int line, ch *format, ...)
{
   va_list        arglist;
   ch             textline[20000];                                                     //  txwidget insert cc limit
   GtkTextBuffer  *textBuff;
   GtkTextIter    iter;
   int            nlines;
   GtkTextTag     *fontag = 0;
   ch             *normfont = zfuncs::appmonofont;
   ch             *boldfont = zfuncs::appmonoboldfont;

   if (! main_thread()) zappcrash("illegal call from thread");

   va_start(arglist,format);
   vsnprintf(textline,19999,format,arglist);
   va_end(arglist);

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget));
   if (! textBuff) return;

   if (line < 0) gtk_text_buffer_get_start_iter(textBuff,&iter);                       //  insert before line 0

   if (line >= 0) {
      nlines = gtk_text_buffer_get_line_count(textBuff);                               //  insert after line
      if (line < nlines - 1)
         gtk_text_buffer_get_iter_at_line(textBuff,&iter,line+1);                      //  start of next line
      else gtk_text_buffer_get_end_iter(textBuff,&iter);                               //  or end of text
   }

   if (bold) fontag = gtk_text_buffer_create_tag(textBuff,0,"font",boldfont,0);        //  prepare bold/norm tag
   else fontag = gtk_text_buffer_create_tag(textBuff,0,"font",normfont,0);
   gtk_text_buffer_insert_with_tags(textBuff,&iter,textline,-1,fontag,null);           //  insert line

   return;
}


//  replace a given line with a new line
//  line = -1: replace last line.  -2: replace last-1 line, etc.
//  new line should normally include trailing \n

void txwidget_replace(GtkWidget *txwidget, int bold, int line, ch *format, ...)
{
   va_list        arglist;
   ch             textline[20000];                                                     //  txwidget replace cc limit
   GtkTextBuffer  *textBuff;
   GtkTextIter    iter1, iter2;
   int            nlines;
   GtkTextTag     *fontag = 0;
   ch             *normfont = zfuncs::appmonofont;
   ch             *boldfont = zfuncs::appmonoboldfont;

   if (! main_thread()) zappcrash("illegal call from thread");

   va_start(arglist,format);
   vsnprintf(textline,19999,format,arglist);
   va_end(arglist);

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget));
   if (! textBuff) return;

   nlines = gtk_text_buffer_get_line_count(textBuff);                                  //  lines now in buffer
   if (line < 0) line = nlines + line - 1;
   if (line >= nlines) line = nlines - 1;

   gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line);                             //  line start
   iter2 = iter1;
   gtk_text_iter_forward_line(&iter2);                                                 //  end
   gtk_text_buffer_delete(textBuff,&iter1,&iter2);                                     //  delete line
   gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line);

   if (bold) fontag = gtk_text_buffer_create_tag(textBuff,0,"font",boldfont,0);        //  prepare bold/norm tag
   else fontag = gtk_text_buffer_create_tag(textBuff,0,"font",normfont,0);
   gtk_text_buffer_insert_with_tags(textBuff,&iter1,textline,-1,fontag,null);          //  insert line

   return;
}


//  delete a given line including the trailing \n

void txwidget_delete(GtkWidget *txwidget, int line)
{
   GtkTextBuffer  *textBuff;
   GtkTextIter    iter1, iter2;
   int            nlines;

   if (! main_thread()) zappcrash("illegal call from thread");

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget));
   if (! textBuff) return;

   nlines = gtk_text_buffer_get_line_count(textBuff);                                  //  lines now in buffer
   if (line < 0 || line >= nlines) return;

   gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line);                             //  line start
   iter2 = iter1;
   gtk_text_iter_forward_line(&iter2);                                                 //  end
   gtk_text_buffer_delete(textBuff,&iter1,&iter2);                                     //  delete line

   return;
}


//  find first line of text containing characters matching input string
//  search is from line1 to end, then from 0 to line1-1
//  returns first matching line or -1 if none
//  comparison is not case sensitive

int txwidget_find(GtkWidget *txwidget, ch *matchtext, int line1, int hilite)
{
   GtkTextBuffer  *textBuff;
   GtkTextIter    iter1, iter2;
   int            line, nlines, cc;
   ch             *textline = 0, *pp1, *pp2;

   if (! main_thread()) zappcrash("illegal call from thread");

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget));
   if (! textBuff) return -1;

   nlines = gtk_text_buffer_get_line_count(textBuff);                                  //  lines now in buffer
   if (! nlines) return -1;

   if (line1 < 0) line1 = 0;                                                           //  starting line to search
   if (line1 >= nlines) line1 = 0;
   line = line1;

   while (true)
   {
      gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line);                          //  line start
      iter2 = iter1;
      gtk_text_iter_forward_line(&iter2);                                              //  end
      textline = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0);                   //  get text
      if (textline) {
         pp1 = strcasestr(textline,matchtext);                                         //  look for matching text
         if (pp1) break;                                                               //  found
         free(textline);
      }
      line++;
      if (line == nlines) line = 0;
      if (line == line1) return -1;                                                    //  wrapped around, not found
   }

   if (hilite)
   {
      cc = strlen(matchtext);                                                          //  highlight matching text
      pp2 = pp1 + cc - 1;
      gtk_text_buffer_get_iter_at_line_index(textBuff,&iter1,line,pp1-textline);
      gtk_text_buffer_get_iter_at_line_index(textBuff,&iter2,line,pp2-textline+1);
      gtk_text_buffer_select_range(textBuff,&iter1,&iter2);
   }

   free(textline);
   return line;
}


//  insert a pixbuf image at designated line

void txwidget_insert_pixbuf(GtkWidget *txwidget, int line, GdkPixbuf *pixbuf)
{
   int            nlines;
   GtkTextBuffer  *textBuff;
   GtkTextIter    iter;

   if (! main_thread()) zappcrash("illegal call from thread");

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget));
   if (! textBuff) return;

   nlines = gtk_text_buffer_get_line_count(textBuff);                                  //  insert after line
   if (line < nlines - 1)
      gtk_text_buffer_get_iter_at_line(textBuff,&iter,line+1);                         //  start of next line
   else gtk_text_buffer_get_end_iter(textBuff,&iter);                                  //  or end of text

   gtk_text_buffer_insert_pixbuf(textBuff,&iter,pixbuf);
   return;
}


//  scroll a txwidget to put a given line on screen
//  1st line = 0.  for last line use line = -1.

void txwidget_scroll(GtkWidget *txwidget, int line)
{
   GtkTextBuffer  *textBuff;
   GtkTextIter    iter;
   GtkTextMark    *mark;
   GtkAdjustment  *vadjust;
   double         upperlimit;

   if (! main_thread()) zappcrash("illegal call from thread");

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget));
   if (! textBuff) return;

   vadjust = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(txwidget));

   if (line < 0) {                                                                     //  bottom
      zmainloop();                                                                     //  make it work (GTK problem)
      upperlimit = gtk_adjustment_get_upper(vadjust);
      gtk_adjustment_set_value(vadjust,upperlimit);
   }

   else {
      gtk_text_buffer_get_iter_at_line(textBuff,&iter,line);
      mark = gtk_text_buffer_create_mark(textBuff,0,&iter,0);
      gtk_text_view_scroll_mark_onscreen(GTK_TEXT_VIEW(txwidget),mark);
   }

   return;
}


//  scroll a txwidget to put a given line at the top of the window

void txwidget_scroll_top(GtkWidget *txwidget, int line)
{
   GtkTextBuffer  *textBuff;
   GtkTextIter    iter;
   GtkTextMark    *mark;

   if (! main_thread()) zappcrash("illegal call from thread");

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget));
   if (! textBuff) return;
   gtk_text_buffer_get_iter_at_line(textBuff,&iter,line);
   mark = gtk_text_buffer_create_mark(textBuff,0,&iter,0);
   gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(txwidget),mark,0,1,0,0);
   return;
}


//  get the range of txwidget lines currently visible in the window

void txwidget_get_visible_lines(GtkWidget *txwidget, int &vtop, int &vbott)
{
   GdkRectangle   rect;
   GtkTextIter    iter1, iter2;
   int            y1, y2;

   if (! main_thread()) zappcrash("illegal call from thread");

   gtk_text_view_get_visible_rect(GTK_TEXT_VIEW(txwidget),&rect);
   y1 = rect.y;
   y2 = y1 + rect.height;
   gtk_text_view_get_line_at_y(GTK_TEXT_VIEW(txwidget), &iter1, y1, 0);
   gtk_text_view_get_line_at_y(GTK_TEXT_VIEW(txwidget), &iter2, y2, 0);
   vtop = gtk_text_iter_get_line(&iter1);
   vbott = gtk_text_iter_get_line(&iter2) - 1;
   return;
}


//  dump the entire txwidget contents into a file

void txwidget_dump(GtkWidget *widget, ch *filename)
{
   FILE        *fid;
   ch          *prec;
   int         line, err;

   if (! main_thread()) zappcrash("illegal call from thread");

   fid = fopen(filename,"w");                                                          //  open file
   if (! fid) {
      zmessageACK(mainwin,TX("cannot open file %s"),filename);
      return;
   }

   for (line = 0; ; line++)
   {
      prec = txwidget_line(widget,line,1);                                             //  get text line, strip \n
      if (! prec) break;
      fprintf(fid,"%s\n",prec);                                                        //  output with \n
   }

   err = fclose(fid);                                                                  //  close file
   if (err) zmessageACK(mainwin,TX("file close error"));
   return;
}


//  dump the entire txwidget contents into a file, using a save-as dialog

void txwidget_save(GtkWidget *widget, GtkWindow *parent)
{
   ch       *file;

   if (! main_thread()) zappcrash("illegal call from thread");

   file = zgetfile("save text to file",parent,"save","noname");
   if (! file) return;
   txwidget_dump(widget,file);
   zfree(file);
   return;
}


//  Get a line of text. Returned text is subject for zfree().
//  trailing \n is included if strip == 0

ch * txwidget_line(GtkWidget *txwidget, int line, int strip)
{
   GtkTextBuffer  *textBuff;
   GtkTextIter    iter1, iter2;
   int            cc, nlines;
   ch             *textline, *ztext;

   if (! main_thread()) zappcrash("illegal call from thread");

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget));
   if (! textBuff) return 0;

   nlines = gtk_text_buffer_get_line_count(textBuff);                                  //  lines now in buffer
   if (line < 0 || line >= nlines) return 0;

   gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line);                             //  line start
   iter2 = iter1;
   gtk_text_iter_forward_line(&iter2);                                                 //  end
   textline = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0);                      //  get text line
   if (! textline) return 0;
   ztext = zstrdup(textline,"txwidget");
   free(textline);
   if (strip) {
      cc = strlen(ztext);
      if (cc && ztext[cc-1] == '\n') ztext[cc-1] = 0;
   }
   return ztext;
}


//  highlight a given line of text

void txwidget_highlight_line(GtkWidget *txwidget, int line)
{
   GtkTextBuffer  *textBuff;
   GtkTextIter    iter1, iter2;
   int            nlines;

   zmainloop();

   if (! main_thread()) zappcrash("illegal call from thread");

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget));
   if (! textBuff) return;

   nlines = gtk_text_buffer_get_line_count(textBuff);                                  //  lines now in buffer
   if (line < 0 || line >= nlines) return;

   gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line);                             //  line start
   iter2 = iter1;
   gtk_text_iter_forward_line(&iter2);                                                 //  end
   gtk_text_buffer_select_range(textBuff,&iter1,&iter2);                               //  highlight
   return;
}


//  get the word at the given position within the line
//  words are defined by line starts and ends, and the given delimiters
//  returns word and delimiter (&end)

ch * txwidget_word(GtkWidget *txwidget, int line, int posn, ch *dlims, ch &end)
{
   GtkTextBuffer  *textBuff;
   ch             *txline, *pp1, *pp2, *ztext;
   int            cc, nn;

   if (! main_thread()) zappcrash("illegal call from thread");

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget));
   if (! textBuff) return 0;

   txline = txwidget_line(txwidget,line,0);
   if (! txline) return 0;

   cc = strlen(txline);
   if (txline[cc-1] == '\n') {
      txline[cc-1] = 0;
      cc--;
   }

   pp1 = pp2 = txline;                                                                 //  fix for utf8
   while (true) {
      while (*pp1 == ' ' || strchr(dlims,*pp1)) pp1++;                                 //  pp1 = start of next word in line
      if (*pp1 == 0) return 0;
      pp2 = pp1;
      while (true) {
         if (*pp2 == 0) break;
         nn = utf8_position(pp2,1);                                                    //  pp2 = next delimiter or EOL
         if (nn < 0) break;
         if (strchr(dlims,pp2[nn])) break;
         pp2 += nn;
         posn += nn-1;
         if (pp2 - txline >= cc) break;
      }
      if (txline+posn >= pp1 && txline+posn <= pp2) break;                             //  pp1-pp2 is word clicked
      nn = utf8_position(pp2,1);
      if (nn < 0) return 0;
      pp1 = pp2 + nn;
      posn += nn-1;
      if (posn >= cc) return 0;
      if (pp1 >= txline+posn) return 0;
   }

   nn = utf8_position(pp2,1);
   if (nn > 0) cc = pp2 - pp1 + nn;
   else cc = pp2 - pp1 + strlen(pp2);
   end = pp1[cc];

   ztext = (ch *) zmalloc(cc+1,"txwidget");
   strncpy0(ztext,pp1,cc+1);
   zfree(txline);
   return ztext;
}


//  highlight text at line and positiion, length cc

void txwidget_highlight_word(GtkWidget *txwidget, int line, int posn, int cc)
{
   GtkTextBuffer  *textBuff;
   GtkTextIter    iter1, iter2;
   ch             *txline, *pp1, *pp2;

   if (! main_thread()) zappcrash("illegal call from thread");

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget));
   if (! textBuff) return;

   txline = txwidget_line(txwidget,line,0);
   if (! txline) return;

   pp1 = txline + posn;
   pp2 = pp1 + cc - 1;
   gtk_text_buffer_get_iter_at_line_index(textBuff,&iter1,line,pp1-txline);
   gtk_text_buffer_get_iter_at_line_index(textBuff,&iter2,line,pp2-txline+1);
   gtk_text_buffer_select_range(textBuff,&iter1,&iter2);

   zfree(txline);
   return;
}


//  convert text to bold text at line, positiion, cc

void txwidget_bold_word(GtkWidget *txwidget, int line, int posn, int cc)
{
   GtkTextBuffer  *textBuff;
   GtkTextIter    iter1, iter2;
   GtkTextTag     *fontag = 0;
   ch             *boldfont = zfuncs::appmonoboldfont;
   ch             *txline, *pp1, *pp2;

   if (! main_thread()) zappcrash("illegal call from thread");

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget));
   if (! textBuff) return;

   txline = txwidget_line(txwidget,line,0);
   if (! txline) return;

   fontag = gtk_text_buffer_create_tag(textBuff,0,"font",boldfont,0);

   /***
   fontag = gtk_text_buffer_create_tag(textBuff,0,"font",boldfont,                     //  example
                       "foreground","red","background","light blue",0);
   ***/

   pp1 = txline + posn;
   pp2 = pp1 + cc - 1;
   gtk_text_buffer_get_iter_at_line_index(textBuff,&iter1,line,pp1-txline);
   gtk_text_buffer_get_iter_at_line_index(textBuff,&iter2,line,pp2-txline+1);
   gtk_text_buffer_apply_tag(textBuff,fontag,&iter1,&iter2);

   zfree(txline);
   return;
}


//  convert text to underlined text at line, positiion, cc

void txwidget_underline_word(GtkWidget *txwidget, int line, int posn, int cc)
{
   GtkTextBuffer  *textBuff;
   GtkTextIter    iter1, iter2;
   GtkTextTag     *fontag = 0;
   ch             *txline, *pp1, *pp2;

   if (! main_thread()) zappcrash("illegal call from thread");

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget));
   if (! textBuff) return;

   txline = txwidget_line(txwidget,line,0);
   if (! txline) return;

   fontag = gtk_text_buffer_create_tag(textBuff,0,"underline",PANGO_UNDERLINE_SINGLE,0);

   pp1 = txline + posn;
   pp2 = pp1 + cc - 1;
   gtk_text_buffer_get_iter_at_line_index(textBuff,&iter1,line,pp1-txline);
   gtk_text_buffer_get_iter_at_line_index(textBuff,&iter2,line,pp2-txline+1);
   gtk_text_buffer_apply_tag(textBuff,fontag,&iter1,&iter2);

   zfree(txline);
   return;
}


//  set font attributes for the entire txwidget (black on white)
//  this does not do anything to the text font - why?                            FIXME

void txwidget_font_attributes(GtkWidget *txwidget)
{
   GtkTextBuffer  *textBuff;
   GtkTextIter    iter1, iter2;
   GtkTextTag     *fontag = 0;

   if (! main_thread()) zappcrash("illegal call from thread");

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget));
   if (! textBuff) return;

   fontag = gtk_text_buffer_create_tag(textBuff,0,                                     //  high contrast
         "font",appmonofont,"foreground","black","background","white",0);

   gtk_text_buffer_get_start_iter(textBuff,&iter1);
   gtk_text_buffer_get_end_iter(textBuff,&iter2);

   gtk_text_buffer_apply_tag(textBuff,fontag,&iter1,&iter2);

   return;
}


//  set an event function for mouse and KB events in txwidget
//    + line selection via mouse click or keyboard up/down arrow key
//    + line and word selection via mouse click
//
//  optional user callback function looks like this:
//    void userfunc(GtkWidget *txwidget, int line, int posn, ch *input)
//      receive clicked line and position (or)
//              dialog completion button (input) (or)
//              KB navigation key (input[0])

void txwidget_set_eventfunc(GtkWidget *txwidget, txwidget_CBfunc_t userfunc)
{
   int txwidget_eventfunc(GtkWidget *txwidget, GdkEvent *event, txwidget_CBfunc_t userfunc);

   if (! main_thread()) zappcrash("illegal call from thread");

   gtk_widget_add_events(txwidget,GDK_BUTTON_PRESS_MASK);
   gtk_widget_add_events(txwidget,GDK_KEY_PRESS_MASK);
   gtk_widget_add_events(txwidget,GDK_POINTER_MOTION_MASK);
   gtk_widget_add_events(txwidget,GDK_FOCUS_CHANGE_MASK);
   if (userfunc) {                                                                     //  25.1
      G_SIGNAL(txwidget,"key-press-event",txwidget_eventfunc,userfunc);
      G_SIGNAL(txwidget,"button-press-event",txwidget_eventfunc,userfunc);
      G_SIGNAL(txwidget,"motion-notify-event",txwidget_eventfunc,userfunc);
      G_SIGNAL(txwidget,"focus-in-event",txwidget_eventfunc,userfunc);
   }
   return;
}


//  txwidget event function:
//  if no user callback function, process KB navigation keys (arrow, page, home/end)
//  if user callback func, send all KB keys to user callback function
//  process mouse clicks, send clicked line and position to user callback function

int txwidget_eventfunc(GtkWidget *txwidget, GdkEvent *event, txwidget_CBfunc_t userfunc)
{
   #define TEXT GTK_TEXT_WINDOW_TEXT
   #define VIEW GTK_TEXT_VIEW

   static GdkCursor  *arrowcursor = 0;
   GdkWindow         *gdkwin;
   GtkTextIter       iter1;
   int               ustat;
   int               button, mpx, mpy, tbx, tby;
   int               line, pos, vtop, vbott, page, KBkey;
   ch                input[20] = "";

   if (! arrowcursor)                                                                  //  first call, get arrow cursor
      arrowcursor = gdk_cursor_new_for_display(display,GDK_TOP_LEFT_ARROW);
   gdkwin = gtk_text_view_get_window(VIEW(txwidget),TEXT);                             //  set arrow cursor for window
   if (gdkwin) gdk_window_set_cursor(gdkwin,arrowcursor);                              //  (must reset every event)

   gtk_widget_grab_focus(txwidget);

   if (event->type == GDK_KEY_PRESS)                                                   //  KB key press event
   {
      KBkey = ((GdkEventKey *) event)->keyval;

      if (userfunc) {                                                                  //  send arrow keys to userfunc
         strcpy(input,"xxx");
         if (KBkey == GDK_KEY_Up) strcpy(input,"up");
         if (KBkey == GDK_KEY_Down) strcpy(input,"down");
         if (KBkey == GDK_KEY_Left) strcpy(input,"left");
         if (KBkey == GDK_KEY_Right) strcpy(input,"right");
         ustat = userfunc(txwidget,-1,-1,input);
         if (ustat) return 1;                                                          //  handled by userfunc
      }

      if (KBkey > 0xff00)                                                              //  navigation key
      {
         txwidget_get_visible_lines(txwidget,vtop,vbott);                              //  range of lines on screen
         page = vbott - vtop - 2;                                                      //  page size, lines
         if (page < 0) page = 0;
         line = 0;                                                                     //  default
         if (KBkey == GDK_KEY_Up) line = vtop - 1;                                     //  handle some navigation keys
         if (KBkey == GDK_KEY_Down) line = vbott + 1;
         if (KBkey == GDK_KEY_Page_Up) line = vtop - page;
         if (KBkey == GDK_KEY_Page_Down) line = vbott + page;
         if (KBkey == GDK_KEY_Home) line = 0;
         if (KBkey == GDK_KEY_End) line = 999999;
         if (line < 0) line = 0;
         txwidget_scroll(txwidget,line);                                               //  put line on screen
         return 1;
      }
   }

   if (! userfunc) return 1;

   if (event->type == GDK_BUTTON_PRESS)                                                //  mouse button press
   {
      button = ((GdkEventButton *) event)->button;                                     //  ignore if not left button
      if (button != 1) return 0;
      mpx = int(((GdkEventButton *) event)->x);                                        //  mouse click position
      mpy = int(((GdkEventButton *) event)->y);
      mpx -= appfontsize / 2;                                                          //  more accurate
      if (mpx < 0) mpx = 0;
      gtk_text_view_window_to_buffer_coords(VIEW(txwidget),TEXT,mpx,mpy,&tbx,&tby);
      if (tbx && tby) {                                                                //  can happen
         gtk_text_view_get_iter_at_location(VIEW(txwidget),&iter1,tbx,tby);
         line = gtk_text_iter_get_line(&iter1);                                        //  clicked txwidget line
         pos = gtk_text_iter_get_line_offset(&iter1);                                  //  clicked position
      }
      else line = pos = 0;
      userfunc(txwidget,line,pos,null);                                                //  pass line and posn to user func
      return 1;
   }

   return 0;
}


/**************************************************************************************

   simplified GTK menu bar, tool bar, status bar functions

   These functions simplify the creation of GTK menus and toolbars.
   The functionality is limited but adequate for most purposes.

   mbar = create_menubar(vbox)                               create menubar
   mitem = add_menubar_item(mbar, label, func)               add menu item to menubar
   msub = add_submenu_item(mitem, label, func, tip)          add submenu item to menu or submenu

   tbar = create_toolbar(vbox, iconsize)                     create toolbar
   add_toolbar_button(tbar, label, tip, icon, func)          add button to toolbar

   stbar = create_stbar(vbox)                                create status bar
   stbar_message(stbar, message)                             display message in status bar

   These functions to the following:
      * create a menu bar and add to existing window vertical packing box
      * add menu item to menu bar
      * add submenu item to menu bar item or submenu item
      * create a toolbar and add to existing window
      * add button to toolbar, using stock icon or custom icon
      * create a status bar and add to existing window
      * display a message in the status bar

   argument definitions:
     vbox         GtkWidget *    a vertical packing box (in a window)
     mbar         GtkWidget *    reference for menu bar
     popup        GtkWidget *    reference for popup menu
     mitem        GtkWidget *    reference for menu item (in a menu bar)
     msub         GtkWidget *    reference for submenu item (in a menu)
     label        ch *           menu or toolbar name or label
     tbar         GtkWidget *    reference for toolbar
     tip          ch *           tool button tool tip (popup text via mouse-over)
     icon         ch *           stock icon name or custom icon file name (see below)
     func         see below      menu or tool button response function
     arg          ch *           argument to response function
     stbar        int            reference for status bar
     message      ch *           message to display in status bar

   The icon argument for the function add_toolbar_button() has two forms.
   For a GTK stock item referenced with a macro like GTK_STOCK_OPEN, use the
   corresponding text name, like "gtk-open".

   For a custom icon, use the icon's file name like "my-icon.png".
   The file is expected to be in  get_zdatadir()/icons.
   The icon file may be any size, and is resized for use on the toolbar.
   If the file is not found, the stock icon "gtk-missing-image" is used
   (".png" and ".jpg" files both work).

   For a button with no icon (text label only), use 0 or null for the icon argument.
   For a menu separator, use the menu name "separator".
   For a toolbar separator, use the label "separator".
   For a title menu (no response function), set the response function to null.

   The response function for both menus and toolbar buttons looks like this:
       void func(GtkWidget *, ch *)

   The following macro is also supplied to simplify the coding of response functions:
   G_SIGNAL(window,event,func,arg)   which expands to:
   g_signal_connect(G_OBJECT(window),event,G_CALLBACK(func),(void *) arg)

***************************************************************************************/

//  create menu bar and add to vertical packing box

GtkWidget * create_menubar(GtkWidget *vbox)
{
   GtkWidget   *wmbar;

   wmbar = gtk_menu_bar_new();
   gtk_box_pack_start(GTK_BOX(vbox),wmbar,0,0,0);
   return wmbar;
}


//  add menu item to menu bar, with optional response function

GtkWidget * add_menubar_item(GtkWidget *wmbar, ch *mname, cbFunc func)
{
   GtkWidget   *wmitem;

   wmitem = gtk_menu_item_new_with_label(mname);
   gtk_menu_shell_append(GTK_MENU_SHELL(wmbar),wmitem);
   if (func) G_SIGNAL(wmitem,"activate",func,mname);
   return  wmitem;
}


//  add submenu item to menu item, with optional response function

GtkWidget * add_submenu_item(GtkWidget *wmitem, ch *mlab, cbFunc func, ch *mtip)
{
   GtkWidget      *wmsub, *wmsubitem;

   wmsub = gtk_menu_item_get_submenu(GTK_MENU_ITEM(wmitem));                           //  add submenu if not already
   if (wmsub == null) {
      wmsub = gtk_menu_new();
      gtk_menu_item_set_submenu(GTK_MENU_ITEM(wmitem),wmsub);
   }

   if (strmatch(mlab,"separator"))
      wmsubitem = gtk_separator_menu_item_new();
   else  wmsubitem = gtk_menu_item_new_with_label(mlab);                               //  add menu item with label only

   gtk_menu_shell_append(GTK_MENU_SHELL(wmsub),wmsubitem);                             //  append submenu item to submenu

   if (func) G_SIGNAL(wmsubitem,"activate",func,mlab);                                 //  connect optional response function

   if (mtip) g_object_set(G_OBJECT(wmsubitem),"tooltip-text",mtip,null);               //  add optional popup menu tip

   return  wmsubitem;
}


/**************************************************************************************/

//  create toolbar and add to vertical packing box

int      tbIconSize = 32;                                                              //  default if not supplied

GtkWidget * create_toolbar(GtkWidget *vbox, int iconsize)
{
   GtkWidget   *wtbar;

   wtbar = gtk_toolbar_new();
   gtk_box_pack_start(GTK_BOX(vbox),wtbar,0,0,0);
   tbIconSize = iconsize;
   return  wtbar;
}


//  add toolbar button with label and icon ("iconfile.png") and tool tip
//  at least one of label and icon should be present

GtkWidget * add_toolbar_button(GtkWidget *wtbar, ch *blab, ch *btip, ch *icon, cbFunc func)
{
   GtkToolItem    *tbutton;
   GError         *gerror = 0;
   PIXBUF         *pixbuf;
   GtkWidget      *wicon = 0;
   ch             iconpath[300], *pp;
   STATB          statB;
   int            err, cc;

   if (blab && strmatch(blab,"separator")) {
      tbutton = gtk_separator_tool_item_new();
      gtk_toolbar_insert(GTK_TOOLBAR(wtbar),GTK_TOOL_ITEM(tbutton),-1);
      return  (GtkWidget *) tbutton;
   }

   if (icon && *icon) {                                                                //  get icon pixbuf
      *iconpath = 0;
      strncatv(iconpath,199,zimagedir,"/",icon,null);
      err = stat(iconpath,&statB);
      if (err) {                                                                       //  alternative path
         cc = readlink("/proc/self/exe",iconpath,300);                                 //  get own program path
         if (cc > 0) iconpath[cc] = 0;                                                 //  readlink() quirk
         pp = strrchr(iconpath,'/');                                                   //  folder of program
         if (pp) *pp = 0;
         strncatv(iconpath,300,"/icons/",icon,null);                                   //  .../icons/iconfile.png
      }
      pixbuf = gdk_pixbuf_new_from_file_at_scale(iconpath,tbIconSize,tbIconSize,1,&gerror);
      if (pixbuf) wicon = gtk_image_new_from_pixbuf(pixbuf);
   }

   tbutton = gtk_tool_button_new(wicon,blab);
   if (! wicon) gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(tbutton),"gtk-missing-image");

   if (btip) gtk_tool_item_set_tooltip_text(tbutton,btip);
   gtk_tool_item_set_homogeneous(tbutton,0);
   gtk_toolbar_insert(GTK_TOOLBAR(wtbar),GTK_TOOL_ITEM(tbutton),-1);
   if (func) G_SIGNAL(tbutton,"clicked",func,blab);
   return  (GtkWidget *) tbutton;
}


/**************************************************************************************/

//  create a status bar and add to the start of a packing box

GtkWidget * create_stbar(GtkWidget *pbox)
{
   GtkWidget      *stbar;

   stbar = gtk_statusbar_new();
   gtk_box_pack_start(GTK_BOX(pbox),stbar,0,0,0);
   gtk_widget_show(stbar);
   return  stbar;
}


//  display message in status bar

int stbar_message(GtkWidget *wstbar, ch *message)
{
   static int     ctx = -1;

   if (ctx == -1)
      ctx = gtk_statusbar_get_context_id(GTK_STATUSBAR(wstbar),"all");
   gtk_statusbar_pop(GTK_STATUSBAR(wstbar),ctx);
   gtk_statusbar_push(GTK_STATUSBAR(wstbar),ctx,message);
   return 0;
}


/**************************************************************************************

   Popup Menu

   GtkWidget   *popup, *mitem
   ch          *label, *arg, *tip
   void func(GtkWidget *, ch *arg)

   popup = create_popmenu()                                    create a popup menu
   mitem = add_popmenu(popup, label, func, arg, tip)           add menu item to popup menu
   popup_menu(GtkWidget *parent, popup)                        popup the menu at mouse position

   Call 'create_popmenu' and then 'add_popmenu' for each item in the menu.
   'label' is the menu name, 'func' the response function, 'arg' an argument
   for 'func', and 'tip' is a tool-tip. 'arg' and 'tip' may be null.
   A call to 'popup_menu' will show all menu entries at the mouse position.
   Clicking an entry will call the respective response function.
   Hovering on the entry will show the tool-tip.

   The response function looks like this:
       void func(GtkWidget *, ch *menu)

***/

//  create a popup menu

GtkWidget * create_popmenu()                                                           //  event processing removed
{
   if (! main_thread()) zappcrash("illegal call from thread");

   GtkWidget   *popmenu;
   popmenu = gtk_menu_new();
   return popmenu;
}


//  add a menu item to a popup menu

void add_popmenu(GtkWidget *popmenu, ch *mname, cbFunc func, ch *arg, ch *mtip)
{
   void popmenu_item_select(GtkWidget *, ch *mtip);

   GtkWidget   *widget;

   widget = gtk_menu_item_new_with_label(mname);
   gtk_menu_shell_append(GTK_MENU_SHELL(popmenu),widget);

   if (func) {
      if (arg) G_SIGNAL(widget,"activate",func,arg);                                   //  call func with arg
      else G_SIGNAL(widget,"activate",func,mname);                                     //  call func with menu name
   }

   if (mtip) {
      G_SIGNAL(widget,"select",popmenu_item_select,mtip);
      G_SIGNAL(widget,"deselect",popmenu_item_select,0);
   }

   return;
}


//  show popup tip for selected menu item

void popmenu_item_select(GtkWidget *wmitem, ch *mtip)                                  //  convoluted code but it works
{
   GdkWindow   *window;
   int         xp, yp, mx, my;

   window = gtk_widget_get_window(wmitem);
   gdk_window_get_origin(window,&xp,&yp);                                              //  menu screen origin
   xp += gdk_window_get_width(window);                                                 //   + width
   gdk_device_get_position(zfuncs::mouse,0,&mx,&my);                                   //  mouse (x,y) screen position
   poptext_screen(mtip,xp,my,0,5);                                                     //  popup px = menu + width, py = mouse
   return;
}


//  Show a popup menu at current mouse position
//  GtkWidget *  argument is not used

void popup_menu(GtkWidget *widget, GtkWidget *popmenu)
{
   gtk_widget_show_all(popmenu);                                                       //  GTK change: show before popup
   gtk_menu_popup_at_pointer(GTK_MENU(popmenu),null);
   return;
}


/**************************************************************************************/

//  popup picklist for user choice of zlist member

ch * popup_choose_choice = 0;

ch * popup_choose(zlist_t *zlist)
{
   void popup_choose_choose(GtkWidget *, ch *);

   static GtkWidget  *popmenu;
   static int        busy = 0;
   int               nl;
   ch                *member;

   if (! main_thread()) zappcrash("illegal call from thread");

   if (busy++) zappcrash("popup_choose() re-entry");                                   //  25.1

   nl = zlist_count(zlist);
   if (! nl) {
      zmessageACK(mainwin,TX("popup_choose: empty list"));
      busy = 0;
      return 0;
   }

   popmenu = create_popmenu();

   for (int ii = 0; ii < nl; ii++)
   {
      member = zlist_get(zlist,ii);
      if (member) add_popmenu(popmenu,member,popup_choose_choose,0,0);
   }

   add_popmenu(popmenu,TX("CLOSE"),popup_choose_choose,0,0);

   if (popup_choose_choice) {                                                          //  25.1
      zfree(popup_choose_choice);
      popup_choose_choice = 0;
   }

   popup_menu(0,popmenu);

   while (! popup_choose_choice) {
      if (! gtk_widget_get_mapped(popmenu)) popup_choose_choice = "NONE";              //  escape if menu abandoned
      zmainsleep(0.2);
   }

   if (strmatch(popup_choose_choice,"NONE")) popup_choose_choice = 0;

   busy = 0;
   return popup_choose_choice;
}


//  popup_choose response function

void popup_choose_choose(GtkWidget *, ch *choice)
{
   if (! choice || strmatch(choice,TX("CLOSE")))
      popup_choose_choice = zstrdup("NONE","zlist choose");
   else popup_choose_choice = zstrdup(choice,"zlist choose");
   return;
}


/**************************************************************************************/

//  popup picklist from a text file and choose an entry (text line)

ch * popup_choose(ch *file)                                                            //  25.1
{
   static zlist_t  *picklist = 0;
   ch    *choice = 0;

   if (! main_thread()) zappcrash("illegal call from thread");

   if (picklist) zlist_free(picklist);

   picklist = zlist_from_file(file);
   if (! picklist) {
      zmessageACK(mainwin,TX("picklist file not found: %s"),file);
      return 0;
   }

   choice = popup_choose(picklist);
   return choice;
}


/**************************************************************************************

   Vertical Menu / Toolbar

   Build a custom vertical menu and/or toolbar in a vertical packing box

   Vmenu    *vbm;
   ch       *name, *icon, *desc, *arg;
   int      iww, ihh;

   void func(GtkWidget *, ch *name);
   void RMfunc(GtkWidget *, ch *name);

   vbm = Vmenu_new(GtkWidget *vbox, float fgRGB[3], float bgRGB[3]);                   //  create base menu

   Vmenu_add(vbm, name, icon, iww, ihh, desc, func, arg);                              //  add left-mouse menu function

   Vmenu_add_setupfunc(vbm, me, func);                                                 //  add opt. setup function
   Vmenu_add_RMfunc(vbm, me, func);                                                    //  add right-mouse menu function

   Vmenu_block(int flag)      1 to block Vmenu, 0 to unblock

   Create a vertical menu / toolbar in a vertical packing box.
   fgRGB and bgRGB are font and background colors, RGB scaled 0-1.0
   Added items can have a menu name, icon, description, response function,
   and function argument. 'name' and 'icon' can be null but not both.

   name        menu name
   icon        menu icon, filespec for a .png file
   iww, ihh    size of icon in menu display
   desc        optional tool tip if mouse is hovered over displayed menu

   When 'name/icon' is clicked, 'func' is called with 'arg'.
   If 'arg' is null, 'name' is used instead.

   To create a menu entry that is a popup menu with multiple entries, do as follows:
        popup = create_popmenu();
        add_popup_menu_item(popup ...);         see create_popmenu()
        add_popup_menu_item(popup ...);
        ...
        Vmenu_add(vbm, name, icon, ww, hh, desc, create_popmenu, (ch *) popup);

   i.e. use create_popmenu() as the response function and use the previously
        created menu 'popup' as the argument (cast to ch *).

***/


namespace Vmenunames
{
   #define margin 10                                                                   //  margins for menu text                 25.1

   PangoFontDescription    *pfont1, *pfont2;
   PangoAttrList           *pattrlist;
   PangoAttribute          *pbackground;
   int      fontheight;
   int      Fblock = 0;

   void  wpaint(GtkWidget *, cairo_t *, Vmenu *);                                      //  window repaint - draw event
   void  mouse_event(GtkWidget *, GdkEventButton *, Vmenu *);                          //  mouse event function
   void  paint_menu(cairo_t *cr, Vmenu *vbm, int me, int hilite);                      //  paint menu entry, opt. highlight
}


//  create Vmenu

Vmenu *Vmenu_new(GtkWidget *vbox, float fgRGB[3], float bgRGB[3])
{
   using namespace Vmenunames;

   int      cc, ww, hh;
   int      K64 = 65536;
   ch       *menufont1, *menufont2;
   PangoLayout   *playout;

   cc = sizeof(Vmenu);
   Vmenu *vbm = (Vmenu *) zmalloc(cc,"Vmenu");
   vbm->fgRGB[0] = fgRGB[0];                                                           //  background color, RGB 0-1.0
   vbm->fgRGB[1] = fgRGB[1];
   vbm->fgRGB[2] = fgRGB[2];
   vbm->bgRGB[0] = bgRGB[0];                                                           //  background color, RGB 0-1.0
   vbm->bgRGB[1] = bgRGB[1];
   vbm->bgRGB[2] = bgRGB[2];
   vbm->vbox = vbox;
   vbm->topwin = gtk_widget_get_toplevel(vbox);
   vbm->layout = gtk_layout_new(0,0);
   vbm->mcount = 0;
   gtk_box_pack_start(GTK_BOX(vbox),vbm->layout,1,1,0);
   vbm->xmax = vbm->ymax = 10;                                                         //  initial layout size

   pattrlist = pango_attr_list_new();
   pbackground = pango_attr_background_new(K64*bgRGB[0],K64*bgRGB[1],K64*bgRGB[2]);
   pango_attr_list_change(pattrlist,pbackground);

   menufont1 = zstrdup(zfuncs::appfont,"Vmenu");                                       //  set menu fonts, normal and bold
   menufont2 = zstrdup(zfuncs::appboldfont,"Vmenu");

   pfont1 = pango_font_description_from_string(menufont1);
   pfont2 = pango_font_description_from_string(menufont2);

   playout = gtk_widget_create_pango_layout(vbm->layout,0);
   pango_layout_set_font_description(playout,pfont1);
   pango_layout_set_text(playout,"Ayg",-1);
   pango_layout_get_pixel_size(playout,&ww,&hh);
   fontheight = hh;

   gtk_widget_add_events(vbm->layout,GDK_BUTTON_PRESS_MASK);
   gtk_widget_add_events(vbm->layout,GDK_BUTTON_RELEASE_MASK);
   gtk_widget_add_events(vbm->layout,GDK_POINTER_MOTION_MASK);
   gtk_widget_add_events(vbm->layout,GDK_LEAVE_NOTIFY_MASK);
   G_SIGNAL(vbm->layout,"button-press-event",mouse_event,vbm);
   G_SIGNAL(vbm->layout,"button-release-event",mouse_event,vbm);
   G_SIGNAL(vbm->layout,"motion-notify-event",mouse_event,vbm);
   G_SIGNAL(vbm->layout,"leave-notify-event",mouse_event,vbm);
   G_SIGNAL(vbm->layout,"draw",wpaint,vbm);

   return vbm;
}


//  add Vmenu entry with name, icon, description, menu function (left click)

void Vmenu_add(Vmenu *vbm, ch *name, ch *icon, int iconww, int iconhh, ch *desc, cbFunc func, ch *arg)
{
   using namespace Vmenunames;

   int         me, cc, xpos, ww, hh;
   ch          iconpath[200], *mdesc, *name__;
   ch          *blanks = "                    ";                                       //  20 blanks
   PIXBUF      *pixbuf;
   GError      *gerror = 0;

   PangoLayout             *playout;
   PangoFontDescription    *pfont;

   if (! name && ! icon) return;

   me = vbm->mcount++;                                                                 //  track no. menu entries

   if (name) vbm->menu[me].name = zstrdup(name,"Vmenu");                               //  create new menu entry from caller data

   if (icon) {
      vbm->menu[me].icon = zstrdup(icon,"Vmenu");
      vbm->menu[me].iconww = iconww;
      vbm->menu[me].iconhh = iconhh;
   }

   if (desc) {                                                                         //  pad description with blanks for looks
      cc = strlen(desc);
      mdesc = (ch *) zmalloc(cc+3,"Vmenu");
      mdesc[0] = ' ';
      strcpy(mdesc+1,desc);
      strcpy(mdesc+cc+1," ");
      vbm->menu[me].desc = mdesc;
   }

   vbm->menu[me].LMfunc = func;                                                        //  left-mouse menu function
   vbm->menu[me].arg = name;                                                           //  argument is menu name or arg if avail.
   if (arg) vbm->menu[me].arg = arg;

   vbm->menu[me].pixbuf = 0;

   if (icon) {                                                                         //  if icon is named, get pixbuf
      *iconpath = 0;
      strncatv(iconpath,199,zfuncs::zimagedir,"/",icon,null);
      pixbuf = gdk_pixbuf_new_from_file_at_scale(iconpath,iconww,iconhh,1,&gerror);
      if (pixbuf) vbm->menu[me].pixbuf = pixbuf;
      else printf("*** Vmenu no icon: %s \n",iconpath);
   }

   if (me == 0) vbm->ymax = margin;                                                    //  first menu, top position

   vbm->menu[me].iconx = 0;
   vbm->menu[me].icony = 0;
   vbm->menu[me].namex = 0;
   vbm->menu[me].namey = 0;

   if (icon) {
      vbm->menu[me].iconx = margin;                                                    //     ______
      vbm->menu[me].icony = vbm->ymax;                                                 //    |      |
      if (name) {                                                                      //    | icon | menu name
         vbm->menu[me].namex = margin + iconww + margin;                               //    |______|
         vbm->menu[me].namey = vbm->ymax + (iconhh - fontheight) / 2;                  //
      }
      vbm->menu[me].ylo = vbm->ymax;
      vbm->ymax += iconhh + iconhh / 8;                                                //  position for next menu entry
      vbm->menu[me].yhi = vbm->ymax;
      if (margin + iconww > vbm->xmax) vbm->xmax = margin + iconww;                    //  keep track of max. layout width
   }

   else if (name) {
      vbm->menu[me].namex = margin;                                                    //  menu name
      vbm->menu[me].namey = vbm->ymax;
      vbm->menu[me].ylo = vbm->ymax;
      vbm->ymax += 1.5 * fontheight;                                                   //  more space between text lines
      vbm->menu[me].yhi = vbm->ymax;
   }

   vbm->menu[me].playout1 = gtk_widget_create_pango_layout(vbm->layout,0);
   vbm->menu[me].playout2 = gtk_widget_create_pango_layout(vbm->layout,0);

   if (name) {
      xpos = vbm->menu[me].namex;

      cc = strlen(name);                                                               //  menu name with trailing blanks
      name__ = zstrdup(name,"Vmenu",22);                                               //  (long enough to overwrite bold name)
      strncpy0(name__+cc,blanks,20);

      playout = vbm->menu[me].playout1;                                                //  normal font
      pfont = pfont1;
      pango_layout_set_attributes(playout,pattrlist);
      pango_layout_set_font_description(playout,pfont);
      pango_layout_set_text(playout,name__,-1);                                        //  compute layout
      pango_layout_get_pixel_size(playout,&ww,&hh);                                    //  pixel width and height of layout

      playout = vbm->menu[me].playout2;                                                //  bold font
      pfont = pfont2;
      pango_layout_set_attributes(playout,pattrlist);
      pango_layout_set_font_description(playout,pfont);
      pango_layout_set_text(playout,name,-1);                                          //  compute layout
      pango_layout_get_pixel_size(playout,&ww,&hh);                                    //  pixel width and height of layout
      if (xpos + ww > vbm->xmax) vbm->xmax = xpos + ww;                                //  keep track of max. layout width
   }

   gtk_widget_set_size_request(vbm->layout,vbm->xmax+margin,0);                        //  add right margin to layout width

   return;
}


//  add opt. setup function to existing menu entry

void Vmenu_add_setup(Vmenu *vbm, int me, cbFunc func, ch *arg)
{
   if (me > vbm->mcount-1) zappcrash("Vmenu_add_RMfunc() bad me: %d",me);
   vbm->menu[me].setupfunc = func;
   vbm->menu[me].setuparg = arg;
   return;
}


//  add alternate function for right-mouse click

void Vmenu_add_RMfunc(Vmenu *vbm, int me, cbFunc func, ch *arg)
{
   if (me > vbm->mcount-1) zappcrash("Vmenu_add_RMfunc() bad me: %d",me);
   vbm->menu[me].RMfunc = func;
   vbm->menu[me].RMarg = arg;
   return;
}


//  block or unblock menu

void Vmenu_block(int flag)
{
   using namespace Vmenunames;

   Fblock = flag;
   return;
}


//  paint window when created, exposed, resized

void Vmenunames::wpaint(GtkWidget *widget, cairo_t *cr, Vmenu *vbm)
{
   using namespace Vmenunames;

   cairo_set_source_rgb(cr,vbm->bgRGB[0],vbm->bgRGB[1],vbm->bgRGB[2]);                 //  background
   cairo_paint(cr);

   for (int me = 0; me < vbm->mcount; me++)                                            //  paint all menu entries
      paint_menu(cr,vbm,me,0);

   return;
}


//  draw menu icon and text into layout

void Vmenunames::paint_menu(cairo_t *cr, Vmenu *vbm, int me, int hilite)
{
   using namespace Vmenunames;

   PIXBUF         *pixbuf;
   PangoLayout    *playout;
   int            xpos, ypos;
   int            iconww, iconhh;
   ch             *name;

   pixbuf = vbm->menu[me].pixbuf;                                                      //  icon
   if (pixbuf) {                                                                       //  draw menu icon at menu position
      xpos = vbm->menu[me].iconx;
      ypos = vbm->menu[me].icony;
      iconww = vbm->menu[me].iconww;
      iconhh = vbm->menu[me].iconhh;

      if (! hilite) {                                                                  //  erase box around icon
         cairo_set_source_rgb(cr,vbm->bgRGB[0],vbm->bgRGB[1],vbm->bgRGB[2]);           //  background
         cairo_rectangle(cr,xpos-1,ypos-1,iconww+2,iconhh+2);
         cairo_fill(cr);
      }

      gdk_cairo_set_source_pixbuf(cr,pixbuf,xpos,ypos);                                //  draw icon
      cairo_paint(cr);

      if (hilite) {
         cairo_set_source_rgb(cr,vbm->fgRGB[0],vbm->fgRGB[1],vbm->fgRGB[2]);           //  draw box around icon
         cairo_set_line_width(cr,1);
         cairo_set_line_join(cr,CAIRO_LINE_JOIN_ROUND);
         cairo_rectangle(cr,xpos,ypos,iconww,iconhh);
         cairo_stroke(cr);
      }
   }

   name = vbm->menu[me].name;                                                          //  menu text
   if (name) {                                                                         //  draw menu text at menu position
      xpos = vbm->menu[me].namex;
      ypos = vbm->menu[me].namey;
      cairo_move_to(cr,xpos,ypos);                                                     //  draw layout with text
      cairo_set_source_rgb(cr,vbm->fgRGB[0],vbm->fgRGB[1],vbm->fgRGB[2]);
      if (hilite) playout = vbm->menu[me].playout2;
      else playout = vbm->menu[me].playout1;
      pango_cairo_show_layout(cr,playout);
   }

   return;
}


//  mouse event function - capture buttons and drag movements

void Vmenunames::mouse_event(GtkWidget *widget, GdkEventButton *event, Vmenu *vbm)
{
   using namespace Vmenunames;

   GdkWindow   *gdkwin;
   ch          *desc;
   int         me, mpx, mpy, button, ww, ylo, yhi;
   static int  me0 = -1, Fmyclick = 0, winww = 0;

   static draw_context_t   context;
   static GtkWidget        *pwidget = 0;
   static cairo_t          *cr = 0;

   if (widget != pwidget) {                                                            //  widget changed
      if (pwidget) draw_context_destroy(context);
      gdkwin = gtk_layout_get_bin_window(GTK_LAYOUT(widget));
      cr = draw_context_create(gdkwin,context);
      gdkwin = gtk_widget_get_window(widget);                                          //  get width of menu widget
      winww = gdk_window_get_width(gdkwin);
      pwidget = widget;
   }

   mpx = int(event->x);                                                                //  mouse position
   mpy = int(event->y);
   button = event->button;
   if (button == 1 && (event->state & GDK_MOD1_MASK)) button = 3;                      //  left butt + ALT key >> right butt

   if (event->type == GDK_MOTION_NOTIFY)                                               //  mouse inside layout
   {
      for (me = 0; me < vbm->mcount; me++) {                                           //  find menu where mouse is
         ylo = vbm->menu[me].ylo;
         yhi = vbm->menu[me].yhi;
         if (mpy < ylo) continue;
         if (mpy < yhi) break;
      }

      if (me != me0 && me0 >= 0) {                                                     //  unhighlight prior
         paint_menu(cr,vbm,me0,0);
         me0 = -1;
      }

      if (me == me0) return;                                                           //  same as before
      if (me == vbm->mcount) return;                                                   //  no new menu match

      paint_menu(cr,vbm,me,1);                                                         //  highlight menu entry at mouse
      desc = vbm->menu[me].desc;                                                       //  show tool tip
      if (desc) poptext_widget(widget,desc,winww,mpy,0,5);                             //  px = menu width, py = mouse           25.0
      me0 = me;                                                                        //  remember last match
      return;
   }

   if (me0 >= 0)                                                                       //  mouse left layout
   {
      poptext_killnow();                                                               //  25.0
      paint_menu(cr,vbm,me0,0);                                                        //  unhighlight prior
      me0 = -1;
   }

   if (event->type == GDK_BUTTON_PRESS)                                                //  menu entry clicked
      Fmyclick = 1;                                                                    //  button click is mine

   if (event->type == GDK_BUTTON_RELEASE)                                              //  menu entry clicked
   {
      if (Fblock) return;                                                              //  menu is blocked

      if (! Fmyclick) return;                                                          //  ignore unmatched button release
      Fmyclick = 0;                                                                    //    (from vanished popup window)

      for (me = 0; me < vbm->mcount; me++) {                                           //  look for clicked menu entry
         ylo = vbm->menu[me].ylo;
         yhi = vbm->menu[me].yhi;
         if (mpy < ylo) continue;
         if (mpy < yhi) break;
      }

      if (me == vbm->mcount) return;                                                   //  no menu match

      zfuncs::vmenuclickbutton = button;                                               //  1/2/3 = left/mid/right button

      ww = vbm->menu[me].iconww;                                                       //  get horiz. click posn. on menu icon
      if (ww) mpx = 100 * (mpx - margin) / ww;                                         //    scale 0-100
      else mpx = 0;
      if (mpx < 0) mpx = 0;
      if (mpx > 100) mpx = 100;
      zfuncs::vmenuclickposn = mpx;

      paint_menu(cr,vbm,me,0);                                                         //  unhighlight menu

      if (vbm->menu[me].setupfunc) {                                                   //  call opt. setup function
         vbm->menu[me].setupfunc(widget,vbm->menu[me].setuparg);
         if (vmenustop) {
            vmenustop = 0;
            return;
         }
      }

      if (button == 3 && vbm->menu[me].RMfunc)                                         //  if right mouse button,
         vbm->menu[me].RMfunc(widget,vbm->menu[me].RMarg);                             //    call right-mouse function

      else if (vbm->menu[me].LMfunc)                                                   //  call left-mouse menu function
         vbm->menu[me].LMfunc(widget,vbm->menu[me].arg);
   }

   return;
}


/**************************************************************************************

   spline curve setup and edit functions
   support multiple frames with multiple curves

   sd = splcurve_init(frame,callback_func)            add draw area widget in dialog frame widget
   sd->Nspc = n                                       Initialize no. of curves in frame
   sd->fact[spc] = 1                                  Initialize active flag for curve spc
   sd->vert[spc] = hv                                 Initialize vert/horz flag for curve spc
   sd->nap[spc], sd->apx[spc][xx], sd->apy[spc][yy]   Initialize anchor points for curve spc
   splcurve_generate(sd,spc)                          Generate data for curve spc

   Curves will now be shown inside the frame when window is realized.
   The callback_func(spc) will be called when curve spc is edited (mouse drag).
   Change curve in program: set anchor points, call splcurve_generate(sd,spc).

   yval = splcurve_yval(sd,spc,xval)            Get y-value (0-1) for curve spc and given x-value (0-1)

   kk = 1000 * xval;                            If faster access to curve is needed (no interpolation)
   if (kk > 999) kk = 999;
   yval = sd->yval[spc][kk];

***/

//  initialize for spline curve editing
//  initial anchor points are pre-loaded into spldat before window is realized

spldat * splcurve_init(GtkWidget *frame, void func(int spc))
{
   int      cc = sizeof(spldat);                                                       //  allocate spc curve data area
   spldat * sd = (spldat *) zmalloc(cc,"splcurve");
   memset(sd,0,cc);

   sd->drawarea = gtk_drawing_area_new();                                              //  drawing area for curves
   gtk_container_add(GTK_CONTAINER(frame),sd->drawarea);
   sd->spcfunc = func;                                                                 //  user callback function

   gtk_widget_add_events(sd->drawarea,GDK_BUTTON_PRESS_MASK);                          //  connect mouse events to drawing area
   gtk_widget_add_events(sd->drawarea,GDK_BUTTON_RELEASE_MASK);
   gtk_widget_add_events(sd->drawarea,GDK_BUTTON1_MOTION_MASK);
   G_SIGNAL(sd->drawarea,"motion-notify-event",splcurve_adjust,sd);
   G_SIGNAL(sd->drawarea,"button-press-event",splcurve_adjust,sd);
   G_SIGNAL(sd->drawarea,"realize",splcurve_resize,sd);
   G_SIGNAL(sd->drawarea,"draw",splcurve_draw,sd);

   return sd;
}


//  modify anchor points in curve using mouse

int splcurve_adjust(void *, GdkEventButton *event, spldat *sd)
{
   int            ww, hh, kk;
   int            mx, my, button, evtype;
   static int     spc, ap, mbusy = 0, Fdrag = 0;                                       //  drag continuation logic
   int            minspc, minap, apset = 0;
   float          mxval, myval, cxval, cyval;
   float          dist2, mindist2 = 0;
   float          dist, dx, dy;
   float          minx = 0.01 * splcurve_minx;                                         //  % to absolute distance

   mx = event->x;                                                                      //  mouse position in drawing area
   my = event->y;
   evtype = event->type;
   button = event->button;

   if (evtype == GDK_MOTION_NOTIFY) {
      if (mbusy) return 0;                                                             //  discard excess motion events
      mbusy++;
      zmainloop();
      mbusy = 0;
   }

   if (evtype == GDK_BUTTON_RELEASE) {
      Fdrag = 0;
      return 0;
   }

   ww = gtk_widget_get_allocated_width(sd->drawarea);                                  //  drawing area size
   hh = gtk_widget_get_allocated_height(sd->drawarea);

   if (mx < 0) mx = 0;                                                                 //  limit edge excursions
   if (mx > ww) mx = ww;
   if (my < 0) my = 0;
   if (my > hh) my = hh;

   if (evtype == GDK_BUTTON_PRESS) Fdrag = 0;                                          //  left or right click

   if (Fdrag)                                                                          //  continuation of drag
   {
      if (sd->vert[spc]) {
         mxval = 1.0 * my / hh;                                                        //  mouse position in curve space
         myval = 1.0 * mx / ww;
      }
      else {
         mxval = 1.0 * mx / ww;
         myval = 1.0 * (hh - my) / hh;
      }

      if (ap < sd->nap[spc] - 1) {                                                     //  not the last anchor point
         dx = sd->apx[spc][ap+1] - mxval;                                              //  get distance to next anchor point
         if (dx < 0.01) return 0;                                                      //  x-value not increasing, forbid
         dy = sd->apy[spc][ap+1] - myval;
         dist = sqrtf(dx * dx + dy * dy);
         if (dist < minx) return 0;                                                    //  too close, forbid
      }
      if (ap > 0) {                                                                    //  not the first anchor point
         dx = mxval - sd->apx[spc][ap-1];                                              //  get distance to prior anchor point
         if (dx < 0.01) return 0;                                                      //  x-value not increasing, forbid
         dy = myval - sd->apy[spc][ap-1];
         dist = sqrtf(dx * dx + dy * dy);
         if (dist < minx) return 0;                                                    //  too close, forbid
      }

      apset = 1;                                                                       //  mxval/myval = new node position
   }

   else                                                                                //  mouse click or new drag begin
   {
      minspc = minap = -1;                                                             //  find closest curve/anchor point
      mindist2 = 999999;

      for (spc = 0; spc < sd->Nspc; spc++)                                             //  loop curves
      {
         if (! sd->fact[spc]) continue;                                                //  not active

         if (sd->vert[spc]) {
            mxval = 1.0 * my / hh;                                                     //  mouse position in curve space
            myval = 1.0 * mx / ww;
         }
         else {
            mxval = 1.0 * mx / ww;
            myval = 1.0 * (hh - my) / hh;
         }

         for (ap = 0; ap < sd->nap[spc]; ap++)                                         //  loop anchor points
         {
            cxval = sd->apx[spc][ap];
            cyval = sd->apy[spc][ap];
            dist2 = (mxval-cxval)*(mxval-cxval)
                  + (myval-cyval)*(myval-cyval);
            if (dist2 < mindist2) {
               mindist2 = dist2;                                                       //  remember closest anchor point
               minspc = spc;
               minap = ap;
            }
         }
      }

      if (minspc < 0) return 0;                                                        //  impossible
      spc = minspc;                                                                    //  nearest curve
      ap = minap;                                                                      //  nearest anchor point
   }

   if (evtype == GDK_BUTTON_PRESS && button == 3)                                      //  right click, remove anchor point
   {
      if (sqrtf(mindist2) > minx) return 0;                                            //  not close enough
      if (sd->nap[spc] < 3) return 0;                                                  //  < 2 anchor points would remain
      sd->nap[spc]--;                                                                  //  decr. before loop
      for (kk = ap; kk < sd->nap[spc]; kk++) {
         sd->apx[spc][kk] = sd->apx[spc][kk+1];
         sd->apy[spc][kk] = sd->apy[spc][kk+1];
      }
      splcurve_generate(sd,spc);                                                       //  regenerate data for modified curve
      gtk_widget_queue_draw(sd->drawarea);
      sd->spcfunc(spc);                                                                //  call user function
      return 0;
   }

   if (! Fdrag)                                                                        //  new drag or left click
   {
      if (sd->vert[spc]) {
         mxval = 1.0 * my / hh;                                                        //  mouse position in curve space
         myval = 1.0 * mx / ww;
      }
      else {
         mxval = 1.0 * mx / ww;
         myval = 1.0 * (hh - my) / hh;
      }

      if (sqrtf(mindist2) < minx)                                                      //  anchor point close enough,
      {                                                                                //    move this one to mouse position
         if (ap < sd->nap[spc]-1) {                                                    //  not the last anchor point
            dx = sd->apx[spc][ap+1] - mxval;                                           //  get distance to next anchor point
            if (dx < 0.01) return 0;                                                   //  x-value not increasing, forbid
            dy = sd->apy[spc][ap+1] - myval;
            dist = sqrtf(dx * dx + dy * dy);
            if (dist < minx) return 0;                                                 //  too close, forbid
         }
         if (ap > 0) {                                                                 //  not the first anchor point
            dx = mxval - sd->apx[spc][ap-1];                                           //  get distance to prior anchor point
            if (dx < 0.01) return 0;                                                   //  x-value not increasing, forbid
            dy = myval - sd->apy[spc][ap-1];
            dist = sqrtf(dx * dx + dy * dy);
            if (dist < minx) return 0;                                                 //  too close, forbid
         }

         apset = 1;                                                                    //  mxval/myval = new node position
      }

      else                                                                             //  none close, add new anchor point
      {
         minspc = -1;                                                                  //  find closest curve to mouse
         mindist2 = 999999;

         for (spc = 0; spc < sd->Nspc; spc++)                                          //  loop curves
         {
            if (! sd->fact[spc]) continue;                                             //  not active

            if (sd->vert[spc]) {
               mxval = 1.0 * my / hh;                                                  //  mouse position in curve space
               myval = 1.0 * mx / ww;
            }
            else {
               mxval = 1.0 * mx / ww;
               myval = 1.0 * (hh - my) / hh;
            }

            cyval = splcurve_yval(sd,spc,mxval);
            dist2 = fabsf(myval - cyval);
            if (dist2 < mindist2) {
               mindist2 = dist2;                                                       //  remember closest curve
               minspc = spc;
            }
         }

         if (minspc < 0) return 0;                                                     //  impossible
         if (mindist2 > minx) return 0;                                                //  not close enough to any curve
         spc = minspc;

         if (sd->nap[spc] > 49) {
            zmessageACK(mainwin,TX("Exceed 50 anchor points"));
            return 0;
         }

         if (sd->vert[spc]) {
            mxval = 1.0 * my / hh;                                                     //  mouse position in curve space
            myval = 1.0 * mx / ww;
         }
         else {
            mxval = 1.0 * mx / ww;
            myval = 1.0 * (hh - my) / hh;
         }

         for (ap = 0; ap < sd->nap[spc]; ap++)                                         //  find anchor point with next higher x
            if (mxval <= sd->apx[spc][ap]) break;                                      //    (ap may come out 0 or nap)

         if (ap < sd->nap[spc] && sd->apx[spc][ap] - mxval < minx)                     //  disallow < minx from next
               return 0;                                                               //    or prior anchor point
         if (ap > 0 && mxval - sd->apx[spc][ap-1] < minx) return 0;

         for (kk = sd->nap[spc]; kk > ap; kk--) {                                      //  make hole for new point
            sd->apx[spc][kk] = sd->apx[spc][kk-1];
            sd->apy[spc][kk] = sd->apy[spc][kk-1];
         }

         sd->nap[spc]++;                                                               //  up point count
         apset = 1;                                                                    //  mxval/myval = new node position
      }
   }

   if (evtype == GDK_MOTION_NOTIFY) Fdrag = 1;                                         //  remember drag is underway

   if (apset)
   {
      sd->apx[spc][ap] = mxval;                                                        //  new or moved anchor point
      sd->apy[spc][ap] = myval;                                                        //    at mouse position

      splcurve_generate(sd,spc);                                                       //  regenerate data for modified curve
      if (sd->drawarea) gtk_widget_queue_draw(sd->drawarea);                           //  redraw graph
      if (sd->spcfunc) sd->spcfunc(spc);                                               //  call user function
   }

   return 0;
}


//  add a new anchor point to a curve
//  spc:     curve number
//  px, py:  node coordinates in the range 0-1

int splcurve_addnode(spldat *sd, int spc, float px, float py)
{
   int      ap, kk;
   float    minx = 0.01 * splcurve_minx;                                               //  % to absolute distance

   for (ap = 0; ap < sd->nap[spc]; ap++)                                               //  find anchor point with next higher x
      if (px <= sd->apx[spc][ap]) break;                                               //    (ap may come out 0 or nap)

   if (ap < sd->nap[spc] && sd->apx[spc][ap] - px < minx)                              //  disallow < minx from next
         return 0;                                                                     //    or prior anchor point
   if (ap > 0 && px - sd->apx[spc][ap-1] < minx) return 0;

   for (kk = sd->nap[spc]; kk > ap; kk--) {                                            //  make hole for new point
      sd->apx[spc][kk] = sd->apx[spc][kk-1];
      sd->apy[spc][kk] = sd->apy[spc][kk-1];
   }

   sd->apx[spc][ap] = px;                                                              //  add node coordinates
   sd->apy[spc][ap] = py;

   sd->nap[spc]++;                                                                     //  up point count
   return 1;
}


//  if height/width too small, make bigger

int splcurve_resize(GtkWidget *drawarea)
{
   int   ww, hh;

   gtk_widget_get_size_request(drawarea,&ww,&hh);
   if (hh < 50) gtk_widget_set_size_request(drawarea,ww,50);
   return 1;
}


//  for expose event or when a curve is changed
//  draw all curves based on current anchor points

int splcurve_draw(GtkWidget *drawarea, cairo_t *cr, spldat *sd)
{
   int         ww, hh, spc, ap;
   float       xval, yval, px, py, qx, qy;

   if (! sd || ! sd->drawarea) return 0;                                               //  25.0

   ww = gtk_widget_get_allocated_width(sd->drawarea);                                  //  drawing area size
   hh = gtk_widget_get_allocated_height(sd->drawarea);
   if (ww < 50 || hh < 50) return 0;

   cairo_set_line_width(cr,1);
   cairo_set_source_rgb(cr,0.7,0.7,0.7);

   for (int ii = 0; ii < sd->Nscale; ii++)                                             //  draw y-scale lines if any
   {
      px = ww * sd->xscale[0][ii];
      py = hh - hh * sd->yscale[0][ii];
      qx = ww * sd->xscale[1][ii];
      qy = hh - hh * sd->yscale[1][ii];
      cairo_move_to(cr,px,py);
      cairo_line_to(cr,qx,qy);
   }
   cairo_stroke(cr);

   cairo_set_source_rgb(cr,0,0,0);

   for (spc = 0; spc < sd->Nspc; spc++)                                                //  loop all curves
   {
      if (! sd->fact[spc]) continue;                                                   //  not active

      if (sd->vert[spc])                                                               //  vert. curve
      {
         for (py = 0; py < hh; py++)                                                   //  generate all points for curve
         {
            xval = 1.0 * py / hh;
            yval = splcurve_yval(sd,spc,xval);
            px = ww * yval;
            if (py == 0) cairo_move_to(cr,px,py);
            cairo_line_to(cr,px,py);
         }
         cairo_stroke(cr);

         for (ap = 0; ap < sd->nap[spc]; ap++)                                         //  draw boxes at anchor points
         {
            xval = sd->apx[spc][ap];
            yval = sd->apy[spc][ap];
            px = ww * yval;
            py = hh * xval;
            cairo_rectangle(cr,px-2,py-2,4,4);
         }
         cairo_fill(cr);
      }
      else                                                                             //  horz. curve
      {
         for (px = 0; px < ww; px++)                                                   //  generate all points for curve
         {
            xval = 1.0 * px / ww;
            yval = splcurve_yval(sd,spc,xval);
            py = hh - hh * yval;
            if (px == 0) cairo_move_to(cr,px,py);
            cairo_line_to(cr,px,py);
         }
         cairo_stroke(cr);

         for (ap = 0; ap < sd->nap[spc]; ap++)                                         //  draw boxes at anchor points
         {
            xval = sd->apx[spc][ap];
            yval = sd->apy[spc][ap];
            px = ww * xval;
            py = hh - hh * yval;
            cairo_rectangle(cr,px-2,py-2,4,4);
         }
         cairo_fill(cr);
      }
   }

   return 0;
}


//  generate all curve data points when anchor points are modified

int splcurve_generate(spldat *sd, int spc)
{
   int      kk, kklo, kkhi;
   float    xval, yvalx;

   spline1(sd->nap[spc],sd->apx[spc],sd->apy[spc]);                                    //  compute curve fitting anchor points

   kklo = 1000 * sd->apx[spc][0] - 30;                                                 //  xval range = anchor point range
   if (kklo < 0) kklo = 0;                                                             //    + 0.03 extra below/above
   kkhi = 1000 * sd->apx[spc][sd->nap[spc]-1] + 30;
   if (kkhi > 1000) kkhi = 1000;

   for (kk = 0; kk < 1000; kk++)                                                       //  generate all points for curve
   {
      xval = 0.001 * kk;                                                               //  remove anchor point limits
      yvalx = spline2(xval);
      if (yvalx < 0) yvalx = 0;                                                        //  yval < 0 not allowed, > 1 OK
      sd->yval[spc][kk] = yvalx;
   }

   sd->mod[spc] = 1;                                                                   //  mark curve modified

   return 0;
}


//  Retrieve curve data using interpolation of saved table of values

float splcurve_yval(spldat *sd, int spc, float xval)
{
   int      ii;
   float    x1, x2, y1, y2, y3;

   if (xval <= 0) return sd->yval[spc][0];
   if (xval >= 0.999) return sd->yval[spc][999];

   x2 = 1000.0 * xval;
   ii = x2;
   x1 = ii;
   y1 = sd->yval[spc][ii];
   y2 = sd->yval[spc][ii+1];
   y3 = y1 + (y2 - y1) * (x2 - x1);
   return y3;
}


//  load curve data from a file
//  returns 0 if success, sd is initialized from file data
//  returns 1 if fail (invalid file data), sd not modified

int splcurve_load(spldat *sd, FILE *fid)
{
   ch          *pp, buff[300];
   int         nn, ii, jj, err, myfid = 0;
   int         Nspc, fact[10], vert[10], nap[10];
   float       apx[10][50], apy[10][50];

   pp = fgets_trim(buff,300,fid,1);
   if (! pp) goto fail;
   nn = sscanf(pp,"%d",&Nspc);                                                         //  no. of curves
   if (nn != 1) goto fail;
   if (Nspc < 1 || Nspc > 10) goto fail;
   if (Nspc != sd->Nspc) goto fail;

   for (ii = 0; ii < Nspc; ii++)                                                       //  loop each curve
   {
      pp = fgets_trim(buff,300,fid,1);
      if (! pp) goto fail;
      nn = sscanf(pp,"%d %d %d",&fact[ii],&vert[ii],&nap[ii]);                         //  active flag, vert flag, anchors
      if (nn != 3) goto fail;
      if (fact[ii] < 0 || fact[ii] > 1) goto fail;
      if (vert[ii] < 0 || vert[ii] > 1) goto fail;
      if (nap[ii] < 2 || nap[ii] > 50) goto fail;

      pp = fgets_trim(buff,300,fid,1);                                                 //  anchor points: nnn/nnn nnn/nnn ...

      for (jj = 0; jj < nap[ii]; jj++)                                                 //  anchor point values
      {
         pp = (ch *) substring(buff,"/ ",2*jj+1);
         if (! pp) goto fail;
         err = convSF(pp,apx[ii][jj],0,1);
         if (err) goto fail;
         pp = (ch *) substring(buff,"/ ",2*jj+2);
         if (! pp) goto fail;
         err = convSF(pp,apy[ii][jj],0,1);
         if (err) goto fail;
      }
   }

   if (myfid) fclose(fid);

   sd->Nspc = Nspc;                                                                    //  copy curve data to caller's arg

   for (ii = 0; ii < Nspc; ii++)
   {
      sd->fact[ii] = fact[ii];
      sd->vert[ii] = vert[ii];
      sd->nap[ii] = nap[ii];

      for (jj = 0; jj < nap[ii]; jj++)
      {
         sd->apx[ii][jj] = apx[ii][jj];
         sd->apy[ii][jj] = apy[ii][jj];
      }
   }

   for (ii = 0; ii < Nspc; ii++)                                                       //  generate curve data from anchor points
      splcurve_generate(sd,ii);

   if (sd->drawarea)                                                                   //  redraw all curves
      gtk_widget_queue_draw(sd->drawarea);

   return 0;                                                                           //  success

fail:
   if (fid && myfid) fclose(fid);
   zmessageACK(mainwin,TX("curve file is invalid"));
   return 1;
}


//  save curve data to a file

int splcurve_save(spldat *sd, FILE *fid)
{
   int         ii, jj, myfid = 0;

   fprintf(fid,"%d \n",sd->Nspc);                                                      //  no. of curves

   for (ii = 0; ii < sd->Nspc; ii++)                                                   //  loop each curve
   {
      fprintf(fid,"%d %d %d \n",sd->fact[ii],sd->vert[ii],sd->nap[ii]);                //  active flag, vert flag, anchors
      for (jj = 0; jj < sd->nap[ii]; jj++)                                             //  anchor point values
         fprintf(fid,"%.4f/%.4f  ",sd->apx[ii][jj],sd->apy[ii][jj]);
      fprintf(fid,"\n");
   }

   if (myfid) fclose(fid);
   return 0;
}


/**************************************************************************************
   simplified GTK dialog functions

   create a dialog with response buttons (OK, cancel ...)
   add widgets to dialog (button, text entry, combo box ...)
   put data into widgets (text or numeric data)
   run the dialog, get user inputs (button press, text entry, checkbox selection ...)
   run caller event function when widgets change from user inputs
   run caller event function when dialog is completed or canceled
   get dialog completion status (OK, cancel, destroyed ...)
   get data from dialog widgets (the user inputs)
   destroy the dialog and free memory

***************************************************************************************/

//  private functions for widget events and dialog completion

int  zdialog_widget_event(GtkWidget *, zdialog *zd);
int  zdialog_delete_event(GtkWidget *, GdkEvent *, zdialog *zd);
int  zdialog_zspin_event(GtkWidget *, GdkEvent *, zdialog *zd);                        //  "zspin" widget

//  create a new zdialog dialog
//  The title and parent arguments may be null.
//  optional arguments: up to zdmaxbutts button labels followed by null
//  returned dialog status: +N = button N (1 to zdmaxbutts)
//                          <0 = [x] button or other GTK destroy action
//  completion buttons are also events like other widgets
//  all dialogs run parallel, use zdialog_wait() if needed
//  The status returned by zdialog_wait() corresponds to the button
//  (1-10) used to end the dialog. Status < 0 indicates the [x] button
//  was used or the dialog was killed by the program itself.

zdialog * zdialog_new(ch *title, GtkWidget *parent, ...)                               //  completion buttons ending with null
{
   zdialog        *zd;
   GtkWidget      *dialog, *hbox, *vbox, *butt, *hsep;
   ch             *bulab[zdmaxbutts];
   int            cc, ii, nbu;
   va_list        arglist;
   static int     uniqueID = 1;

   if (! main_thread()) zappcrash("illegal call from thread");

   va_start(arglist,parent);
   for (nbu = 0; nbu < zdmaxbutts; nbu++) {                                            //  get completion buttons
      bulab[nbu] = va_arg(arglist, ch *);
      if (! bulab[nbu]) break;
   }
   va_end(arglist);

   if (! title) title = "";

   dialog = gtk_window_new(GTK_WINDOW_TOPLEVEL);
   gtk_window_set_title(GTK_WINDOW(dialog),title);
   vbox = gtk_box_new(VERTICAL,0);                                                     //  vertical packing box
   gtk_container_add(GTK_CONTAINER(dialog),vbox);                                      //  add to main window
   gtk_window_set_default_size(GTK_WINDOW(dialog),10,10);                              //  stop auto width of 150 pixels

   if (parent)
      gtk_window_set_transient_for(GTK_WINDOW(dialog),GTK_WINDOW(parent));

   gtk_box_set_spacing(GTK_BOX(vbox),2);
   gtk_container_set_border_width(GTK_CONTAINER(vbox),5);

   cc = sizeof(zdialog);                                                               //  allocate zdialog
   zd = (zdialog *) zmalloc(cc,"zdialog");

   if (zdialog_count == zdialog_max) {                                                 //  add to active list
      for (ii = 0; ii < zdialog_count; ii++)
         printf("*** dialog: %s \n",zdialog_list[ii]->widget[0].data);
      zappcrash("max. zdialogs exceeded");
   }

   zdialog_list[zdialog_count] = zd;
   zdialog_count++;

   if (nbu) {                                                                          //  completion buttons
      hbox = gtk_box_new(HORIZONTAL,2);                                                //  add hbox for buttons at bottom
      gtk_box_pack_end(GTK_BOX(vbox),hbox,0,0,2);
      hsep = gtk_separator_new(HORIZONTAL);                                            //  add separator line
      gtk_box_pack_end(GTK_BOX(vbox),hsep,0,0,5);

      for (ii = nbu-1; ii >= 0; ii--) {                                                //  add buttons to hbox
         butt = gtk_button_new_with_label(bulab[ii]);                                  //  reverse order nbu-1...0
         gtk_box_pack_end(GTK_BOX(hbox),butt,0,0,2);
         G_SIGNAL(butt,"clicked",zdialog_widget_event,zd);                             //  connect to event function
         zd->compwidget[ii] = butt;                                                    //  save button widgets
         zd->compbutton[ii] = bulab[ii];                                               //  and button labels
      }
   }

   zd->compbutton[nbu] = 0;                                                            //  mark EOL

   zd->dialog = dialog;                                                                //  dialog window
   zd->title = zstrdup(title,"zdialog");                                               //  dialog title
   zd->parent = parent;                                                                //  parent window
   zd->sentinel1 = zdsentinel | (lrandz() & 0x0000FFFF);                               //  validity sentinels
   zd->sentinel2 = zd->sentinel1;                                                      //  fixed part + random part
   zd->uniqueID = uniqueID++;                                                          //  increment unique ID
   zd->eventCB = 0;                                                                    //  no user event callback function
   zd->zstat = 0;                                                                      //  no zdialog status
   zd->disabled = 1;                                                                   //  widget signals disabled
   zd->saveposn = 0;                                                                   //  position not saved

   zd->widget[0].wname = "dialog";                                                     //  set up 1st widget = dialog
   zd->widget[0].type = "dialog";
   zd->widget[0].pname = 0;                                                            //  no parent
   zd->widget[0].data = zstrdup(title,"zdialog");
   zd->widget[0].widget = dialog;
   zd->widget[1].type = 0;                                                             //  eof - no contained widgets yet

   return zd;
}


//  change a zdialog title

void zdialog_set_title(zdialog *zd, ch *title)
{
   if (! main_thread()) zappcrash("illegal call from thread");
   gtk_window_set_title(GTK_WINDOW(zd->widget[0].widget),title);
   return;
}


//  set a zdialog to be modal

void zdialog_set_modal(zdialog *zd)
{
   if (! main_thread()) zappcrash("illegal call from thread");
   GtkWidget  *widget = zdialog_gtkwidget(zd,"dialog");
   gtk_window_set_modal(GTK_WINDOW(widget),1);
   gtk_window_set_keep_above(GTK_WINDOW(widget),1);
   return;
}


//  set a zdialog to be decorated or not

void zdialog_set_decorated(zdialog *zd, int decorated)
{
   void zdialog_drag(GtkWidget *widget, GdkEventButton *event, void *);

   GtkWidget  *widget;

   if (! main_thread()) zappcrash("illegal call from thread");

   widget = zdialog_gtkwidget(zd,"dialog");
   gtk_window_set_decorated(GTK_WINDOW(widget),decorated);
   if (decorated) return;
   gtk_widget_add_events(widget,GDK_BUTTON_PRESS_MASK);
   gtk_widget_add_events(widget,GDK_BUTTON_RELEASE_MASK);
   gtk_widget_add_events(widget,GDK_POINTER_MOTION_MASK);
   G_SIGNAL(widget,"button-press-event",zdialog_drag,0);                               //  connect mouse events to drag
   G_SIGNAL(widget,"button-release-event",zdialog_drag,0);                             //    undecorated window
   G_SIGNAL(widget,"motion-notify-event",zdialog_drag,0);
   return;
}

void zdialog_drag(GtkWidget *widget, GdkEventButton *event, void *)
{
   static int  bdown = 0, type;
   static int  mx0, my0, mx, my;
   static int  wx0, wy0, wx, wy;

   if (! main_thread()) zappcrash("illegal call from thread");

   type = event->type;
   gdk_device_get_position(zfuncs::mouse,0,&mx,&my);                                   //  mouse position in monitor

   if (type == GDK_BUTTON_PRESS) {
      bdown = 1;
      mx0 = mx;                                                                        //  drag start
      my0 = my;
      gtk_window_get_position(GTK_WINDOW(widget),&wx0,&wy0);                           //  initial window position
   }

   if (type == GDK_BUTTON_RELEASE)
      bdown = 0;

   if (type == GDK_MOTION_NOTIFY) {
      if (! bdown) return;
      wx = wx0 + mx - mx0;
      wy = wy0 + my - my0;
      gtk_window_move(GTK_WINDOW(widget),wx,wy);
   }

   return;
}


//  present a zdialog (visible and on top)

void zdialog_present(zdialog *zd)
{
   if (! main_thread()) zappcrash("illegal call from thread");

   zmainsleep(0.1);
   if (! zdialog_valid2(zd)) return;
   gtk_window_present(GTK_WINDOW(zd->dialog));
   return;
}


//  set focus on dialog window or window and named widget
//  (widget name may be null or missing)
//  see also: gtk_window_activate_focus(GtkWindow *)

void zdialog_set_focus(zdialog *zd, ch *wname)
{
   GtkWindow   *window;
   GtkWidget   *widget;

   if (! main_thread()) zappcrash("illegal call from thread");

   window = GTK_WINDOW(zd->dialog);
   if (wname) widget = zdialog_gtkwidget(zd,wname);
   else widget = 0;

   if (wname) gtk_window_set_focus(window,widget);
   else gtk_window_activate_focus(window);
   return;
}


//  add widget to existing zdialog
//
//   Arguments after parent are optional and default to 0.
//   zd         zdialog *, created with zdialog_new()
//   type       string, one of the widget types listed below
//   wname      string, widget name, used to stuff or fetch widget data
//   parent     string, parent name: "dialog" or a previously added container widget
//   data       string, initial data for widget (label name, entry string, spin value, etc.)
//   size       cc for text entry or pixel size for image widget
//   homog      for hbox or vbox to make even space allocation for contained widgets
//   expand     widget should expand with dialog box expansion
//   space      extra space between this widget and neighbors, pixels
//   wrap       allow text to wrap at right margin
//
//   data can be a string ("initial widget data") or a number in string form ("12.345")
//   data for togbutt / check / radio: use "0" or "1" for OFF or ON
//   data for spin / zspin / hscale / vscale: use "min|max|step|value" (default: 0 | 100 | 1 | 50)
//   data for colorbutt: use "rrr|ggg|bbb" with values 0-255 for each RGB color.
//   This format is used to initialize the control and read back when user selects a color.
//   Multiple radio buttons with same parent are a group: pressing one turns the others OFF.

int zdialog_add_widget_long (
     zdialog *zd, ch *type, ch *wname, ch *pname,                                      //  mandatory args
     ch *data, int size, int homog, int expand, int space, int wrap)                   //  optional args (default = 0)
{
   int textview_focus_in_event(GtkWidget *widget);                                     //  for popup_text insertion
   int zdialog_activate_event(GtkWidget *, zdialog *zd);

   GtkWidget      *widget = 0, *pwidget = 0, *fwidget = 0;
   GtkWidget      *image, *vbox;
   GtkTextBuffer  *editBuff = 0;
   PIXBUF         *pixbuf = 0;
   GdkRGBA        gdkrgba;
   GError         *gerror = 0;
   ch             *pp, *ptype = 0;
   ch             vdata[30], iconpath[200];
   double         min, max, step, val;
   double         f256 = 1.0 / 256.0;
   int            iiw, iip, kk, err;
   
   if (! main_thread()) zappcrash("illegal call from thread");

   if (! zdialog_valid(zd)) zappcrash("zdialog invalid");

   for (iiw = 1; zd->widget[iiw].type; iiw++);                                         //  find next avail. slot
   if (iiw > zdmaxwidgets-2) zappcrash("too many widgets: %d",iiw);

   zd->widget[iiw].type = zstrdup(type,"zdialog");                                     //  initz. widget struct
   zd->widget[iiw].wname = zstrdup(wname,"zdialog");
   zd->widget[iiw].pname = zstrdup(pname,"zdialog");
   zd->widget[iiw].data = 0;
   zd->widget[iiw].size = size;
   zd->widget[iiw].homog = homog;
   zd->widget[iiw].expand = expand;
   zd->widget[iiw].space = space;
   zd->widget[iiw].wrap = wrap;
   zd->widget[iiw].widget = 0;

   zd->widget[iiw+1].type = 0;                                                         //  set new EOF marker

   if (strmatchV(type,"dialog","hbox","vbox","hsep","vsep","frame","scrwin",
                      "label","link","entry","zentry","zedit","text","report",
                      "button","zbutton","togbutt","check","radio",
                      "imagebutt","colorbutt","combo","spin","zspin",
                      "hscale","hscale2","vscale","icon","image",null) == 0) {
      printf("*** zdialog, bad widget type: %s \n",type);
      return 0;
   }

   for (iip = iiw-1; iip >= 0; iip--)                                                  //  find parent (container) widget
      if (strmatch(pname,zd->widget[iip].wname)) break;
   if (iip < 0) zappcrash("zdialog, no parent for widget: %s",wname);

   pwidget = zd->widget[iip].widget;                                                   //  parent widget, type
   ptype = zd->widget[iip].type;

   if (strmatchV(ptype,"dialog","hbox","vbox","frame","scrwin",null) == 0)
      zappcrash("zdialog, bad widget parent type: %s",ptype);

   if (strmatch(type,"hbox")) widget = gtk_box_new(HORIZONTAL,space);                  //  expandable container boxes
   if (strmatch(type,"vbox")) widget = gtk_box_new(VERTICAL,space);
   if (strstr("hbox vbox",type))
      gtk_box_set_homogeneous(GTK_BOX(widget),homog);

   if (strmatch(type,"hsep")) widget = gtk_separator_new(HORIZONTAL);                  //  horiz. & vert. separators
   if (strmatch(type,"vsep")) widget = gtk_separator_new(VERTICAL);

   if (strmatch(type,"frame")) {                                                       //  frame around contained widgets
      widget = gtk_frame_new(data);
      gtk_frame_set_shadow_type(GTK_FRAME(widget),GTK_SHADOW_IN);
      data = 0;
   }

   if (strmatch(type,"scrwin")) {                                                      //  scrolled window container
      widget = gtk_scrolled_window_new(0,0);
      gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(widget),
                        GTK_POLICY_AUTOMATIC,GTK_POLICY_AUTOMATIC);
      gtk_scrolled_window_set_overlay_scrolling(GTK_SCROLLED_WINDOW(widget),0);
      data = 0;
   }

   if (strmatch(type,"label")) {                                                       //  label (static text)
      widget = gtk_label_new(data);
      if (size) gtk_label_set_width_chars(GTK_LABEL(widget),size);
      if (data && strstr(data,"<span"))
         gtk_label_set_markup(GTK_LABEL(widget),data);
      data = 0;
   }

   if (strmatch(type,"link")) {                                                        //  label is clickable
      if (strmatch(wname,"nolabel"))
         widget = gtk_link_button_new(data);                                           //  link is also label
      else
         widget = gtk_link_button_new_with_label(data,wname);                          //  label >> link
      G_SIGNAL(widget,"clicked",zdialog_widget_event,zd);
      data = 0;
   }

   if (strmatch(type,"entry")) {                                                       //  text input, single line
      widget = gtk_entry_new();
      if (data) gtk_entry_set_text(GTK_ENTRY(widget),data);
      if (size) gtk_entry_set_width_chars(GTK_ENTRY(widget),size);
      G_SIGNAL(widget,"changed",zdialog_widget_event,zd);
      G_SIGNAL(widget,"activate",zdialog_activate_event,zd);
   }

   if (strmatch(type,"zentry")) {                                                      //  text input, single line
      widget = gtk_text_view_new();
      gtk_text_view_set_top_margin(GTK_TEXT_VIEW(widget),2);
      gtk_text_view_set_bottom_margin(GTK_TEXT_VIEW(widget),2);
      gtk_text_view_set_left_margin(GTK_TEXT_VIEW(widget),5);
      if (! size) size = 10;                                                           //  scale widget for font size
      gtk_widget_set_size_request(widget,size*appfontsize,2*appfontsize);
      gtk_text_view_set_editable(GTK_TEXT_VIEW(widget),1);
      gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(widget),GTK_WRAP_NONE);                //  this does nothing    GTK bug
      gtk_text_view_set_accepts_tab(GTK_TEXT_VIEW(widget),0);
      editBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
      if (data) gtk_text_buffer_set_text(editBuff,data,-1);
      G_SIGNAL(editBuff,"changed",zdialog_widget_event,zd);                            //  buffer signals, not widget
      G_SIGNAL(widget,"focus-in-event",textview_focus_in_event,widget);                //  for popup_text inserts
   }

   if (strmatch(type,"zedit")) {                                                       //  text input, opt. multi-line
      widget = gtk_text_view_new();
      gtk_text_view_set_top_margin(GTK_TEXT_VIEW(widget),2);
      gtk_text_view_set_bottom_margin(GTK_TEXT_VIEW(widget),2);
      gtk_text_view_set_left_margin(GTK_TEXT_VIEW(widget),5);
      if (! size) size = 10;                                                           //  scale widget for font size
      gtk_widget_set_size_request(widget,size*appfontsize,2*appfontsize);
      gtk_text_view_set_editable(GTK_TEXT_VIEW(widget),1);
      if (wrap) gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(widget),GTK_WRAP_WORD);
      gtk_text_view_set_accepts_tab(GTK_TEXT_VIEW(widget),0);
      editBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
      if (data) gtk_text_buffer_set_text(editBuff,data,-1);
      G_SIGNAL(editBuff,"changed",zdialog_widget_event,zd);                            //  buffer signals, not widget
      G_SIGNAL(widget,"focus-in-event",textview_focus_in_event,widget);                //  for popup_text inserts
   }

   if (strmatch(type,"text") || strmatch(type,"report")) {                             //  text output (not editable)
      widget = gtk_text_view_new();
      gtk_text_view_set_top_margin(GTK_TEXT_VIEW(widget),2);
      gtk_text_view_set_bottom_margin(GTK_TEXT_VIEW(widget),2);
      gtk_text_view_set_left_margin(GTK_TEXT_VIEW(widget),5);
      if (wrap) gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(widget),GTK_WRAP_WORD);
      editBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
      if (data) gtk_text_buffer_set_text(editBuff,data,-1);
      gtk_text_view_set_editable(GTK_TEXT_VIEW(widget),0);
      gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(widget),0);
      if (strmatch(type,"text")) gtk_widget_set_can_focus(widget,0);                   //  inactivate KB navi keys
      type = "text";
   }

   if (strmatch(type,"button")) {                                                      //  button
      widget = gtk_button_new_with_label(data);
      G_SIGNAL(widget,"clicked",zdialog_widget_event,zd);
      data = 0;
   }

   if (strmatch(type,"zbutton")) {                                                     //  checkbox used as small button
      if (data) widget = gtk_check_button_new_with_label(data);
      else  widget = gtk_check_button_new();
      G_SIGNAL(widget,"toggled",zdialog_widget_event,zd);
      data = "0";                                                                      //  default data
   }

   if (strmatch(type,"togbutt")) {                                                     //  toggle button
      widget = gtk_toggle_button_new_with_label(data);
      G_SIGNAL(widget,"toggled",zdialog_widget_event,zd);
      data = "0";                                                                      //  default data
   }

   if (strmatch(type,"imagebutt")) {                                                   //  button with image
      snprintf(iconpath,200,"%s/%s",get_zimagedir(),data);
      data = 0;
      pixbuf = gdk_pixbuf_new_from_file_at_scale(iconpath,size,size,1,&gerror);
      if (pixbuf) {
         image = gtk_image_new_from_pixbuf(pixbuf);
         g_object_unref(pixbuf);
      }
      else image = gtk_image_new_from_icon_name("missing",GTK_ICON_SIZE_BUTTON);
      widget = gtk_button_new_with_label(data);
      gtk_button_set_image(GTK_BUTTON(widget),image);
      G_SIGNAL(widget,"clicked",zdialog_widget_event,zd);
   }

   if (strmatch(type,"colorbutt")) {                                                   //  color edit button
      if (! data) data = "0|0|0";                                                      //  data format: "nnn|nnn|nnn" = RGB
      pp = substring(data,'|',1); gdkrgba.red = f256 * atoi(pp);                       //  RGB values are 0-1
      pp = substring(data,'|',2); gdkrgba.green = f256 * atoi(pp);
      pp = substring(data,'|',3); gdkrgba.blue = f256 * atoi(pp);
      gdkrgba.alpha = 1.0;
      widget = gtk_color_button_new_with_rgba(&gdkrgba);
      G_SIGNAL(widget,"color-set",zdialog_widget_event,zd);
   }

   if (strmatch(type,"check")) {                                                       //  checkbox
      if (data) widget = gtk_check_button_new_with_label(data);
      else  widget = gtk_check_button_new();
      G_SIGNAL(widget,"toggled",zdialog_widget_event,zd);
      data = "0";                                                                      //  default data
   }

   if (strmatch(type,"combo")) {                                                       //  combo box
      widget = gtk_combo_box_text_new();
      G_SIGNAL(widget,"changed",zdialog_widget_event,zd);
      G_SIGNAL(widget,"popdown",zdialog_widget_event,zd);                              //  fails for wayland
   }

   if (strmatch(type,"radio")) {                                                       //  radio button
      for (kk = iip+1; kk <= iiw; kk++)
         if (strmatch(zd->widget[kk].pname,pname) &&                                   //  find first radio button
             strmatch(zd->widget[kk].type,"radio")) break;                             //    with same container
      if (kk == iiw)
         widget = gtk_radio_button_new_with_label(null,data);                          //  this one is first
      else
         widget = gtk_radio_button_new_with_label_from_widget                          //  not first, add to group
              (GTK_RADIO_BUTTON(zd->widget[kk].widget),data);
      G_SIGNAL(widget,"toggled",zdialog_widget_event,zd);
      data = "0";                                                                      //  default data
   }

   if (strmatchV(type,"spin","hscale","hscale2","vscale",null)) {                      //  spin button or sliding scale
      if (! data) zappcrash("zdialog_add_widget(): data missing");                     //  "min|max|step|value"
      pp = substring(data,'|',1); err = convSD(pp,min);
      pp = substring(data,'|',2); err += convSD(pp,max);
      pp = substring(data,'|',3); err += convSD(pp,step);
      pp = substring(data,'|',4); err += convSD(pp,val);
      if (err) zappcrash("zdialog_add_widget(): bad data: %s",data);

      zd->widget[iiw].lolim = min;
      zd->widget[iiw].hilim = max;
      zd->widget[iiw].step = step;

      if (strmatch(type,"spin")) {
         widget = gtk_spin_button_new_with_range(min,max,step);
         gtk_spin_button_set_value(GTK_SPIN_BUTTON(widget),val);
      }
      if (strmatch(type,"hscale")) {
         widget = gtk_scale_new_with_range(HORIZONTAL,min,max,step);
         gtk_range_set_value(GTK_RANGE(widget),val);
         gtk_scale_set_draw_value(GTK_SCALE(widget),0);
         gtk_scale_set_has_origin(GTK_SCALE(widget),0);
      }
      if (strmatch(type,"hscale2")) {                                                  //  add digital value on the right
         widget = gtk_scale_new_with_range(HORIZONTAL,min,max,step);
         gtk_range_set_value(GTK_RANGE(widget),val);
         gtk_scale_set_draw_value(GTK_SCALE(widget),1);
         gtk_scale_set_value_pos(GTK_SCALE(widget),GTK_POS_RIGHT);
         gtk_scale_set_has_origin(GTK_SCALE(widget),0);
      }
      if (strmatch(type,"vscale")) {
         widget = gtk_scale_new_with_range(VERTICAL,min,max,step);
         gtk_range_set_value(GTK_RANGE(widget),val);
         gtk_scale_set_draw_value(GTK_SCALE(widget),0);
      }

      G_SIGNAL(widget,"value-changed",zdialog_widget_event,zd);
      snprintf(vdata,30,"%g",val);
      data = vdata;
   }

   if (strmatch(type,"zspin")) {                                                       //  "zspin" widget with range
      if (! data) zappcrash("zdialog_add_widget(): data missing");                     //  "min|max|step|value"
      pp = substring(data,'|',1); err = convSD(pp,min);
      pp = substring(data,'|',2); err += convSD(pp,max);
      pp = substring(data,'|',3); err += convSD(pp,step);
      pp = substring(data,'|',4); err += convSD(pp,val);
      if (err) zappcrash("zdialog_add_widget(): bad data: %s",data);

      zd->widget[iiw].lolim = min;
      zd->widget[iiw].hilim = max;
      zd->widget[iiw].step = step;
      err = convDS(val,7,vdata);                                                       //  initial value >> text
      data = vdata;
      widget = gtk_text_view_new();                                                    //  GTK widget is text_view
      gtk_text_view_set_top_margin(GTK_TEXT_VIEW(widget),2);
      gtk_text_view_set_left_margin(GTK_TEXT_VIEW(widget),5);
      if (! size) size = 5;                                                            //  scale widget for font size
      gtk_widget_set_size_request(widget,size*appfontsize,2*appfontsize);
      gtk_text_view_set_editable(GTK_TEXT_VIEW(widget),1);
      gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(widget),GTK_WRAP_NONE);
      gtk_text_view_set_accepts_tab(GTK_TEXT_VIEW(widget),0);
      gtk_text_view_set_input_purpose(GTK_TEXT_VIEW(widget),GTK_INPUT_PURPOSE_NUMBER);
      editBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
      gtk_text_buffer_set_text(editBuff,data,-1);
      gtk_widget_add_events(widget,GDK_SCROLL_MASK);
      gtk_widget_add_events(widget,GDK_FOCUS_CHANGE_MASK);
      G_SIGNAL(editBuff,"changed",zdialog_zspin_event,zd);                             //  buffer signals, not widget
      G_SIGNAL(widget,"key-press-event",zdialog_zspin_event,zd);
      G_SIGNAL(widget,"focus-out-event",zdialog_zspin_event,zd);
      G_SIGNAL(widget,"scroll-event",zdialog_zspin_event,zd);
   }

   if (strmatch(type,"icon")) {                                                        //  image widget from icon
      snprintf(iconpath,200,"%s/%s",get_zimagedir(),data);
      data = 0;                                                                        //  data not further used
      pixbuf = gdk_pixbuf_new_from_file_at_scale(iconpath,size,size,1,&gerror);
      if (pixbuf) {
         widget = gtk_image_new_from_pixbuf(pixbuf);
         g_object_unref(pixbuf);
      }
      else widget = gtk_image_new_from_icon_name("missing",GTK_ICON_SIZE_BUTTON);
   }

   if (strmatch(type,"image"))                                                         //  image widget from pixbuf
      widget = gtk_image_new_from_pixbuf((GdkPixbuf *) data);                          //  use (ch *) pixbuf in call

   //  all widget types come here

   zd->widget[iiw].widget = widget;                                                    //  set widget in zdialog

   if (strmatchV(type,"zentry","zspin","zedit","text","scrwin",null)) {                //  add frame around these widgets
      if (! strmatch(ptype,"scrwin")) {                                                //    unless parent is scroll widget
         fwidget = gtk_frame_new(0);
         gtk_frame_set_shadow_type(GTK_FRAME(fwidget),GTK_SHADOW_IN);
         gtk_container_add(GTK_CONTAINER(fwidget),widget);
         widget = fwidget;
      }
   }

   if (strmatch(ptype,"hbox") || strmatch(ptype,"vbox"))                               //  add to hbox/vbox
      gtk_box_pack_start(GTK_BOX(pwidget),widget,expand,expand,space);
   if (strmatch(ptype,"frame"))                                                        //  add to frame
      gtk_container_add(GTK_CONTAINER(pwidget),widget);
   if (strmatch(ptype,"scrwin"))                                                       //  add to scroll window
      gtk_container_add(GTK_CONTAINER(pwidget),widget);
   if (strmatch(ptype,"dialog")) {                                                     //  add to dialog box
      vbox = gtk_bin_get_child(GTK_BIN(pwidget));                                      //  dialog is a gtkwindow
      gtk_box_pack_start(GTK_BOX(vbox),widget,expand,expand,space);
   }
   if (data) zd->widget[iiw].data = zstrdup(data,"zdialog");                           //  widget memory

   return 0;
}


//  add widget to existing zdialog - alternative form (clearer and easier code)
//  options: "size=nn | homog | expand | space=nn | wrap"  (all optional, any order)

int zdialog_add_widget(zdialog *zd, ch *type, ch *wname, ch *parent, ch *data, ch *options)
{
   int      size = 0, homog = 0, expand = 0, space = 0, wrap = 0;
   ch       *pp, *param;
   int      stat, Nth, pval = 0;

   if (! main_thread()) zappcrash("illegal call from thread");

   if (! options) options = "";

   for (Nth = 1; true; Nth++)
   {
      param = substring(options,"|",Nth);                                              //  get pname=value
      if (! param) break;
      pp = strchr(param,'=');
      if (pp) pval = atoi(pp+1);
      if (strstr(param,"size")) size = pval;
      else if (strstr(param,"homog")) homog = 1;
      else if (strstr(param,"expand")) expand = 1;
      else if (strstr(param,"space")) space = pval;
      else if (strstr(param,"wrap")) wrap = 1;
      else zappcrash("bad zdialog options: %s",options);
   }

   stat = zdialog_add_widget_long(zd,type,wname,parent,data,size,homog,expand,space,wrap);
   return stat;
}


//  return 1/0 if zdialog is valid/invalid

int zdialog_valid(zdialog *zd, ch *title)                                              //  title is optional
{
   int      ok, ii;

   if (! zd) return 0;

   for (ii = 0; ii < zdialog_count; ii++)                                              //  find in valid zdialog list
      if (zd == zdialog_list[ii]) break;
   if (ii == zdialog_count) {
      printf("*** zdialog invalid %s \n",title);
      return 0;
   }

   ok = 1;
   if ((zd->sentinel1 & 0xFFFF0000) != zdsentinel) ok = 0;
   if (zd->sentinel2 != zd->sentinel1) ok = 0;
   if (! ok) {
      printf("*** zdialog sentinel invalid %s \n",title);
      return 0;
   }

   if (title && ! strmatch(title,zd->title)) {
      printf("*** zdialog title invalid %s \n",title);
      return 0;
   }

   return 1;
}


//  return 1/0 if zdialog is valid/invalid
//  silent version to use when zdialog is possibly destroyed

int zdialog_valid2(zdialog *zd, ch *title)
{
   int      ok, ii;

   if (! zd) return 0;

   for (ii = 0; ii < zdialog_count; ii++)
      if (zd == zdialog_list[ii]) break;
   if (ii == zdialog_count) return 0;

   ok = 1;
   if ((zd->sentinel1 & 0xFFFF0000) != zdsentinel) ok = 0;
   if (zd->sentinel2 != zd->sentinel1) ok = 0;
   if (! ok) return 0;

   if (title && ! strmatch(title,zd->title)) return 0;

   return 1;
}


//  find zdialog widget from zdialog and widget name

int zdialog_find_widget(zdialog *zd, ch *wname)
{
   int      ii;

   if (! zdialog_valid(zd)) {
      printf("*** invalid zdialog %p \n",zd);
      return 0;
   }

   for (ii = 0; zd->widget[ii].type; ii++)
      if (strmatch(zd->widget[ii].wname,wname)) return ii;
   return 0;
}


//  get GTK widget from zdialog and widget name

GtkWidget * zdialog_gtkwidget(zdialog *zd, ch *wname)
{
   if (strmatch(wname,"dialog")) return zd->widget[0].widget;
   int ii = zdialog_find_widget(zd,wname);
   if (ii) return zd->widget[ii].widget;
   return 0;
}


//  set an "image" widget type from a GDK pixbuf
//  returns 0 if OK, else +N

int zdialog_set_image(zdialog *zd, ch *wname, GdkPixbuf *pixbuf)
{
   GtkWidget   *widget;
   int         ii;

   if (! main_thread()) zappcrash("illegal call from thread");

   ii = zdialog_find_widget(zd,wname);
   if (! ii) return 2;
   if (! strmatch(zd->widget[ii].type,"image")) return 3;
   widget = zd->widget[ii].widget;
   gtk_image_set_from_pixbuf(GTK_IMAGE(widget),pixbuf);
   return 0;
}


//  add a popup tool tip to a zdialog widget

int zdialog_add_ttip(zdialog *zd, ch *wname, ch *ttip)
{
   GtkWidget   *widget;
   int         ii;

   if (! main_thread()) zappcrash("illegal call from thread");

   if (! zdialog_valid(zd)) return 0;

   for (ii = 0; zd->compwidget[ii]; ii++)                                              //  search completion buttons
      if (strmatch(zd->compbutton[ii],TX(wname))) {                                    //    for matching wname
         gtk_widget_set_tooltip_text(zd->compwidget[ii],ttip);
         return 1;
      }

   widget = zdialog_gtkwidget(zd,wname);                                               //  search zdialog widgets
   if (! widget) return 0;

   gtk_widget_set_tooltip_text(widget,ttip);
   return 1;
}


//  resize dialog to a size greater than initial size
//  (as determined by the included widgets)

int zdialog_resize(zdialog *zd, int width, int height)
{
   if (! main_thread()) zappcrash("illegal call from thread");

   if (! zdialog_valid(zd)) return 0;
   if (! width) width = 10;                                                            //  stop spurious GTK warnings
   if (! height) height = 10;
   GtkWidget *window = zd->widget[0].widget;
   gtk_window_set_default_size(GTK_WINDOW(window),width,height);
   return 1;
}


//  put data into a zdialog widget
//  private function

int zdialog_put_data(zdialog *zd, ch *wname, ch *data)
{
   GtkWidget      *widget;
   GtkTextBuffer  *textBuff;
   GdkRGBA        gdkrgba;
   int            iiw, nn, kk, err, Nsteps;
   ch             *type, *pp;
   ch             *wdata, sdata[32];
   double         dval;
   double         f256 = 1.0 / 256.0;
   double         lval, hval, nval, F, F2;
   double         fdata, lolim, hilim, step;                                           //  double

   if (! main_thread()) zappcrash("illegal call from thread");

   iiw = zdialog_find_widget(zd,wname);
   if (! iiw) return 0;

   type = zd->widget[iiw].type;
   widget = zd->widget[iiw].widget;

   wdata = zd->widget[iiw].data;
   if (wdata) zfree(wdata);                                                            //  free prior data memory
   zd->widget[iiw].data = 0;

   if (data) {
      if (utf8_check(data)) wdata = zstrdup("bad UTF8 data","zdialog");                //  replace bad UTF-8 encoding
      else wdata = zstrdup(data,"zdialog");                                            //  set new data for widget
      zd->widget[iiw].data = wdata;
   }

   zd->disabled++;                                                                     //  disable for widget stuffing

   if (strmatch(type,"label"))
      gtk_label_set_text(GTK_LABEL(widget),data);

   if (strmatch(type,"link"))
      gtk_label_set_text(GTK_LABEL(widget),data);

   if (strmatch(type,"entry"))
      gtk_entry_set_text(GTK_ENTRY(widget),data);

   if (strmatch(type,"zentry")) {                                                      //  text input, single line
      textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
      gtk_text_buffer_set_text(textBuff,data,-1);
   }

   if (strmatchV(type,"button","zbutton",null))                                        //  change button label
      gtk_button_set_label(GTK_BUTTON(widget),data);

   if (strmatch(type,"zedit")) {                                                       //  text input to editable text
      textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
      gtk_text_buffer_set_text(textBuff,data,-1);
   }

   if (strmatch(type,"text")) {                                                        //  text output
      textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
      gtk_text_buffer_set_text(textBuff,data,-1);
   }

   if (strmatchV(type,"togbutt","check","radio",null))
   {
      if (! data) kk = nn = 0;
      else kk = convSI(data,nn);
      if (kk != 0) nn = 0;                                                             //  data not integer, force zero
      if (nn <= 0) nn = 0; else nn = 1;
      gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget),nn);                      //  set gtk widget value
   }

   if (strmatch(type,"spin")) {
      kk = convSD(data,dval);
      if (kk != 0) dval = 0.0;
      gtk_spin_button_set_value(GTK_SPIN_BUTTON(widget),dval);
   }

   if (strmatch(type,"zspin")) {                                                       //  "zspin" widget
      lolim = zd->widget[iiw].lolim;
      hilim = zd->widget[iiw].hilim;
      step = zd->widget[iiw].step;
      err = convSD(data,fdata);                                                        //  string --> double
      if (err) goto retx;
      Nsteps = (fdata - lolim) / step + 0.5;                                           //  nearest exact step
      fdata = lolim + Nsteps * step;
      if (fdata < lolim) fdata = lolim;                                                //  enforce limits
      if (fdata > hilim) fdata = hilim;
      convDS(fdata,7,sdata);                                                           //  double --> string, precision 7
      textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
      gtk_text_buffer_set_text(textBuff,sdata,-1);
   }

   if (strmatch(type,"colorbutt")) {                                                   //  color button data is nnn|nnn|nnn
      pp = substring(data,'|',1);
      if (pp) gdkrgba.red = f256 * atoi(pp);                                           //  RGB range is 0-1
      pp = substring(data,'|',2);
      if (pp) gdkrgba.green = f256 * atoi(pp);
      pp = substring(data,'|',3);
      if (pp) gdkrgba.blue = f256 * atoi(pp);
      gdkrgba.alpha = 1.0;
      gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(widget),&gdkrgba);
   }

   if (strmatchV(type,"hscale","hscale2","vscale",null))                               //  slider widget
   {
      kk = convSD(data,dval);                                                          //  zdialog widget value
      if (kk != 0) dval = 0.0;

      if (zd->widget[iiw].rescale)                                                     //  widget value --> slider value
      {
         lval = zd->widget[iiw].lval;                                                  //  rescaled for more sensitivity
         nval = zd->widget[iiw].nval;                                                  //    around neutral value
         hval = zd->widget[iiw].hval;

         if (dval > lval && dval <= nval) {                                            //  if dval == lval or dval == hval
            F2 = (nval - dval) / (nval - lval);                                        //    then dval is not revised
            F = sqrtf(F2);
            dval = nval - F * (nval - lval);
         }

         else if (dval >= nval && dval < hval) {
            F2 = (dval - nval) / (hval - nval);
            F = sqrtf(F2);
            dval = nval + F * (hval - nval);
         }
      }

      gtk_range_set_value(GTK_RANGE(widget),dval);
   }

   if (strmatch(type,"combo"))                                                         //  combo box
   {
      if (blank_null(data))                                                            //  if blank, set no active entry
         gtk_combo_box_set_active(GTK_COMBO_BOX(widget),-1);
      else {
         if (! zd->widget[iiw].zlist)                                                  //  add parallel zlist if not already
            zd->widget[iiw].zlist = zlist_new(0);
         nn = zlist_find(zd->widget[iiw].zlist,data,0);                                //  find matching zlist entry
         if (nn < 0) {
            zlist_append(zd->widget[iiw].zlist,data,0);                                //  not found, append new entry to zlist
            gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(widget),data);           //  append new entry to combo box
            nn = zlist_count(zd->widget[iiw].zlist) - 1;                               //  entry = count - 1
         }
         gtk_combo_box_set_active(GTK_COMBO_BOX(widget),nn);                           //  set combo box active entry
      }
   }

retx:
   zd->disabled--;                                                                     //  re-enable dialog
   return iiw;
}


//  get data from a dialog widget based on its name
//  private function

ch * zdialog_get_data(zdialog *zd, ch *wname)
{
   if (! main_thread()) zappcrash("illegal call from thread");
   int ii = zdialog_find_widget(zd,wname);
   if (ii) return zd->widget[ii].data;
   return 0;
}


//  set new limits for a numeric data entry widget (spin, zspin, hscale, vscale)

int zdialog_set_limits(zdialog *zd, ch *wname, double min, double max)
{
   GtkWidget   *widget;
   ch          *type;
   int         iiw;

   if (! main_thread()) zappcrash("illegal call from thread");

   iiw = zdialog_find_widget(zd,wname);
   if (! iiw) {
      printf("*** zdialog_set_limits, %s not found \n",wname);
      return 0;
   }

   widget = zd->widget[iiw].widget;
   type = zd->widget[iiw].type;

   if (*type == 's')
      gtk_spin_button_set_range(GTK_SPIN_BUTTON(widget),min,max);

   if (*type == 'h' || *type == 'v')
      gtk_range_set_range(GTK_RANGE(widget),min,max);

   if (*type == 'z') {                                                                 //  zspin
      zd->widget[iiw].lval = min;
      zd->widget[iiw].hval = max;
   }

   return 1;
}


//  get lower and upper limits for numeric data entry widget
//  returns 1 if OK, 0 if not a widget with limits

int zdialog_get_limits(zdialog *zd, ch *wname, double &min, double &max)
{
   int      iiw;

   min = max = 0;

   iiw = zdialog_find_widget(zd,wname);
   if (! iiw) return 0;
   if (! strstr("spin zspin hscale hscale2 vscale",zd->widget[iiw].type))
      return 0;
   min = zd->widget[iiw].lolim;
   max = zd->widget[iiw].hilim;
   return 1;
}


//  Expand a widget scale in the region around the neutral value.
//  Control small adjustments near the neutral value more precisely.
//  lval and hval: the range of values to be rescaled.
//  nval: the neutral value where the scale will be expanded the most.
//        lval <= nval <= hval

int zdialog_rescale(zdialog *zd, ch *wname, float lval, float nval, float hval)
{
   int      iiw;

   iiw = zdialog_find_widget(zd,wname);
   if (! iiw) return 0;

   if (lval > nval || nval > hval) {
      printf("*** zdialog_rescale, bad data: %s \n",wname);
      return 0;
   }

   zd->widget[iiw].rescale = 1;
   zd->widget[iiw].lval = lval;
   zd->widget[iiw].nval = nval;
   zd->widget[iiw].hval = hval;

   return 1;
}


//  run the dialog and send events to the event function
//
//  evfunc: int func(zdialog *zd, ch *event)
//  If present, eventFunc is called when a dialog widget is changed or the dialog
//  is completed. If a widget was changed, event is the widget name.
//  Get the new widget data with zdialog_fetch().
//  If a completion button was pressed, event is "zstat" and zd->zstat will be
//  the button number 1-N.
//  If the dialog was destroyed, event is "zstat" and zd->zstat is negative.
//
//  posn: optional dialog box position:
//    "mouse" = position at mouse
//    "desktop" = center on desktop                      DEFAULT
//    "parent" = center on parent window
//    "nn/nn" = position NW corner at relative x/y position in parent window,
//              where nn/nn is a percent 0-100 of the parent window dimensions.
//    "save" = save last user-set position and use this whenever the dialog
//             is repeated, also across sessions.
//
//  KBevent: extern void KBevent(GdkEventKey *event)
//  This function must be supplied by the caller of zdialog.
//  It is called when Ctrl|Shift|Alt|F1 is pressed.

int zdialog_run(zdialog *zd, zdialog_event evfunc, ch *posn)
{
   int   zdialog_KB_press(GtkWidget *, GdkEventKey *event, zdialog *zd);
   int   zdialog_focus_in_event(GtkWidget *, GdkEvent *event, zdialog *zd);

   GtkWidget   *dialog;

   if (! main_thread()) zappcrash("illegal call from thread");

   if (! zdialog_valid(zd)) return 0;
   if (zd->zrunning) {
      printf("zdialog is already running \n");
      return 0;
   }

   if (posn) zdialog_set_position(zd,posn);                                            //  put dialog at desired position
   else zdialog_set_position(zd,"desktop");                                            //  use default

   if (evfunc) zd->eventCB = (void *) evfunc;                                          //  link to dialog event callback

   printf("dialog started: %s \n",zd->title);

   dialog = zd->widget[0].widget;
   gtk_widget_show_all(dialog);                                                        //  activate dialog

   G_SIGNAL(dialog,"focus-in-event",zdialog_focus_in_event,zd);                        //  connect focus event function
   G_SIGNAL(dialog,"key-press-event",zdialog_KB_press,zd);                             //  connect key press event function
   G_SIGNAL(dialog,"delete-event",zdialog_delete_event,zd);                            //  connect delete event function

   zd->zstat = 0;                                                                      //  dialog status incomplete
   zd->disabled = 0;                                                                   //  enable widget events
   zd->zrunning = 1;                                                                   //  dialog is running
   return 0;
}


//  zdialog event handler - called for dialog events.
//  Updates data in zdialog, calls user callback function (if present).
//  private function

int zdialog_widget_event(GtkWidget *widget, zdialog *zd)
{
   zdialog_event  *evfunc = 0;                                                         //  dialog event callback function

   GtkTextView       *textView = 0;
   GtkTextBuffer     *textBuff = 0;
   GtkTextIter       iter1, iter2;
   GdkRGBA           gdkrgba;
   int               ii, nn;
   ch                *wname, *wtype, *wdata;
   ch                *pp, sdata[20];
   double            dval;
   float             lval, nval, hval, F;

   if (! zdialog_valid2(zd)) return 1;                                                 //  zdialog gone
   if (zd->disabled) return 1;                                                         //  events disabled

   zd->disabled = 1;                                                                   //  disable nested events

   for (ii = 0; ii < zdmaxbutts; ii++) {                                               //  check completion buttons
      if (zd->compwidget[ii] == null) break;                                           //  EOL
      if (zd->compwidget[ii] != widget) continue;
      zd->zstat = ii+1;                                                                //  zdialog status = button no.
      strncpy0(zd->event,"zstat",40);
      strncpy0(zd->zstat_button,zd->compbutton[ii],40);                                //  button label "Cancel" etc.
      wtype = "completion button";
      printf("dialog: %s  button: %s \n",zd->title,zd->zstat_button);
      goto call_evfunc;                                                                //  call zdialog event function
   }

   for (ii = 1; zd->widget[ii].type; ii++)                                             //  find widget in zdialog
      if (zd->widget[ii].widget == widget) goto found_widget;

   for (ii = 1; zd->widget[ii].type; ii++) {                                           //  failed, test if buffer
      if (strmatchV(zd->widget[ii].type,"zedit","zentry",null)) {                      //    of text view widget
         textView = GTK_TEXT_VIEW(zd->widget[ii].widget);
         textBuff = gtk_text_view_get_buffer(textView);
         if (widget == (GtkWidget *) textBuff) goto found_widget;
      }
   }

   printf("zdialog event ignored: %s \n",zd->title);                                   //  not found, ignore event
   zd->disabled = 0;
   return 1;

found_widget:

   wname = zd->widget[ii].wname;
   wtype = zd->widget[ii].type;
   wdata = 0;

   if (strmatch(wtype,"button"))
      wdata = (ch *) gtk_button_get_label(GTK_BUTTON(widget));                         //  button label

   if (strmatch(wtype,"zbutton")) {                                                    //  checkbox as smaller button
      wdata = (ch *) gtk_button_get_label(GTK_BUTTON(widget));
      gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget),0);                       //  reset checkmark = off
   }

   if (strmatch(wtype,"zedit")) {                                                      //  multi-line
      gtk_text_buffer_get_bounds(textBuff,&iter1,&iter2);
      wdata = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0);
   }

   if (strmatch(wtype,"entry"))
      wdata = (ch *) gtk_entry_get_text(GTK_ENTRY(widget));

   if (strmatch(wtype,"zentry")) {                                                     //  single line
      gtk_text_buffer_get_bounds(textBuff,&iter1,&iter2);
      wdata = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0);
      pp = strchr(wdata,GDK_KEY_Linefeed);                                             //  prohibit return character
      if (pp) {
         *pp = 0;
         gtk_text_buffer_set_text(textBuff,wdata,-1);                                  //  works, generates bogus error message
      }
   }

   if (strmatchV(wtype,"radio","check","togbutt",null))
   {
      nn = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
      if (nn == 0) wdata = "0";
      else wdata = "1";
   }

   if (strmatch(wtype,"combo"))
      wdata = gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(widget));

   if (strmatch(wtype,"spin"))
   {
      dval = gtk_spin_button_get_value(GTK_SPIN_BUTTON(widget));
      snprintf(sdata,20,"%g",dval);
      wdata = sdata;
   }

   if (strmatch(wtype,"colorbutt"))                                                    //  color button
   {
      gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(widget),&gdkrgba);
      snprintf(sdata,20,"%.0f|%.0f|%.0f",gdkrgba.red*255,gdkrgba.green*255,gdkrgba.blue*255);
      wdata = sdata;
   }

   if (strmatchV(wtype,"hscale","hscale2","vscale",null))
   {
      dval = gtk_range_get_value(GTK_RANGE(widget));

      if (zd->widget[ii].rescale)                                                      //  slider value --> widget value
      {
         lval = zd->widget[ii].lval;
         nval = zd->widget[ii].nval;
         hval = zd->widget[ii].hval;

         if (dval > lval && dval < nval) {                                             //  lval ... nval
            F = (nval - dval) / (nval - lval);                                         //  1 ... 0
            dval = (1.0 - F * F) * (nval - lval) + lval;                               //  lval ... nval
         }

         else if (dval > nval && dval < hval) {                                        //  nval ... hval
            F = (dval - nval) / (hval - nval);                                         //  0 ... 1
            dval = F * F * (hval - nval) + nval;                                       //  nval ... hval
         }
      }

      snprintf(sdata,20,"%g",dval);
      wdata = sdata;
   }

    //  all widgets come here

   if (zd->widget[ii].data) zfree(zd->widget[ii].data);                                //  clear prior data
   zd->widget[ii].data = 0;
   if (wdata) zd->widget[ii].data = zstrdup(wdata,"zdialog");                          //  set new data
   zd->lastwidget = widget;                                                            //  remember last widget updated
   strncpy0(zd->event,wname,40);                                                       //  event = widget name

call_evfunc:                                                                           //  call zdialog event function

   if (zd->eventCB) {
      evfunc = (zdialog_event *) zd->eventCB;                                          //  do callback function
      evfunc(zd,zd->event);
   }

   if (zdialog_valid2(zd)) zd->disabled = 0;                                           //  'event' may cause zdialog_free()
   return 1;
}


//  special zdialog handler for GtkTextView widgets
//  track current input widget for popup_text

int textview_focus_in_event(GtkWidget *widget)
{
   curr_textview_widget = GTK_TEXT_VIEW(widget);
   return 1;
}


//  zdialog response handler for "focus-in-event" signal
//  private function

zdialog  *zdialog_focus_zd;                                                            //  current zdialog

int zdialog_focus_in_event(GtkWidget *, GdkEvent *event, zdialog *zd)
{
   if (! zdialog_valid2(zd)) return 0;
   if (zd->zstat) return 0;                                                            //  already complete
   zdialog_focus_zd = zd;
   zdialog_send_event(zd,"focus");                                                     //  notify dialog event function
   return 0;                                                                           //  must be 0
}


//  zdialog response handler for "activate" signal
//  private function

int zdialog_activate_event(GtkWidget *, zdialog *zd)
{
   if (! zdialog_valid2(zd)) return 0;
   if (zd->zstat) return 0;                                                            //  already complete
   zdialog_send_event(zd,"activate");                                                  //  notify dialog event function
   return 0;                                                                           //  must be 0
}


//  zdialog response handler for keyboard events
//  key symbols can be found at /usr/include/gtk-3.0/gdk/gdkkeysyms.h
//  main app must provide: extern void KBevent(GdkEventKey *event)
//  private function

int zdialog_KB_press(GtkWidget *widget, GdkEventKey *kbevent, zdialog *zd)
{
   void  zdialog_copyfunc(GtkWidget *, GtkClipboard *);
   void  zdialog_pastefunc(GtkClipboard *, ch *, void *);

   GtkWidget   *focuswidget;
   int         KBkey = kbevent->keyval;
   ch          *type;
   int         ii;

   if (KBkey == GDK_KEY_Escape) {                                                      //  escape key
      printf("zdialog escape key \n");
      Fescape = 1;
      zd->zstat = -2;
      if (zd->eventCB) zdialog_send_event(zd,"zstat");
      return 1;
   }

   if (KBkey == GDK_KEY_F1) { KBevent(kbevent); return 1; };                           //  these keys handled by main app window
   if (KBkey == GDK_KEY_F10) { KBevent(kbevent); return 1; };
   if (KBkey == GDK_KEY_F11) { KBevent(kbevent); return 1; };

   focuswidget = gtk_window_get_focus(GTK_WINDOW(widget));                             //  find widget in zdialog

   for (ii = 1; zd->widget[ii].type; ii++)
      if (zd->widget[ii].widget == focuswidget) break;

   type = zd->widget[ii].type;                                                         //  screening input widget types removed
   if (! type) return 0;

   strncpy0(zd->event,zd->widget[ii].wname,40);                                        //  save event name
   return 0;                                                                           //  pass KB key to widget
}


//  event function for "zspin" widget

int zdialog_zspin_event(GtkWidget *widget, GdkEvent *event, zdialog *zd)
{
   zdialog_event  *evfunc = 0;                                                         //  dialog event callback function

   GtkTextBuffer        *textBuff;
   GtkTextIter          iter1, iter2;

   int            KBkey;
   int            ii, err, Nsteps, state;
   static float   fincr = 0;                                                           //  25.2
   double         fdata, lolim, hilim, step;                                           //  double
   ch             *wdata, sdata[20];
   int            time, elaps, Fchanged;
   static int     time0 = 0, time1 = 0;

   if (event->type < 0) return 1;                                                      //  GDK bug ?

   if (! zdialog_valid2(zd)) return 0;                                                 //  event after dialog destroyed
   if (zd->disabled) return 0;                                                         //  zdialog events disabled

   for (ii = 1; zd->widget[ii].type; ii++)                                             //  find "zspin" (text view) widget
      if (zd->widget[ii].widget == widget) break;
   if (! zd->widget[ii].type) return 0;                                                //  not found

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));                         //  get widget data
   gtk_text_buffer_get_bounds(textBuff,&iter1,&iter2);
   wdata = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0);

   lolim = zd->widget[ii].lolim;                                                       //  limits and step size
   hilim = zd->widget[ii].hilim;
   step = zd->widget[ii].step;

   if (event->type == GDK_SCROLL) {                                                    //  mouse wheel event
      gtk_widget_grab_focus(widget);
      fincr += - ((GdkEventScroll *) event)->delta_y;                                  //  accumulate scroll values              25.2
      if (fincr > -0.7 && fincr < +0.7) return 0;                                      //  (new mouse, smooth scroll)
      if (fincr < 0) fincr = -1;
      else fincr = +1;
      state = ((GdkEventScroll *) event)->state;                                       //  if shift key held, use 10x step
      if (state & GDK_SHIFT_MASK) fincr *= 10;
      goto checklimits;
   }

   if (event->type == GDK_KEY_PRESS) {                                                 //  KB button press
      KBkey = ((GdkEventKey *) event)->keyval;
      if (KBkey == GDK_KEY_Return) goto checklimits;                                   //  return = entry finished
      if (KBkey == GDK_KEY_Up) fincr = 1;
      if (KBkey == GDK_KEY_Down) fincr = -1;
      if (! fincr) return 0;                                                           //  must return 0

      state = ((GdkEventKey *) event)->state;                                          //  if shift key held, use 10x step
      if (state & GDK_SHIFT_MASK) fincr *= 10;

      time = ((GdkEventKey *) event)->time;                                            //  track time key is held down
      if (time - time1 > 300) time0 = time;
      time1 = time;
      elaps = time - time0;

      if (elaps > 5000) step = 10 * step;                                              //  acceleration table for
      else if (elaps > 4500) step = 9 * step;                                          //    hold time 1-5+ seconds
      else if (elaps > 4000) step = 8 * step;                                          //  use integer values only
      else if (elaps > 3500) step = 7 * step;
      else if (elaps > 3000) step = 6 * step;
      else if (elaps > 2500) step = 5 * step;
      else if (elaps > 2000) step = 4 * step;
      else if (elaps > 1500) step = 3 * step;
      else if (elaps > 1000) step = 2 * step;
      goto checklimits;
   }

   if (event->type == GDK_FOCUS_CHANGE) goto checklimits;                              //  focus change = entry finished
   if (event->type == GDK_LEAVE_NOTIFY) goto checklimits;                              //  pointer out - entry finished
   return 0;

checklimits:

   convSD(wdata,fdata);                                                                //  ignore bad char. inputs
   fdata += fincr * step;

   Nsteps = (fdata - lolim) / step + 0.5;                                              //  set nearest exact step
   fdata = lolim + Nsteps * step;

   fincr = 0;                                                                          //  reset accumulating counter            25.2

   err = 0;
   
   if (fdata < lolim) {
      err = 1;
      fdata = lolim;                                                                   //  force within limits
   }

   if (fdata > hilim) {
      err = 2;
      fdata = hilim;
   }

   if (err) gtk_widget_grab_focus(widget);                                             //  if error, restore focus

   convDS(fdata,7,sdata);                                                              //  round to 7 digits
   gtk_text_buffer_set_text(textBuff,sdata,-1);                                        //  insert recognized value

   Fchanged = 0;
   if (zd->widget[ii].data) {
      if (! strmatch(zd->widget[ii].data,sdata)) Fchanged = 1;                         //  detect if widget data changed
      zfree(zd->widget[ii].data);                                                      //  clear prior widget data
   }
   zd->widget[ii].data = zstrdup(sdata,"zdialog");                                     //  set new data

   zd->lastwidget = widget;                                                            //  remember last widget updated
   strncpy0(zd->event,zd->widget[ii].wname,40);                                        //  event = widget name

   if (zd->eventCB && Fchanged) {                                                      //  if widget data changed
      zd->disabled = 1;
      evfunc = (zdialog_event *) zd->eventCB;                                          //  do event callback function
      evfunc(zd,zd->event);
      if (zdialog_valid2(zd)) zd->disabled = 0;                                        //  'event' may cause zdialog_free()
   }

   if (event->type == GDK_FOCUS_CHANGE) return 0;                                      //  no propagate
   return 1;                                                                           //  propagate
}


//  process Ctrl+C (copy text from widget to clipboard)
//  private function

void zdialog_copyfunc(GtkWidget *widget, GtkClipboard *clipboard)
{
   GtkTextView    *textView = 0;
   GtkTextBuffer  *textBuff = 0;
   zdialog        *zd;
   int            ii, cc = 0;
   ch             *wname;
   ch             text[1000];

   widget = gtk_window_get_focus(GTK_WINDOW(widget));
   if (! widget) return;

   zd = zdialog_focus_zd;
   if (! zdialog_valid2(zd)) return;

   for (ii = 1; zd->widget[ii].type; ii++)                                             //  find widget in zdialog
      if (zd->widget[ii].widget == widget) goto found_widget;
   for (ii = 1; zd->widget[ii].type; ii++) {                                           //  failed, test if buffer
      if (strmatchV(zd->widget[ii].type,"zedit","zentry",null)) {                      //    of text view widget
         textView = GTK_TEXT_VIEW(zd->widget[ii].widget);
         textBuff = gtk_text_view_get_buffer(textView);
         if (widget == (GtkWidget *) textBuff) goto found_widget;
      }
   }
   return;                                                                             //  not found

found_widget:
   wname = zd->widget[ii].wname;
   zdialog_fetch(zd,wname,text,999);                                                   //  current text in widget
   cc = strlen(text);
   gtk_clipboard_set_text(clipboard,text,cc);
   return;
}


//  process Ctrl+V (paste text from clipboard to widget with KB focus)
//  private function

void zdialog_pastefunc(GtkClipboard *clipboard, ch *cliptext, void *arg)
{
   GtkWindow      *window;
   GtkWidget      *widget;
   GtkTextView    *textView = 0;
   GtkTextBuffer  *textBuff = 0;
   zdialog        *zd;
   int            ii, cc = 0;
   ch             *wname;
   ch             text[1000];

   window = (GtkWindow *) arg;
   widget = gtk_window_get_focus(window);
   if (! widget) return;                                                               //  widget for pasted text
   if (! cliptext || ! *cliptext) return;                                              //  clipboard text pasted

   zd = zdialog_focus_zd;
   if (! zdialog_valid2(zd)) return;

   if (zd->zstat) return;

   for (ii = 1; zd->widget[ii].type; ii++)                                             //  find widget in zdialog
      if (zd->widget[ii].widget == widget) goto found_widget;
   for (ii = 1; zd->widget[ii].type; ii++) {                                           //  failed, test if buffer
      if (strmatchV(zd->widget[ii].type,"zedit","zentry",null)) {                      //    of text view widget
         textView = GTK_TEXT_VIEW(zd->widget[ii].widget);
         textBuff = gtk_text_view_get_buffer(textView);
         if (widget == (GtkWidget *) textBuff) goto found_widget;
      }
   }
   return;                                                                             //  not found

found_widget:
   wname = zd->widget[ii].wname;
   zdialog_fetch(zd,wname,text,999);                                                   //  current text in widget
   cc = strlen(text);
   if (cc > 995) return;
   strncpy(text+cc,cliptext,999-cc);                                                   //  add clipboard text
   text[999] = 0;
   zdialog_stuff(zd,wname,text);
   return;
}


//  private function called when zdialog is completed.
//  called when dialog is canceled via [x] button or destroyed by GTK (zstat < 0).

int zdialog_delete_event(GtkWidget *, GdkEvent *, zdialog *zd)
{
   zdialog_event  *evfunc = 0;                                                         //  dialog event callback function

   if (! zd) return 0;

   zd->widget[0].widget = 0;                                                           //  widget no longer valid

   if (! zdialog_valid2(zd)) return 1;                                                 //  already destroyed
   if (zd->zstat) return 1;                                                            //  already complete
   if (zd->disabled) return 1;                                                         //  in process

   zd->zstat = -1;                                                                     //  set zdialog cancel status

   printf("dialog: %s killed\n",zd->title);

   if (zd->eventCB) {
      evfunc = (zdialog_event *) zd->eventCB;                                          //  do callback function
      zd->disabled = 1;
      evfunc(zd,"zstat");
      if (zdialog_valid2(zd)) zd->disabled = 0;                                        //  'event' may cause zdialog_free()
   }

   zdialog_free(zd);
   return 0;
}


//  Send an event name to an active zdialog.
//  The response function eventFunc() will be called with this event.
//  zdialog completion buttons KB shortcuts removed

int zdialog_send_event(zdialog *zd, ch *event)
{
   zdialog_event * evfunc = 0;                                                         //  dialog event callback function

   if (! main_thread()) zappcrash("illegal call from thread");

   if (! zdialog_valid2(zd)) return 0;                                                 //  zdialog canceled
   if (zd->disabled) return 0;                                                         //  zdialog busy

   evfunc = (zdialog_event *) zd->eventCB;
   if (! evfunc) return 0;
   zd->disabled = 1;
   printf("zdialog send event: %s \n",event);
   evfunc(zd,event);                                                                   //  call dialog event function
   if (zdialog_valid2(zd)) zd->disabled = 0;                                           //  'event' may cause zdialog_free()
   return 1;
}


//  Complete an active dialog and assign a status.
//  Equivalent to the user pressing a dialog completion button.
//  The dialog completion function is called if defined,
//  and zdialog_wait() is unblocked.
//  returns:  0 = no active dialog or completion function, 1 = OK

int zdialog_send_response(zdialog *zd, int zstat)
{
   zdialog_event  *evfunc = 0;                                                         //  dialog event callback function

   if (! main_thread()) zappcrash("illegal call from thread");

   if (! zdialog_valid2(zd)) return 0;
   if (zd->disabled) return 0;
   zd->zstat = zstat;                                                                  //  set status
   evfunc = (zdialog_event *) zd->eventCB;
   if (! evfunc) return 0;
   zd->disabled = 1;
   evfunc(zd,"zstat");
   if (zdialog_valid2(zd)) zd->disabled = 0;                                           //  'event' may cause zdialog_free()
   return 1;
}


//  show or hide a zdialog window
//  returns 1 if successful, 0 if zd does not exist.

int zdialog_show(zdialog *zd, int show)
{
   static GtkWidget  *widget, *pwidget = 0;
   static int        posx, posy;

   if (! main_thread()) zappcrash("illegal call from thread");

   if (! zdialog_valid(zd)) return 0;

   widget = zdialog_gtkwidget(zd,"dialog");

   if (show) {                                                                         //  show window
      if (widget == pwidget) {                                                         //  restore prior position
         gtk_window_move(GTK_WINDOW(widget),posx,posy);
         pwidget = 0;
      }
      gtk_widget_show_all(widget);
      gtk_window_present(GTK_WINDOW(widget));                                          //  set focus on restored window
   }
   else {                                                                              //  hide window
      pwidget = widget;
      gtk_window_get_position(GTK_WINDOW(widget),&posx,&posy);                         //  save position
      gtk_widget_hide(widget);
   }
   return 1;
}


//  Destroy the zdialog - must be done by zdialog_run() caller
//  (else dialog continues active even after completion button).
//  Data in widgets remains valid until zdialog_free() is called.

int zdialog_destroy(zdialog *zd)
{
   if (! main_thread()) zappcrash("illegal call from thread");

   if (! zdialog_valid2(zd)) return 0;                                                 //  destroyed, not freed yet

   if (zd->saveposn) zdialog_save_position(zd);                                        //  save position for next use

   if (zd->widget[0].widget) {                                                         //  multiple destroys OK
      gtk_widget_destroy(zd->widget[0].widget);                                        //  destroy GTK dialog
      zd->widget[0].widget = 0;
   }

   if (! zd->zstat) zd->zstat = -1;                                                    //  status = destroyed
   zd->zrunning = 0;                                                                   //  not running
   zd->disabled = 1;                                                                   //  ignore events after destroy
   return 1;
}


//  free zdialog memory (will destroy first, if not already)
//  zd is set to null

int zdialog_free(zdialog *&zd)                                                         //  reference
{
   int      ii;

   if (! main_thread()) zappcrash("illegal call from thread");

   if (! zd) return 0;

   if (! zdialog_valid2(zd)) return 0;                                                 //  validate zd pointer

   zdialog_save_inputs(zd);                                                            //  save user inputs for next use

   zdialog_destroy(zd);                                                                //  destroy GTK dialog if there

   zd->sentinel1 = zd->sentinel2 = 0;                                                  //  mark sentinels invalid
   zfree(zd->title);                                                                   //  free title memory
   zfree(zd->widget[0].data);

   for (ii = 1; zd->widget[ii].type; ii++)                                             //  loop through widgets
   {
      zfree((ch *) zd->widget[ii].type);                                               //  free strings
      zfree((ch *) zd->widget[ii].wname);
      if (zd->widget[ii].pname) zfree((ch *) zd->widget[ii].pname);                    //  parent widget name
      if (zd->widget[ii].data) zfree(zd->widget[ii].data);                             //  free data
      if (zd->widget[ii].zlist) zlist_free(zd->widget[ii].zlist);                      //  free combo box zlist
   }

   for (ii = 0; ii < zdialog_count; ii++)                                              //  remove from valid zdialog list
      if (zd == zdialog_list[ii]) break;
   if (ii < zdialog_count) {
      zdialog_count--;
      for (NOP; ii < zdialog_count; ii++)                                              //  pack down list
         zdialog_list[ii] = zdialog_list[ii+1];
   }
   else printf("*** zdialog_free(), not in zdialog_list \n");

   zfree(zd);                                                                          //  free zdialog memory
   zd = 0;                                                                             //  caller pointer = null
   return 1;
}


//  Wait for a dialog to complete or be destroyed. This is a zmainloop() loop.
//  The returned status is the button 1-N used to complete the dialog, or negative
//  if the dialog was destroyed with [x] or otherwise by GTK. If the status was 1-N and
//  the dialog will be kept active, set zd->zstat = 0 to restore the active state.

int zdialog_wait(zdialog *zd)
{
   if (! main_thread()) zappcrash("illegal call from thread");

   zdialog_present(zd);                                                                //  initially has focus

   while (true)
   {
      zmainsleep(0.01);
      if (! zd) return -1;
      if (! zdialog_valid2(zd)) return -1;
      if (zd->zstat) return zd->zstat;
   }
}


//  put cursor at named widget

int zdialog_goto(zdialog *zd, ch *wname)
{
   GtkWidget   *widget;

   if (! main_thread()) zappcrash("illegal call from thread");

   if (! zdialog_valid(zd)) return 0;

   widget = zdialog_gtkwidget(zd, wname);
   if (! widget) return 0;
   gtk_widget_grab_focus(widget);

   return 1;
}


//  set cursor for zdialog (e.g. a busy cursor)

void zdialog_set_cursor(zdialog *zd, GdkCursor *cursor)
{
   GtkWidget   *dialog;
   GdkWindow   *window;

   if (! main_thread()) zappcrash("illegal call from thread");

   if (! zdialog_valid(zd)) return;
   dialog = zd->widget[0].widget;
   if (! dialog) return;
   window = gtk_widget_get_window(dialog);
   gdk_window_set_cursor(window,cursor);
   return;
}


//  insert data into a zdialog widget

int zdialog_stuff(zdialog *zd, ch *wname, ch *data)                                    //  stuff a string
{
   if (! main_thread()) zappcrash("illegal call from thread");
   if (data) zdialog_put_data(zd,wname,data);
   else zdialog_put_data(zd,wname,"");                                                 //  null > ""
   return 1;
}

int zdialog_stuff(zdialog *zd, ch *wname, int idata)                                   //  stuff an integer
{
   ch       string[16];
   double   min, max;

   if (! main_thread()) zappcrash("illegal call from thread");

   if (zdialog_get_limits(zd,wname,min,max))
      if (idata < min || idata > max) return 0;                                        //  bad data, do nothing
   snprintf(string,16,"%d",idata);
   zdialog_put_data(zd,wname,string);
   return 1;
}

int zdialog_stuff(zdialog *zd, ch *wname, double ddata)                                //  stuff a double
{
   ch       string[32];
   double   min, max;

   if (! main_thread()) zappcrash("illegal call from thread");

   if (zdialog_get_limits(zd,wname,min,max))
      if (ddata < min || ddata > max) return 0;                                        //  bad data, do nothing
   snprintf(string,32,"%.7g",ddata);                                                   //  increase from 6 to 7 digits
   zdialog_put_data(zd,wname,string);                                                  //  'g' uses decimal or comma
   return 1;                                                                           //      (per locale)
}

int zdialog_stuff(zdialog *zd, ch *wname, double ddata, ch *format)                    //  stuff a double, formatted
{
   ch       string[32];
   double   min, max;

   if (! main_thread()) zappcrash("illegal call from thread");

   if (zdialog_get_limits(zd,wname,min,max))
      if (ddata < min || ddata > max) return 0;                                        //  bad data, do nothing
   snprintf(string,32,format,ddata);                                                   //  use "%.2g" etc. for
   zdialog_put_data(zd,wname,string);                                                  //    locale dependent point/comma
   return 1;
}

int zdialog_labelfont(zdialog *zd, ch *labl, ch *font, ch *txt)                        //  stuff label text using specified font
{
   GtkWidget   *widget;
   ch          *format =  "<span font=\"%s\" >%s</span>";
   ch          txt2[1000];

   if (! main_thread()) zappcrash("illegal call from thread");

   if (! font) font = zfuncs::appfont;                                                 //  default font
   snprintf(txt2,1000,format,font,txt);
   widget = zdialog_gtkwidget(zd,labl);
   gtk_label_set_markup(GTK_LABEL(widget),txt2);
   return 1;
}


//  get data from a zdialog widget

int zdialog_fetch(zdialog *zd, ch *wname, ch *data, int maxcc)                         //  fetch string data
{
   ch  *zdata;

   if (! main_thread()) zappcrash("illegal call from thread");

   zdata = zdialog_get_data(zd,wname);
   if (! zdata) {
      *data = 0;
      return 0;
   }

   return strncpy0(data,zdata,maxcc);                                                  //  0 = OK, 1 = truncation
}

int zdialog_fetch(zdialog *zd, ch *wname, int &idata)                                  //  fetch an integer
{
   ch  *zdata;

   if (! main_thread()) zappcrash("illegal call from thread");

   zdata = zdialog_get_data(zd,wname);
   if (! zdata) {
      idata = 0;
      return 0;
   }

   idata = atoi(zdata);
   return 1;
}

int zdialog_fetch(zdialog *zd, ch *wname, double &ddata)                               //  fetch a double
{
   int         stat;
   ch          *zdata;

   if (! main_thread()) zappcrash("illegal call from thread");

   zdata = zdialog_get_data(zd,wname);
   if (! zdata) {
      ddata = 0;
      return 0;
   }

   stat = convSD(zdata,ddata);                                                         //  period or comma decimal point OK
   if (stat < 4) return 1;
   return 0;
}

int zdialog_fetch(zdialog *zd, ch *wname, float &fdata)                                //  fetch a float
{
   int         stat;
   ch          *zdata;
   double      ddata;

   if (! main_thread()) zappcrash("illegal call from thread");

   zdata = zdialog_get_data(zd,wname);
   if (! zdata) {
      fdata = 0;
      return 0;
   }

   stat = convSD(zdata,ddata);                                                         //  period or comma decimal point OK
   fdata = ddata;
   if (stat < 4) return 1;
   return 0;
}


//  clear combo box entries

int zdialog_combo_clear(zdialog *zd, ch *wname)
{
   int         ii;

   if (! main_thread()) zappcrash("illegal call from thread");

   ii = zdialog_find_widget(zd,wname);
   if (! ii) return 0;
   gtk_combo_box_text_remove_all(GTK_COMBO_BOX_TEXT(zd->widget[ii].widget));           //  remove all entries
   if (zd->widget[ii].zlist) zlist_clear(zd->widget[ii].zlist,0);
   return 1;
}


//  popup (open) combo box pick list

int zdialog_combo_popup(zdialog *zd, ch *wname)
{
   int      ii;

   if (! main_thread()) zappcrash("illegal call from thread");

   ii = zdialog_find_widget(zd,wname);
   if (! ii) return 0;
   gtk_combo_box_popup(GTK_COMBO_BOX(zd->widget[ii].widget));
   return 1;
}


/**************************************************************************************/

//  Load/save all function widget data from/to a file.
//  dirname for data files: /home/<user>/.appname/funcname
//    where zdialog data is saved for the respective function.
//  return 0 = OK, +N = error

int zdialog_load_widgets(zdialog *zd, spldat *sd, ch *funcname, FILE *fid)
{
   using namespace zfuncs;

   ch       *mess = "Load settings from file";
   int      myfid = 0;
   ch       *filename, dirname[200], buff[1000];
   ch       *wname, *wdata, wdata2[1000];
   ch       *pp, *pp1, *pp2;
   int      ii, kk, err, cc1, cc2;

   if (! main_thread()) zappcrash("illegal call from thread");

   if (! fid)                                                                          //  fid from script
   {
      snprintf(dirname,200,"%s/%s",get_zhomedir(),funcname);                           //  folder for data files
      filename = zgetfile(mess,GTK_WINDOW(zd->dialog),"file",dirname,0);               //  open data file
      if (! filename) return 1;                                                        //  user cancel
      fid = fopen(filename,"r");
      if (! fid) {
         zmessageACK(zd->dialog,"%s \n %s",filename,strerror(errno));
         return 1;
      }
      zfree(filename);
      myfid = 1;
   }

   for (ii = 0; ii < zdmaxwidgets; ii++)                                               //  read widget data recs
   {
      pp = fgets_trim(buff,1000,fid,1);
      if (! pp) break;
      if (strmatch(pp,"curves")) {
         if (! sd) goto baddata;
         err = splcurve_load(sd,fid);                                                  //  load curves data
         if (err) goto baddata;
         continue;
      }
      if (strmatch(pp,"end")) break;
      pp1 = pp;
      pp2 = strstr(pp1," ==");
      if (! pp2) continue;                                                             //  widget has no data
      cc1 = pp2 - pp1;
      if (cc1 > 100) continue;
      pp1[cc1] = 0;
      wname = pp1;                                                                     //  widget name
      if (strstr("defcats deftags",wname)) continue;                                   //  fotocx only kludge
      pp2 += 3;
      if (*pp2 == ' ') pp2++;
      wdata = pp2;                                                                     //  widget data
      cc2 = strlen(wdata);
      if (cc2 < 1) wdata = "";
      if (cc2 > 1000) continue;
      repl_1str(wdata,wdata2,1000,"\\n","\n");                                         //  replace "\n" with newline chars.
      kk = zdialog_put_data(zd,wname,wdata2);
      if (! kk) goto baddata;
   }

   if (myfid) fclose(fid);
   return 0;

baddata:
   zmessageACK(zd->dialog,"file data does not fit dialog");
   if (myfid) fclose(fid);
   return 1;
}


int zdialog_save_widgets(zdialog *zd, spldat *sd, ch *funcname, FILE *fid)
{
   using namespace zfuncs;

   ch       *mess = "Save settings to a file";
   int      myfid = 0;
   ch       *filename, dirname[200];
   ch       *wtype, *wname, *wdata, wdata2[1000];
   int      ii, cc;

   ch       *editwidgets = "entry zentry edit text togbutt check combo"                //  widget types to save
                           "radio spin zspin hscale hscale2 vscale colorbutt";
   ch       *editwidgetsX = "defcats deftags";                                         //  exclude list, fotocx kludge

   if (! main_thread()) zappcrash("illegal call from thread");

   if (! fid)                                                                          //  fid from script
   {
      snprintf(dirname,200,"%s/%s",get_zhomedir(),funcname);                           //  folder for data files
      filename = zgetfile(mess,GTK_WINDOW(zd->dialog),"save",dirname,0);               //  open data file
      if (! filename) return 1;                                                        //  user cancel
      fid = fopen(filename,"w");
      if (! fid) {
         zmessageACK(zd->dialog,"%s \n %s",filename,strerror(errno));
         return 1;
      }
      myfid = 1;
      zfree(filename);
   }

   for (ii = 0; ii < zdmaxwidgets; ii++)
   {
      wtype = (ch *) zd->widget[ii].type;
      if (! wtype) break;
      if (! strstr(editwidgets,wtype)) continue;
      wname = (ch *) zd->widget[ii].wname;                                             //  write widget data recs:
      if (strstr(editwidgetsX,wname)) continue;
      wdata = zd->widget[ii].data;                                                     //  widgetname == widgetdata
      if (! wdata) continue;
      cc = strlen(wdata);
      if (cc > 900) continue;
      repl_1str(wdata,wdata2,1000,"\n","\\n");                                         //  replace newline with "\n"
      fprintf(fid,"%s == %s \n",wname,wdata);
   }

   if (sd) {
      fprintf(fid,"curves\n");
      splcurve_save(sd,fid);
   }

   fprintf(fid,"end\n");

   if (myfid) fclose(fid);
   return 0;
}


//  functions to support [prev] buttons in function dialogs
//  load or save last-used widgets

int zdialog_load_prev_widgets(zdialog *zd, spldat *sd, ch *funcname)
{
   using namespace zfuncs;

   ch       filename[200];
   FILE     *fid;
   int      err;

   if (! main_thread()) zappcrash("illegal call from thread");

   snprintf(filename,200,"%s/%s/last-used",get_zhomedir(),funcname);
   fid = fopen(filename,"r");
   if (! fid) {
      zmessageACK(zd->dialog,"%s \n %s",filename,strerror(errno));
      return 1;
   }

   err = zdialog_load_widgets(zd,sd,funcname,fid);
   fclose(fid);
   return err;
}


int zdialog_save_last_widgets(zdialog *zd, spldat *sd, ch *funcname)
{
   using namespace zfuncs;

   ch       filename[200], dirname[200];
   FILE     *fid;
   int      err;

   if (! main_thread()) zappcrash("illegal call from thread");

   snprintf(filename,200,"%s/%s/last-used",get_zhomedir(),funcname);
   fid = fopen(filename,"w");
   if (! fid) {
      snprintf(dirname,200,"%s/%s",get_zhomedir(),funcname);                           //  create missing folder
      err = mkdir(dirname,0750);
      if (err) {
         printf("*** %s \n %s \n",dirname,strerror(errno));
         return 1;
      }
      fid = fopen(filename,"w");                                                       //  open again
   }

   if (! fid) {
      printf("*** %s \n %s \n",filename,strerror(errno));
      return 1;
   }

   err = zdialog_save_widgets(zd,sd,funcname,fid);
   fclose(fid);
   return err;
}


/**************************************************************************************/

//  functions to save and recall zdialog window positions

namespace zdposn_names
{
   struct zdposn_t {
      ch       wintitle[64];                                                           //  window title (ID)
      float    xpos, ypos;                                                             //  window posn WRT parent or desktop, 0-100
      int      xsize, ysize;                                                           //  window size, pixels
   }  zdposn[200];                                                                     //  space to remember 200 windows

   int      Nzdposn;                                                                   //  no. in use
   int      Nzdpmax = 200;                                                             //  table size
}


//  Load zdialog positions table from its file (application startup)
//  or save zdialog positions table to its file (application exit).
//  Action is "load" or "save". Number of table entries is returned.

int zdialog_geometry(ch *action)
{
   using namespace zdposn_names;

   ch       posfile[200], buff[100], wintitle[64], *pp;
   float    xpos, ypos;
   int      xsize, ysize;
   int      ii, nn, cc;
   FILE     *fid;

   if (! main_thread()) zappcrash("illegal call from thread");

   snprintf(posfile,199,"%s/zdialog_geometry",zhomedir);                               //  /home/<user>/.appname/zdialog_geometry

   if (strmatch(action,"load"))                                                        //  load dialog positions table from file
   {
      fid = fopen(posfile,"r");
      if (! fid) {
         Nzdposn = 0;
         return 0;
      }

      for (ii = 0; ii < Nzdpmax; )
      {
         pp = fgets(buff,100,fid);
         if (! pp) break;
         pp = strstr(buff,"||");
         if (! pp) continue;
         cc = pp - buff;
         strncpy0(wintitle,buff,cc);
         strTrim(wintitle);
         if (strlen(wintitle) < 3) continue;
         nn = sscanf(pp+2," %f %f %d %d ",&xpos,&ypos,&xsize,&ysize);
         if (nn != 4) continue;

         strcpy(zdposn[ii].wintitle,wintitle);
         zdposn[ii].xpos = xpos;
         zdposn[ii].ypos = ypos;
         zdposn[ii].xsize = xsize;
         zdposn[ii].ysize = ysize;
         ii++;
      }

      fclose(fid);
      Nzdposn = ii;
      return Nzdposn;
   }

   if (strmatch(action,"save"))                                                        //  save dialog positions table to file
   {
      fid = fopen(posfile,"w");
      if (! fid) {
         printf("*** cannot write zdialog_geometry file \n");
         return 0;
      }

      for (ii = 0; ii < Nzdposn; ii++) {
         fprintf(fid,"%s || %0.1f %0.1f %d %d \n",                                     //  dialog-title || xpos ypos xsize ysize
                           zdposn[ii].wintitle,
                           zdposn[ii].xpos, zdposn[ii].ypos,
                           zdposn[ii].xsize, zdposn[ii].ysize);
      }

      fclose(fid);
      return Nzdposn;
   }

   printf("*** zdialog_geometry bad action: %s \n",action);
   return 0;
}


//  Set the initial or new zdialog window position from "posn".
//  Called by zdialog_run().  Private function.
//     null:      window manager decides
//    "mouse"     put dialog at mouse position
//    "desktop"   center dialog in desktop window
//    "parent"    center dialog in parent window
//    "save"      use the same position last set by the user
//    "nn/nn"     put NW corner of dialog in parent window at % size
//                (e.g. "50/50" puts NW corner at center of parent)

void zdialog_set_position(zdialog *zd, ch *posn)
{
   using namespace zdposn_names;

   int         ii, ppx, ppy, zdpx, zdpy, pww, phh;
   float       xpos, ypos;
   int         xsize, ysize;
   ch          wintitle[64], *pp;
   GtkWidget   *parent, *dialog;

   if (! main_thread()) zappcrash("illegal call from thread");

   if (! zdialog_valid(zd)) return;

   parent = zd->parent;
   dialog = zd->widget[0].widget;

   if (strmatch(posn,"mouse")) {
      window_to_mouse(zd->dialog);
      return;
   }

   if (strmatch(posn,"desktop")) {
      gtk_window_set_position(GTK_WINDOW(dialog),GTK_WIN_POS_CENTER);
      return;
   }

   if (strmatch(posn,"parent")) {
      gtk_window_set_position(GTK_WINDOW(dialog),GTK_WIN_POS_CENTER_ON_PARENT);
      return;
   }

   if (! parent) parent = zd->dialog;

   if (! parent) {                                                                     //  no parent window
      ppx = ppy = 0;                                                                   //  use desktop
      pww = monitor_ww;
      phh = monitor_hh;
   }
   else {
      gtk_window_get_position(GTK_WINDOW(parent),&ppx,&ppy);                           //  parent window NW corner
      gtk_window_get_size(GTK_WINDOW(parent),&pww,&phh);                               //  parent window size
   }

   if (strmatch(posn,"save"))                                                          //  use last saved window position
   {
      zd->saveposn = 1;                                                                //  set flag for zdialog_free()

      pp = (ch *) gtk_window_get_title(GTK_WINDOW(dialog));                            //  get window title, used as ID
      if (! pp || strlen(pp) < 2) goto center_parent;

      strncpy0(wintitle,pp,64);                                                        //  window title, < 64 chars.

      for (ii = 0; ii < Nzdposn; ii++)                                                 //  search table for title
         if (strmatch(wintitle,zdposn[ii].wintitle)) break;
      if (ii == Nzdposn) goto center_parent;                                           //  not found - zdialog_destroy() will add

      zdpx = ppx + 0.01 * zdposn[ii].xpos * pww;                                       //  set position for dialog window
      zdpy = ppy + 0.01 * zdposn[ii].ypos * phh;
      xsize = zdposn[ii].xsize;                                                        //  set size
      ysize = zdposn[ii].ysize;
      gtk_window_move(GTK_WINDOW(dialog),zdpx,zdpy);
      gtk_window_resize(GTK_WINDOW(dialog),xsize,ysize);
      return;
   }

   else     //  "nn/nn"                                                                //  position from caller
   {
      ii = sscanf(posn,"%f/%f",&xpos,&ypos);                                           //  parse "nn/nn"
      if (ii != 2) goto center_parent;

      zdpx = ppx + 0.01 * xpos * pww;                                                  //  position for dialog window
      zdpy = ppy + 0.01 * ypos * phh;
      gtk_window_move(GTK_WINDOW(dialog),zdpx,zdpy);
      return;
   }

center_parent:                                                                         //  center on parent window
   gtk_window_set_position(GTK_WINDOW(dialog),GTK_WIN_POS_CENTER_ON_PARENT);
   return;
}


//  If the dialog window position is "save" then save
//  its position WRT parent or desktop for next use.
//  called by zdialog_destroy().  Private function.

void zdialog_save_position(zdialog *zd)
{
   using namespace zdposn_names;

   int         ii, ppx, ppy, pww, phh, zdpx, zdpy;
   float       xpos, ypos;
   int         xsize, ysize;
   ch          wintitle[64], *pp;
   GtkWidget   *parent, *dialog;

   if (! main_thread()) zappcrash("illegal call from thread");

   if (! zdialog_valid(zd)) return;

   dialog = zd->widget[0].widget;
   if (! dialog) return;
   if (! gtk_widget_get_window(dialog)) return;

   gtk_window_get_position(GTK_WINDOW(dialog),&zdpx,&zdpy);                            //  dialog window NW corner
   if (! zdpx && ! zdpy) return;                                                       //  (0,0) ignore

   gtk_window_get_size(GTK_WINDOW(dialog),&xsize,&ysize);                              //  window size

   parent = zd->parent;                                                                //  parent window

   if (! parent) parent = zd->dialog;

   if (! parent) {                                                                     //  no parent window
      ppx = ppy = 0;                                                                   //  use desktop
      pww = monitor_ww;
      phh = monitor_hh;
   }
   else {
      gtk_window_get_position(GTK_WINDOW(parent),&ppx,&ppy);                           //  parent window NW corner
      gtk_window_get_size(GTK_WINDOW(parent),&pww,&phh);                               //  parent window size
   }

   xpos = 100.0 * (zdpx - ppx) / pww;                                                  //  dialog window relative position
   ypos = 100.0 * (zdpy - ppy) / phh;                                                  //  (as percent of parent size)

   pp = (ch *) gtk_window_get_title(GTK_WINDOW(dialog));
   if (! pp) return;
   if (strlen(pp) < 2) return;
   strncpy0(wintitle,pp,64);                                                           //  window title, < 64 chars.

   for (ii = 0; ii < Nzdposn; ii++)                                                    //  search table for window
      if (strmatch(wintitle,zdposn[ii].wintitle)) break;
   if (ii == Nzdposn) {                                                                //  not found
      if (ii == Nzdpmax) return;                                                       //  table full
      Nzdposn++;                                                                       //  new entry
   }

   strcpy(zdposn[ii].wintitle,wintitle);                                               //  add window to table
   zdposn[ii].xpos = xpos;                                                             //  save window position
   zdposn[ii].ypos = ypos;
   zdposn[ii].xsize = xsize;                                                           //  and window size
   zdposn[ii].ysize = ysize;
   return;
}


/**************************************************************************************/

//  Functions to save and restore zdialog user inputs
//    within an app session or across app sessions.

namespace zdinputs_names
{
   #define  Nwmax zdmaxwidgets                                                         //  max. widgets in a dialog
   #define  Nzdmax 200                                                                 //  max. zdialogs
   #define  ccmax1 100                                                                 //  max. widget name length
   #define  ccmax2 400                                                                 //  max. widget data length

   struct zdinputs_t {
      ch       *zdtitle = 0;                                                           //  zdialog title
      int      Nw;                                                                     //  no. of widgets
      ch       **wname;                                                                //  list of widget names
      ch       **wdata;                                                                //  list of widget data
   }  zdinputs[Nzdmax];                                                                //  space for Nzdmax dialogs

   int      Nzd = 0;                                                                   //  no. zdialogs in use
}


//  Load zdialog input fields from its file (app startup)
//  or save zdialog input fields to its file (app shutdown).
//  Action is "load" or "save".
//  Number of zdialogs is returned.

int zdialog_inputs(ch *action)                                                         //  sequence numbers added                25.1
{
   using namespace zdinputs_names;

   ch       zdinputsfile[200], buff[ccmax2];
   ch       zdtitle[ccmax1], wname[Nwmax][ccmax1], wdata[Nwmax][ccmax2];
   ch       *pp, *pp1, *pp2, wdata2[ccmax2+50];
   FILE     *fid;
   int      Nw, ii, jj, cc, cc1, cc2;
   int      seq;

   if (! main_thread()) zappcrash("illegal call from thread");

   snprintf(zdinputsfile,200,"%s/zdialog_inputs",zhomedir);                            //  /home/<user>/.appname/zdialog_inputs

   if (strmatch(action,"load"))                                                        //  load dialog input fields from its file
   {
      Nzd = seq = 0;

      fid = fopen(zdinputsfile,"r");                                                   //  no file
      if (! fid) return 0;

      while (true)
      {
         pp = fgets_trim(buff,ccmax2,fid,1);                                           //  read next zdialog title record
         if (! pp || seq++ != atoi(pp)) goto abend;
         pp += 4;

         if (strstr(pp,"EOF")) break;                                                  //  normal EOF, OK

         if (! strmatchN(pp,"zdialog == ",11)) continue;                               //  expect zdialog title record

         strncpy0(zdtitle,pp+11,ccmax1);                                               //  save new zdialog title

         pp = fgets_trim(buff,ccmax2,fid,1);                                           //  read next zdialog title record
         if (! pp || seq++ != atoi(pp)) goto abend;
         pp += 4;

         Nw = atoi(pp);                                                                //  dialog widget count
         if (Nw < 1 || Nw > Nwmax) goto abend;

         for (ii = 0; ii < Nw; ii++)                                                   //  read widget data recs
         {
            pp = fgets_trim(buff,ccmax2,fid,1);                                        //  read next zdialog title record
            if (! pp || seq++ != atoi(pp)) goto abend;
            pp += 4;

            pp1 = pp;
            pp2 = strstr(pp1," ==");
            if (! pp2) break;                                                          //  widget has no data
            cc1 = pp2 - pp1;
            pp1[cc1] = 0;
            pp2 += 3;
            if (*pp2 == ' ') pp2++;
            cc2 = strlen(pp2);
            if (cc1 < 1 || cc1 >= ccmax1) break;
            if (cc2 < 1) pp2 = "";
            if (cc2 >= ccmax2) break;                                                  //  do not copy large inputs
            strcpy(wname[ii],pp1);                                                     //  save widget name and data
            strcpy(wdata2,pp2);
            repl_1str(wdata2,wdata[ii],ccmax2,"\\n","\n");                             //  replace "\n" with newline chars.
         }

         if (ii < Nw) goto abend;

         if (Nzd == Nzdmax) {
            printf("*** zdialog_inputs() overflow \n");
            break;
         }

         zdinputs[Nzd].zdtitle = zstrdup(zdtitle,"zdialog_inputs");                    //  save acculumated zdialog data
         zdinputs[Nzd].Nw = Nw;
         cc = Nw * sizeof(ch *);
         zdinputs[Nzd].wname = (ch **) zmalloc(cc,"zdialog_inputs");
         zdinputs[Nzd].wdata = (ch **) zmalloc(cc,"zdialog_inputs");
         for (ii = 0; ii < Nw; ii++) {
            zdinputs[Nzd].wname[ii] = zstrdup(wname[ii],"zdialog_inputs");
            zdinputs[Nzd].wdata[ii] = zstrdup(wdata[ii],"zdialog_inputs");
         }

         Nzd++;
      }

      fclose(fid);
      return Nzd;

   abend:
      printf("*** zdialog_inputs file corrupted - deleted \n");                        //  25.1
      fclose(fid);
      remove(zdinputsfile);
      Nzd = 0;
      return 0;
   }

   if (strmatch(action,"save"))                                                        //  save dialog input fields to its file
   {
      fid = fopen(zdinputsfile,"w");
      if (! fid) {
         printf("*** zdialog_inputs() cannot write file \n");
         return 0;
      }

      seq = 0;

      for (ii = 0; ii < Nzd; ii++)
      {
         fprintf(fid,"%03d zdialog == %s \n",seq++,zdinputs[ii].zdtitle);              //  zdialog == zdialog title
         Nw = zdinputs[ii].Nw;
         fprintf(fid,"%03d %d \n",seq++,Nw);                                           //  widget count
         for (jj = 0; jj < Nw; jj++) {
            pp1 = zdinputs[ii].wname[jj];                                              //  widget name == widget data
            pp2 = zdinputs[ii].wdata[jj];
            repl_1str(pp2,wdata2,ccmax2+50,"\n","\\n");                                //  replace newline chars. with "\n"
            fprintf(fid,"%03d %s == %s \n",seq++,pp1,wdata2);
         }
         fprintf(fid,"%03d \n",seq++);
      }

      fprintf(fid,"%03d EOF",seq);
      fclose(fid);
      return Nzd;
   }

   printf("*** zdialog_inputs bad action: %s \n",action);
   return 0;
}


//  Save dialog user input fields when a dialog is finished.
//  Called automatically by zdialog_free(). Private function.

int zdialog_save_inputs(zdialog *zd)
{
   using namespace zdinputs_names;

   ch       zdtitle[ccmax1], wname[ccmax1], wdata[ccmax2];
   ch       *wnamex, *type;
   int      ii, jj, Nw, cc;

   ch       *skipwidgets = "dialog hbox vbox hsep vsep frame scrwin"                   //  non-input widgets to omit
                           "label link button zbutton";
   ch       *skipexceptions = "searchtags";                                            //  fotocx kludge

   if (! main_thread()) zappcrash("illegal call from thread");

   if (! zdialog_valid(zd)) return 0;
   if (! zd->saveinputs) return 0;                                                     //  zdialog does not use this service

   strncpy0(zdtitle,zd->widget[0].data,ccmax1);                                        //  zdialog title is widget[0].data

   for (ii = 0; ii < Nzd; ii++)                                                        //  find zdialog in zdinputs table
      if (strmatch(zdtitle,zdinputs[ii].zdtitle)) break;

   if (ii < Nzd) {                                                                     //  found
      zfree(zdinputs[ii].zdtitle);                                                     //  delete obsolete zdinputs data
      for (jj = 0; jj < zdinputs[ii].Nw; jj++) {
         zfree(zdinputs[ii].wname[jj]);
         zfree(zdinputs[ii].wdata[jj]);
      }
      zfree(zdinputs[ii].wname);
      zfree(zdinputs[ii].wdata);
      Nzd--;                                                                           //  decr. zdialog count
      for (NOP; ii < Nzd; ii++)                                                        //  pack down the rest
         zdinputs[ii] = zdinputs[ii+1];
   }

   if (Nzd == Nzdmax) {
      printf("*** zdialog_save_inputs, too many zdialogs \n");
      return 0;
   }

   ii = Nzd;                                                                           //  next zdinputs table entry

   for (Nw = 0, jj = 1; zd->widget[jj].type; jj++) {                                   //  count zdialog widgets
      wnamex = (ch *) zd->widget[jj].wname;
      type = (ch *) zd->widget[jj].type;
      if (strstr(skipwidgets,type))                                                    //  skip non-input widgets
         if (! strstr(skipexceptions,wnamex)) continue;
      Nw++;
   }

   if (! Nw) return 0;                                                                 //  no input widgets
   if (Nw > Nwmax) {
      printf("*** zdialog_save_inputs() bad data: %s \n",zdtitle);
      return 0;
   }

   zdinputs[ii].zdtitle = zstrdup(zdtitle,"zdialog_save");                             //  set zdialog title

   cc = Nw * sizeof(ch *);                                                             //  allocate pointers for widgets
   zdinputs[ii].wname = (ch **) zmalloc(cc,"zdialog_save");
   zdinputs[ii].wdata = (ch **) zmalloc(cc,"zdialog_save");

   for (Nw = 0, jj = 1; zd->widget[jj].type; jj++) {                                   //  add widget names and data
      wnamex = (ch *) zd->widget[jj].wname;
      type = (ch *) zd->widget[jj].type;
      if (strstr(skipwidgets,type))                                                    //  skip non-input widgets
         if (! strstr(skipexceptions,wnamex)) continue;
      strncpy0(wname,zd->widget[jj].wname,ccmax1);
      if (zd->widget[jj].data)
         strncpy0(wdata,zd->widget[jj].data,ccmax2);
      else strcpy(wdata,"");
      zdinputs[ii].wname[Nw] = zstrdup(wname,"zdialog_save");
      zdinputs[ii].wdata[Nw] = zstrdup(wdata,"zdialog_save");
      Nw++;
   }

   zdinputs[ii].Nw = Nw;                                                               //  set widget count
   Nzd++;                                                                              //  add zdialog to end of zdinputs

   return 1;
}


//  Restore user input fields from prior use of the same dialog.
//  Call this if wanted after zdialog is built and before it is run.
//  Override old user inputs with zdialog_stuff() where needed.

int zdialog_load_inputs(zdialog *zd)
{
   using namespace zdinputs_names;

   ch       *zdtitle, *wname, *wdata;
   int      ii, jj;

   zd->saveinputs = 1;                                                                 //  flag, save data at zdialog_free()

   if (! main_thread()) zappcrash("illegal call from thread");

   zdtitle = (ch *) zd->widget[0].data;                                                //  zdialog title

   for (ii = 0; ii < Nzd; ii++)                                                        //  find zdialog in zdinputs
      if (strmatch(zdtitle,zdinputs[ii].zdtitle)) break;
   if (ii == Nzd) return 0;                                                            //  not found

   for (jj = 0; jj < zdinputs[ii].Nw; jj++) {                                          //  stuff all saved widget data
      wname = zdinputs[ii].wname[jj];
      wdata = zdinputs[ii].wdata[jj];
      zdialog_put_data(zd,wname,wdata);
   }

   return 1;
}


/**************************************************************************************/

//  get text input from a popup dialog - multiple lines can be entered
//  returned text is subject for zfree()
//  null is returned if user presses [cancel] button.

ch * zdialog_text(GtkWidget *parent, ch *title, ch *inittext)
{
   zdialog     *zd;
   int         zstat;
   ch          *text;

   if (! main_thread()) zappcrash("illegal call from thread");

   zd = zdialog_new(title,parent,"OK","X",null);
   zdialog_add_widget(zd,"frame","fred","dialog");
   zdialog_add_widget(zd,"zedit","text","fred");
   if (inittext) zdialog_stuff(zd,"text",inittext);

   zdialog_resize(zd,300,0);
   zdialog_set_modal(zd);
   zdialog_run(zd,0,"mouse");
   zstat = zdialog_wait(zd);
   if (zstat == 1)
      text = (ch *) zdialog_get_data(zd,"text");
   else text = 0;
   if (text) text = zstrdup(text,"zdialog_text");
   zdialog_free(zd);
   return text;
}


/**************************************************************************************/

//  get text input from a popup dialog - one line only
//  returned text is subject for zfree()
//  null is returned if user presses [cancel] button.

ch * zdialog_text1(GtkWidget *parent, ch *title, ch *inittext, int cc)
{
   zdialog     *zd;
   int         zstat;
   ch          *text;
   ch          size[20];
   
   if (! main_thread()) zappcrash("illegal call from thread");

   zd = zdialog_new(title,parent,"OK","X",null);
   zdialog_add_widget(zd,"hbox","hbtext","dialog",0,"space=5");
   snprintf(size,20,"size=%d",cc);
   zdialog_add_widget(zd,"entry","text","hbtext",0,size);
   if (inittext) zdialog_stuff(zd,"text",inittext);

   zdialog_resize(zd,300,0);
   zdialog_set_modal(zd);
   zdialog_run(zd,0,"mouse");
   zstat = zdialog_wait(zd);
   if (zstat == 1)
      text = (ch *) zdialog_get_data(zd,"text");
   else text = 0;
   if (text) text = zstrdup(text,"zdialog_text");
   zdialog_free(zd);
   return text;
}


/**************************************************************************************/

//  get password input from a popup dialog - one line only
//  user input is hidden as '*' characters.
//  returned text is subject for zfree()
//  null is returned if user presses [cancel] button.

ch * zdialog_password(GtkWidget *parent, ch *title, ch *inittext)
{
   int zdialog_password_event(zdialog *zd, ch *event);

   zdialog     *zd;
   int         zstat;
   ch          *text;
   GtkWidget   *widget;

   if (! main_thread()) zappcrash("illegal call from thread");

   zd = zdialog_new(title,parent,"OK","X",null);
   zdialog_add_widget(zd,"entry","text","dialog",0);
   widget = zdialog_gtkwidget(zd,"text");
   gtk_entry_set_visibility(GTK_ENTRY(widget),0);
   if (inittext) zdialog_stuff(zd,"text",inittext);

   zdialog_resize(zd,300,0);
   zdialog_set_modal(zd);
   zdialog_run(zd,zdialog_password_event,"mouse");
   zstat = zdialog_wait(zd);
   if (zstat == 1) text = (ch *) zdialog_get_data(zd,"text");
   else text = 0;
   if (text) text = zstrdup(text,"zdialog_password");
   zdialog_free(zd);
   return text;
}


int zdialog_password_event(zdialog *zd, ch *event)
{
   if (strmatch(event,"activate")) zd->zstat = 1;
   return 1;
}


/**************************************************************************************/

//  Display a dialog with 1-5 radio buttons with text labels .
//  Button 1 is the default and is pre-selected.
//  Returns choice 1-5 corresponding to button selected.
//  Returns -1 if cancel button [x] or completion button [X] is selected.
//
//  nn = zdialog_choose(parent, where, message, text1, text2, ... null)
//  'where' is    null:       window manager decides dialog placement
//                "mouse"     put dialog at mouse position
//                "desktop"   center dialog in desktop window
//                "parent"    center dialog in parent window

namespace zdialog_choose_names
{
   int      button;                                                                    //  button pushed 1-5
}

int zdialog_choose(GtkWidget *parent, ch *where, ch *message, ...)                     //  improved         26.0
{
   using namespace zdialog_choose_names;

   int zdialog_choose_event(zdialog *zd, ch *event);

   zdialog     *zd;
   va_list     arglist;
   int         ii, Ntext;
   ch          *text[5];
   ch          butt[4] = "B0";

   if (! main_thread()) zappcrash("illegal call from thread");

   button = 0;                                                                         //  no button pushed

   va_start(arglist,message);

   for (ii = 0; ii < 5; ii++)
   {
      text[ii] = va_arg(arglist,ch *);
      if (! text[ii]) break;
   }

   Ntext = ii;
   if (! Ntext) zappcrash("zdialog_choose(), no buttons");

   zd = zdialog_new("choose",parent,null);
   zdialog_add_widget(zd,"label","labmess","dialog",message,"space=5");
   zdialog_add_widget(zd,"hbox","hbb","dialog",0,"space=5");

   for (ii = 0; ii < Ntext; ii++) {
      butt[1] = '1' + ii;                                                              //  button names: "B1" "B2" etc.
      zdialog_add_widget(zd,"zbutton",butt,"hbb",text[ii],"space=2");
   }

   zdialog_set_decorated(zd,0);
   zdialog_set_modal(zd);
   zdialog_run(zd,zdialog_choose_event,where);
   zdialog_wait(zd);
   zdialog_free(zd);
   printf("dialog choose: %s \n",text[button-1]);
   return button;
}


//  dialog event and completion function

int zdialog_choose_event(zdialog *zd, ch *event)
{
   using namespace zdialog_choose_names;

   if (! main_thread()) zappcrash("illegal call from thread");
   if (strstr("B1 B2 B3 B4 B5",event)) button = event[1] - '0';                        //  set button pushed 1-5
   zd->zstat = 0;
   if (button) zd->zstat = 1;
   return 1;
}


/**************************************************************************************/

//  popup zdialog to edit a text file
//  returns 0 if OK or user cancel, +N if error

int zdialog_edit_textfile(GtkWidget *parent, char *file)
{
   zdialog  *zd;
   int      cc, err, zstat, maxcc = 1000000;                                           //  max. file size
   char     *pp, *title, buff[1000001];
   FILE     *fid;

   if (! main_thread()) zappcrash("illegal call from thread");

   snprintf(buff,maxcc,"du -b %s",file);                                               //  command to get file size

   fid = popen(buff,"r");                                                              //  run 'du' command
   if (! fid) goto filerr;

   pp = fgets(buff,100,fid);                                                           //  read command output
   pclose(fid);
   if (! pp) goto filerr;

   cc = atoi(pp);                                                                      //  file size
   if (cc < 0 || cc > maxcc) {                                                         //  reject file > 1 MB
      printf("*** file size > %d: %s \n",maxcc,file);
      return 1;
   }

   fid = fopen(file,"r");                                                              //  open file
   if (! fid) goto filerr;

   for (cc = 0; ; ) {
      pp = fgets(buff+cc,maxcc-cc,fid);                                                //  read entire file
      if (! pp) break;
      cc += strlen(pp);
   }

   fclose(fid);

   title = strrchr(file,'/');                                                          //  get root file name
   if (title) title++;
   else title = file;

   zd = zdialog_new(title,parent,TX("Apply"),"X",null);                                //  create text edit dialog
   zdialog_add_widget(zd,"scrwin","scroll","dialog",0,"expand");
   zdialog_add_widget(zd,"zedit","text","scroll",0,"expand");

   zdialog_stuff(zd,"text",buff);                                                      //  stuff file text into dialog

   zdialog_resize(zd,500,400);
   zdialog_run(zd,0,"parent");                                                         //  run dialog

   zstat = zdialog_wait(zd);                                                           //  wait for completion
   if (zstat != 1) {
      zdialog_free(zd);                                                                //  user cancel
      return 0;
   }

   zdialog_fetch(zd,"text",buff,maxcc);                                                //  get edited text
   zdialog_free(zd);

   cc = strlen(buff);
   if (cc > maxcc) {
      printf("*** edited file size > %d: %s \n",maxcc,file);
      return 1;
   }

   fid = fopen(file,"w");
   if (! fid) goto filerr;

   err = fputs(buff,fid);
   if (err < 0 || err == EOF) goto filerr;

   err = fclose(fid);
   if (err) goto filerr;

   return 0;

filerr:
   printf("*** file error: %s %s \n",strerror(errno),file);
   return 1;
}


/**************************************************************************************

   popup window with scrolling text report
   line numbers and line positions are zero based

   open the report window with given title and pixel dimensions
   Fheader    add optional non-scrolling header at top of report window
   CBfunc     optional callback function:
                 void CBfunc(GtkWidget *, int line, int posn, ch *input)
    ...       optional dialog completion buttons terminated with null:
                 [OK] [X] [Find] [Save] [Esc] are processed here
                 others are passed to callback function ( --> input)
   zdialog->zstat = 1/2 for buttons [OK] / [X]

***/

zdialog * popup_report_open(ch *title, GtkWidget *parent, int ww, int hh,
                    int wrap, int Fheader, txwidget_CBfunc_t CBfunc, ...)
{
   int popup_report_dialog_event(zdialog *zd, ch *event);

   va_list     arglist;
   ch          *butn[6];                                                               //  up to 5 buttons + null
   int         ii, NB;
   zdialog     *zd;
   GtkWidget   *mHead, *mText;

   if (! main_thread()) zappcrash("illegal call from thread");

   va_start(arglist,CBfunc);                                                           //  get button args, if any

   for (ii = 0; ii < 5; ii++) {                                                        //  up to 5 buttons                       25.1
      butn[ii] = va_arg(arglist,ch *);
      if (! butn[ii]) break;
   }

   NB = ii;                                                                            //  no. buttons

   zd = zdialog_new(title,parent,null);

   if (Fheader) {                                                                      //  non-scrolling header
      zdialog_add_widget(zd,"text","header","dialog");
      zdialog_add_widget(zd,"hsep","hsep","dialog");
   }

   zdialog_add_widget(zd,"scrwin","scroll","dialog",0,"expand");                       //  scrolling text window for report

   if (wrap) zdialog_add_widget(zd,"report","text","scroll",0,"expand|wrap");          //  text > report
   else zdialog_add_widget(zd,"report","text","scroll",0,"expand");

   if (NB) {                                                                           //  optional event buttons
      zdialog_add_widget(zd,"hbox","hbbutn","dialog");
      zdialog_add_widget(zd,"label","space","hbbutn",0,"expand");
      for (ii = 0; ii < NB; ii++)
         zdialog_add_widget(zd,"button",butn[ii],"hbbutn",butn[ii],"space=5");
   }

   zdialog_resize(zd,ww,hh);                                                           //  show report dialog box
   zdialog_run(zd,popup_report_dialog_event);                                          //  keep window size and position

   if (Fheader) {
      mHead = zdialog_gtkwidget(zd,"header");                                          //  header initially invisible
      gtk_widget_set_visible(mHead,0);
   }

   mText = zdialog_gtkwidget(zd,"text");                                               //  report text not editable
   gtk_widget_grab_focus(mText);

   if (CBfunc) {                                                                       //  25.1
      txwidget_set_eventfunc(mText,CBfunc);                                            //  set mouse/KB event function
      zd->popup_report_CB = (void *) CBfunc;
   }

   return zd;
}


//  dialog event and completion function  [OK] [ X ] [Find] [Save] [Esc]

int popup_report_dialog_event(zdialog *zd, ch *event)
{
   txwidget_CBfunc_t  *CBfunc;

   GtkWidget    *mText;
   static ch    findtext[40] = "";
   int          linem, line1, line2;
   zdialog      *zdf;

   if (! main_thread()) zappcrash("illegal call from thread");

   if (! zdialog_valid(zd)) return 1;                                                  //  report cancelled

   if (strmatch(event,"focus")) return 0;

   if (zd->zstat) {                                                                    //  [x] cancel or escape, kill dialog
      zdialog_free(zd);
      return 1;
   }

   if (strstr(" OK  X  escape",event)) {                                               //  kill report window                    25.0
      zdialog_free(zd);
      return 1;
   }

   if (strmatch(event,"Find")) {                                                       //  [Find]
      zdf = zdialog_new("find text",zd->dialog,"Find","X",null);                       //  popup dialog to enter text
      zdialog_add_widget(zdf,"zentry","text","dialog",findtext,"size=20");
      zdialog_run(zdf,0,"mouse");
      linem = -1;                                                                      //  no match line yet
      while (true)
      {
         zdialog_wait(zdf);
         if (zdf->zstat != 1) {                                                        //  [cancel]
            zdialog_free(zdf);
            return 1;
         }
         zdf->zstat = 0;
         zdialog_fetch(zdf,"text",findtext,40);                                        //  get text
         popup_report_get_visible_lines(zd,line1,line2);                               //  lines now visible
         if (linem < 0) linem = line1;                                                 //  search from 1st visible line
         linem = popup_report_find(zd,findtext,linem);                                 //  search for text
         if (linem < 0) continue;                                                      //  not found
         popup_report_scroll_top(zd,linem);                                            //  found, scroll to top
         linem++;                                                                      //  next search from line
      }
   }

   if (strmatch(event,"Save")) {                                                       //  [Save]   save text to file
      mText = zdialog_gtkwidget(zd,"text");
      txwidget_save(mText,GTK_WINDOW(zd->parent));
      return 1;
   }

   mText = zdialog_gtkwidget(zd,"text");

   CBfunc = (txwidget_CBfunc_t *) zd->popup_report_CB;                                 //  other event
   if (CBfunc) CBfunc(mText,-1,-1,event);                                              //  pass to callback function

   return 1;
}


//  write a non-scrolling header line

void popup_report_header(zdialog *zd, int bold, ch *format, ...)
{
   va_list     arglist;
   ch          message[1000];
   GtkWidget   *mHead;

   if (! main_thread()) zappcrash("illegal call from thread");

   if (! zdialog_valid(zd)) { printf("report cancelled \n"); return; }

   va_start(arglist,format);
   vsnprintf(message,999,format,arglist);
   va_end(arglist);

   mHead = zdialog_gtkwidget(zd,"header");
   txwidget_append(mHead,bold,message);
   gtk_widget_set_visible(mHead,1);

   return;
}


//  write a new text line at the end

void popup_report_write(zdialog *zd, int bold, ch *format, ...)
{
   va_list     arglist;
   ch          message[20000];
   GtkWidget   *mText;

   if (! main_thread()) zappcrash("illegal call from thread");

   if (! zdialog_valid(zd)) { printf("report cancelled \n"); return; }

   va_start(arglist,format);
   vsnprintf(message,19999,format,arglist);
   va_end(arglist);

   mText = zdialog_gtkwidget(zd,"text");
   txwidget_append(mText,bold,"%s",message);
   return;
}


//  write a new text line at the end, scroll down to end

void popup_report_write2(zdialog *zd, int bold, ch *format, ...)
{
   va_list     arglist;
   ch          message[20000];
   GtkWidget   *mText;

   if (! main_thread()) zappcrash("illegal call from thread");

   if (! zdialog_valid(zd)) { printf("report cancelled \n"); return; }

   va_start(arglist,format);
   vsnprintf(message,19999,format,arglist);
   va_end(arglist);

   mText = zdialog_gtkwidget(zd,"text");
   txwidget_append2(mText,bold,"%s",message);
   return;
}


//  scroll window back to top line

void popup_report_top(zdialog *zd)
{
   if (! main_thread()) zappcrash("illegal call from thread");
   GtkWidget *mText = zdialog_gtkwidget(zd,"text");
   txwidget_scroll(mText,0);
   return;
}


//  scroll window back to bottom line

void popup_report_bottom(zdialog *zd)
{
   if (! main_thread()) zappcrash("illegal call from thread");
   GtkWidget *mText = zdialog_gtkwidget(zd,"text");
   txwidget_scroll(mText,-1);
   return;
}


//  clear the report window

void popup_report_clear(zdialog *zd)
{
   if (! main_thread()) zappcrash("illegal call from thread");
   GtkWidget *mText = zdialog_gtkwidget(zd,"text");
   txwidget_clear(mText);
   return;
}


//  clear the report window from line to end

void popup_report_clear(zdialog *zd, int line)
{
   if (! main_thread()) zappcrash("illegal call from thread");
   GtkWidget *mText = zdialog_gtkwidget(zd,"text");
   txwidget_clear(mText,line);
   return;
}


//  insert a new line after a given line

void popup_report_insert(zdialog *zd, int bold, int line, ch *format, ...)
{
   va_list     arglist;
   ch          message[20000];
   GtkWidget   *mText;

   if (! main_thread()) zappcrash("illegal call from thread");

   va_start(arglist,format);
   vsnprintf(message,19999,format,arglist);
   va_end(arglist);

   if (! zdialog_valid(zd)) { printf("report cancelled \n"); return; }

   mText = zdialog_gtkwidget(zd,"text");
   txwidget_insert(mText,bold,line,message);
   return;
}


//  replace a given line

void popup_report_replace(zdialog *zd, int bold, int line, ch *format, ...)
{
   va_list     arglist;
   ch          message[20000];
   GtkWidget   *mText;

   if (! main_thread()) zappcrash("illegal call from thread");

   va_start(arglist,format);
   vsnprintf(message,19999,format,arglist);
   va_end(arglist);

   if (! zdialog_valid(zd)) { printf("report cancelled \n"); return; }

   mText = zdialog_gtkwidget(zd,"text");
   txwidget_replace(mText,bold,line,message);
   return;
}


//  delete a given line

void popup_report_delete(zdialog *zd, int line)
{
   if (! main_thread()) zappcrash("illegal call from thread");
   GtkWidget *mText = zdialog_gtkwidget(zd,"text");
   txwidget_delete(mText,line);
   return;
}


//  find first line of text containing characters matching input string
//  search is from line1 to end, then from 0 to line1-1
//  returns first matching line or -1 if none
//  comparison is not case sensitive


int  popup_report_find(zdialog *zd, ch *matchtext, int line1)
{
   if (! main_thread()) zappcrash("illegal call from thread");
   if (! zdialog_valid(zd)) { printf("report cancelled \n"); return 1; }
   GtkWidget *mText = zdialog_gtkwidget(zd,"text");
   return txwidget_find(mText,matchtext,line1,1);                                      //  highlight line
}


//  insert a pixbuf image after a given line

void popup_report_insert_pixbuf(zdialog *zd, int line, GdkPixbuf *pixbuf)
{
   if (! main_thread()) zappcrash("illegal call from thread");
   if (! zdialog_valid(zd)) { printf("report cancelled \n"); return; }
   GtkWidget *mText = zdialog_gtkwidget(zd,"text");
   txwidget_insert_pixbuf(mText,line,pixbuf);
   return;
}


//  scroll to bring a given line into the report window

void popup_report_scroll(zdialog *zd, int line)
{
   if (! main_thread()) zappcrash("illegal call from thread");
   if (! zdialog_valid(zd)) { printf("report cancelled \n"); return; }
   GtkWidget *mText = zdialog_gtkwidget(zd,"text");
   txwidget_scroll(mText,line);
   return;
}


//  scroll to bring a given line to the top of the report window

void popup_report_scroll_top(zdialog *zd, int line)
{
   if (! main_thread()) zappcrash("illegal call from thread");
   if (! zdialog_valid(zd)) { printf("report cancelled \n"); return; }
   GtkWidget *mText = zdialog_gtkwidget(zd,"text");
   txwidget_scroll_top(mText,line);
   return;
}


//  get the range of visible lines in the report window

void popup_report_get_visible_lines(zdialog *zd, int &vtop, int &vbott)
{
   if (! main_thread()) zappcrash("illegal call from thread");
   if (! zdialog_valid(zd)) { printf("report cancelled \n"); return; }
   GtkWidget *mText = zdialog_gtkwidget(zd,"text");
   txwidget_get_visible_lines(mText,vtop,vbott);
   return;
}


//  retrieve a given line and optionally strip the trailing \n

ch * popup_report_line(zdialog *zd, int line, int strip)
{
   if (! main_thread()) zappcrash("illegal call from thread");
   if (! zdialog_valid(zd)) { printf("report cancelled \n"); return 0; }
   GtkWidget *mText = zdialog_gtkwidget(zd,"text");
   return txwidget_line(mText,line,strip);
}


//  retrieve the word starting at a given position in a given line

ch * popup_report_word(zdialog *zd, int line, int posn, ch *dlims, ch &end)
{
   if (! main_thread()) zappcrash("illegal call from thread");
   GtkWidget *mText = zdialog_gtkwidget(zd,"text");
   return txwidget_word(mText,line,posn,dlims,end);
}


//  highlight a given line of text

void popup_report_highlight_line(zdialog *zd, int line)
{
   if (! main_thread()) zappcrash("illegal call from thread");
   if (! zdialog_valid(zd)) { printf("report cancelled \n"); return; }
   GtkWidget *mText = zdialog_gtkwidget(zd,"text");
   txwidget_highlight_line(mText,line);
   return;
}


//  highlight the text at a given position and length in a given line

void popup_report_highlight_word(zdialog *zd, int line, int posn, int cc)
{
   if (! main_thread()) zappcrash("illegal call from thread");
   if (! zdialog_valid(zd)) { printf("report cancelled \n"); return; }
   GtkWidget *mText = zdialog_gtkwidget(zd,"text");
   txwidget_highlight_word(mText,line,posn,cc);
   return;
}


//  underline the text at a given position and length in a given line

void popup_report_underline_word(zdialog *zd, int line, int posn, int cc)
{
   if (! main_thread()) zappcrash("illegal call from thread");
   if (! zdialog_valid(zd)) { printf("report cancelled \n"); return; }
   GtkWidget *mText = zdialog_gtkwidget(zd,"text");
   txwidget_underline_word(mText,line,posn,cc);
   return;
}


//  bold the text at a given position and length in a given line

void popup_report_bold_word(zdialog *zd, int line, int posn, int cc)
{
   if (! main_thread()) zappcrash("illegal call from thread");
   if (! zdialog_valid(zd)) { printf("report cancelled \n"); return; }
   GtkWidget *mText = zdialog_gtkwidget(zd,"text");
   txwidget_bold_word(mText,line,posn,cc);
   return;
}


//  set font attributes for entire report                                              //  temp. kludge

void popup_report_font_attributes(zdialog *zd)
{
   if (! main_thread()) zappcrash("illegal call from thread");
   if (! zdialog_valid(zd)) return;
   GtkWidget *mText = zdialog_gtkwidget(zd,"text");
   txwidget_font_attributes(mText);
   return;
}


//  close report after given seconds (OK to leave it open until user closes)
//  also connected to window destroy signal (secs = 0)

void popup_report_close(zdialog *zd, int secs)
{
   void popup_report_timeout(zdialog *zd);

   if (! main_thread()) zappcrash("illegal call from thread");

   if (! zdialog_valid(zd)) return;

   if (secs < 1) {
      zdialog_free(zd);
      return;
   }

   g_timeout_add_seconds(secs,(GSourceFunc) popup_report_timeout,zd);
   return;
}


//  private function for report timeout

void popup_report_timeout(zdialog *zd)
{
   if (! main_thread()) zappcrash("illegal call from thread");
   if (! zdialog_valid(zd)) return;
   zdialog_free(zd);
   return;
}


/**************************************************************************************/

//  execute a shell command and show the output in a scrolling popup window
//  returns: 0 = EOF  1 = command failure

int popup_command(ch *command, int ww, int hh, GtkWidget *parent, int top)
{
   FILE        *fid;
   ch          buff[1000], *pp;
   zdialog     *zd;

   if (! main_thread()) zappcrash("illegal call from thread");

   printf("run command: %s \n",command);

   zd = popup_report_open(command,parent,ww,hh,0,0,0,"Find","Save","X",null);

   fid = popen(command,"r");
   if (! fid) return 1;
   while (true) {
      pp = fgets_trim(buff,1000,fid);
      if (! pp) break;
      popup_report_write2(zd,0,"%s\n",pp);
   }
   pclose(fid);

   if (top) popup_report_top(zd);                                                      //  back to top of window
   return 0;
}


/**************************************************************************************/

//  show a text file in a popup window
//  show appended records in real-time until window is closed
//  return: 0 = OK
//          1 = not a text file
//          2 = cannot open popup report window
//          3 = cannot open file

int monitor_file(ch *file)                                                             //  25.1
{
   zdialog  *zd;
   FILE     *fid;
   ch       buff[500];
   ch       *title, *pp;

   if (! main_thread()) zappcrash("illegal call from thread");

   printf("monitor file: %s \n",file);

   snprintf(buff,500,"file \"%s\" ",file);                                             //  check for text file
   fid = popen(buff,"r");
   pp = fgets(buff,500,fid);
   pclose(fid);
   if (! pp || ! strstr(pp,"text")) return 1;

   title = strrchr(file,'/') + 1;
   zd = popup_report_open(title,0,600,400,1,0,0,"Find","Save","X",null);
   if (! zd) return 2;

   snprintf(buff,500,"tail -f \"%s\" ",file);
   fid = popen(buff,"r");
   if (! fid) return 3;

   while (true)
   {
      zmainsleep(0.01);
      if (! zdialog_valid(zd)) break;                                                  //  popup window closed, exit
      pp = fgets_pend(buff,500,fid);                                                   //  read next log record
      if (! pp) break;                                                                 //  EOF or error, exit
      if (strmatch(pp,"fgets_pend\n")) continue;                                       //  next record not available yet
      popup_report_write2(zd,0,"%s",pp);                                               //  output record to window
   }

   pclose(fid);
   exit(0);
}


/**************************************************************************************/

//  convert a dialog message into a bold/red font

ch * boldred(ch *message)                                                              //  25.1
{
   static ch   boldredmess[2000];

   snprintf(boldredmess,2000,"<span font=\"bold\" color=\"red\">%s</span>",message);
   return boldredmess;
}


/**************************************************************************************/

//  Display popup message box and wait for user acknowledgement.
//  Messages are presented sequentially from main() and from threads.

namespace zmessageACK_names
{
   int         Finit = 0;
   ch          buffer[2000];
   GtkWidget   *Pwindow;
}

void zmessageACK(GtkWidget *parent, ch *format, ... )                                  //  rewrite for threads                   25.1
{
   using namespace zmessageACK_names;

   int zmessageACK_timerfunc(void *);

   va_list        arglist;
   ch             message[2000];

   if (! Finit) {                                                                      //  initz. message processor
      Finit = 1;
      *buffer = 0;
      g_timeout_add(100,zmessageACK_timerfunc,0);
   }

   va_start(arglist,format);                                                           //  format the message
   vsnprintf(message,2000,format,arglist);
   va_end(arglist);

   printf("%s \n",message);                                                            //  output to log file

   while (*buffer) zsleep(0.1);                                                        //  wait for parallel caller
   strncpy0(buffer,boldred(message),2000);                                             //  bold/red                              25.1
   Pwindow = parent;
   while (* buffer) zmainsleep(0.1);                                                   //  wait for this caller response
   return;
}


int zmessageACK_timerfunc(void *)                                                      //  loop every 0.1 sec.
{
   using namespace zmessageACK_names;

   zdialog  *zd;
   ch       *posn;

   if (! *buffer) return 1;
   if (Fshutdown) return 0;

   if (Pwindow) posn = "parent";
   else posn = "desktop";

   zd = zdialog_new("ACK",Pwindow,"OK",null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","lab1","hb1",buffer,"space=5");
   zdialog_resize(zd,200,0);
   zdialog_set_modal(zd);
   zdialog_set_decorated(zd,0);
   zdialog_run(zd,0,posn);
   zdialog_wait(zd);
   zdialog_free(zd);
   *buffer = 0;
   return 1;
}


/**************************************************************************************/

//  display message box and wait for user Yes or No response
//  returns 1 or 0

int zmessageYN(GtkWidget *parent, ch *format, ... )
{
   va_list        arglist;
   ch             message[500];
   ch             *posn;
   zdialog        *zd;
   int            zstat;

   if (! main_thread()) zappcrash("illegal call from thread");

   va_start(arglist,format);
   vsnprintf(message,500,format,arglist);
   va_end(arglist);

   printf("%s \n",message);                                                            //  output to log file

   if (parent) posn = "parent";
   else posn = "desktop";

   zd = zdialog_new("YN",parent,TX("Yes"),TX("No"),null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","lab1","hb1",boldred(message),"space=5");
   zdialog_resize(zd,200,0);
   zdialog_set_modal(zd);
   zdialog_set_decorated(zd,0);
   zdialog_run(zd,0,posn);
   zstat = zdialog_wait(zd);
   zdialog_free(zd);
   zmainloop();
   if (zstat == 1) return 1;
   return 0;
}


/**************************************************************************************/

//  display message until timeout (can be forever) or user cancel
//  or caller kills it with zdialog_free()
//  posn - from zdialog_run():
//    "mouse" = position at mouse
//    "desktop" = center on desktop
//    "parent" = center on parent window
//    "nn/nn" = position NW corner at relative x/y position in parent window,
//              where nn/nn is a percent 0-100 of the parent window dimensions.
//  seconds: time to keep message on screen, 0 = forever until cancelled

typedef struct {
   zdialog     *zd;
   int         uniqueID;
}  zdx_t;


zdialog * zmessage_post(GtkWidget *parent, ch *posn, int seconds, ch *format, ... )
{
   int  zmessage_post_timeout(zdx_t *zdx);

   va_list           arglist;
   ch                message[400];
   static zdx_t      zdx[100];
   static int        ii = 0;
   zdialog           *zd;

   if (! main_thread()) zappcrash("illegal call from thread");

   va_start(arglist,format);
   vsnprintf(message,400,format,arglist);
   va_end(arglist);

   printf("%s \n",message);                                                            //  output to log file

   zd = zdialog_new("post",parent,null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","lab1","hb1",boldred(message),"space=5");
   zdialog_run(zd,0,posn);                                                             //  mouse position
   zdialog_set_decorated(zd,0);
   zdialog_present(zd);

   if (seconds) {
      if (ii < 99) ii++;                                                               //  track unique zdialogs
      else ii = 0;
      zdx[ii].zd = zd;
      zdx[ii].uniqueID = zd->uniqueID;
      g_timeout_add_seconds(seconds,(GSourceFunc) zmessage_post_timeout,&zdx[ii]);
   }

   return zd;
}


int zmessage_post_timeout(zdx_t *zdx)
{
   zdialog  *zd = zdx->zd;                                                             //  check unique zdialog active
   if (! zdialog_valid2(zd,"post")) return 0;
   if (zd->uniqueID != zdx->uniqueID) return 0;
   zdialog_free(zd);
   return 0;
}


/**************************************************************************************/

//  functions to show popup text windows

namespace poptext {
   ch          *ptext = 0;
   GtkWidget   *popwin = 0;
   ch          *pcurrent = 0;
   #define GSFNORMAL GTK_STATE_FLAG_NORMAL
}


//  timer function to show popup window after a given time

int poptext_show(ch *current)
{
   using namespace poptext;

   if (! main_thread()) zappcrash("illegal call from thread");
   if (current != pcurrent) return 0;
   if (popwin) gtk_widget_show_all(popwin);
   return 0;
}


//  timer function to kill popup window after a given time

int poptext_timeout(ch *current)
{
   using namespace poptext;

   if (! main_thread()) zappcrash("illegal call from thread");
   if (current != pcurrent) return 0;
   poptext_killnow();                                                                  //  25.0
   return 0;
}


//  Show a popup text message at a given absolute screen position.
//  Any prior popup will be killed and replaced.
//  If text == null, kill without replacement.
//  secs1 is time to delay before showing the popup.
//  secs2 is time to kill the popup after it is shown (0 = never).
//  This function returns immediately.

void poptext_screen(ch *text, int px, int py, float secs1, float secs2)
{
   using namespace poptext;

   GtkWidget   *label;
   int         millisec1, millisec2;

   if (! main_thread()) zappcrash("illegal call from thread");

   poptext_killnow();

   pcurrent++;                                                                         //  make current != pcurrent

   if (! text) return;

   popwin = gtk_window_new(GTK_WINDOW_POPUP);
   label = gtk_label_new(text);
   gtk_container_set_border_width(GTK_CONTAINER(popwin),5);                            //  25.0
   gtk_container_add(GTK_CONTAINER(popwin),label);
   gtk_window_move(GTK_WINDOW(popwin),px,py);

   if (secs1 > 0) {                                                                    //  delayed popup display
      millisec1 = secs1 * 1000;
      g_timeout_add(millisec1,(GSourceFunc) poptext_show,pcurrent);
   }
   else gtk_widget_show_all(popwin);                                                   //  immediate display

   if (secs2 > 0) {                                                                    //  popup kill timer
      millisec2 = (secs1 + secs2) * 1000;
      g_timeout_add(millisec2,(GSourceFunc) poptext_timeout,pcurrent);
   }

   return;
}


//  Show a popup text message at current mouse position + offsets.

void poptext_mouse(ch *text, int dx, int dy, float secs1, float secs2)
{
   using namespace poptext;

   int         mx, my;
   ch          *ptext2;
   GtkWidget   *popwin2;

   if (! main_thread()) zappcrash("illegal call from thread");

   if (! text) {
      poptext_killnow();
      return;
   }

   popwin2 = popwin;
   ptext2 = ptext;
   popwin = 0;
   ptext = 0;

   gdk_device_get_position(zfuncs::mouse,0,&mx,&my);                                   //  mouse screen position
   poptext_screen(text,mx+dx,my+dy,secs1,secs2);                                       //  add displacements

   zmainloop();

   if (popwin2) gtk_widget_destroy(popwin2);                                           //  kill prior after create new
   if (ptext2) zfree(ptext2);                                                          //  (prevent flicker)

   return;
}


//  Show a popup text message at the given widget position.

void poptext_widget(GtkWidget *widget, ch *text, int dx, int dy, float secs1, float secs2)
{
   GdkWindow   *win;
   int         px, py;

   if (! main_thread()) zappcrash("illegal call from thread");

   if (! text) {
      poptext_killnow();
      return;
   }
   win = gtk_widget_get_window(widget);
   gdk_window_get_origin(win,&px,&py);
   poptext_screen(text,px+dx,py+dy,secs1,secs2);
   return;
}


//  kill popup window unconditionally

int poptext_killnow()
{
   using namespace poptext;

   if (popwin) gtk_widget_destroy(popwin);
   if (ptext) zfree(ptext);
   popwin = 0;
   ptext = 0;
   return 0;
}


/**************************************************************************************

    File chooser dialog for one or more files

    Action:  "file"            select an existing file
             "files"           select multiple existing files
             "save"            select an existing or new file
             "folder"          select existing folder
             "folders"         select multiple existing folders
             "create folder"   select existing or new folder

    hidden   if > 0, add button to toggle display of hidden files
             optional, default = 0

    Returns a list of filespecs terminated with null.
    Memory for returned list and returned files are subjects for zfree();

*********************************************************************************/

//  version for 1 file only: file, save, folder, create folder
//  returns one filespec or null
//  returned file is subject for zfree()

ch * zgetfile(ch *title, GtkWindow *parent, ch *action, ch *initfile, int hidden)
{
   if (! main_thread()) zappcrash("illegal call from thread");

   if (! strmatchV(action,"file","save","folder","create folder",null))
      zappcrash("zgetfile() call error: %s",action);

   ch   **flist = zgetfiles(title,parent,action,initfile,hidden);
   if (! flist) return 0;
   ch   *file = *flist;
   zfree(flist);
   return file;
}


//  version for 2 or more files
//  returns a list of filespecs (ch **) terminated with null
//  returns null if canceled by user
//  returned list members and returned list are subjects for zfree()

ch ** zgetfiles(ch *title, GtkWindow *parent, ch *action, ch *initfile, int hidden)
{
   void zgetfile_preview(GtkWidget *dialog, GtkWidget *pvwidget);                      //  private functions
   int  zgetfile_KBkey(GtkWidget *dialog, GdkEventKey *event, int &fcdes);
   void zgetfile_newfolder(GtkFileChooser *dialog, void *);

   GtkFileChooserAction fcact = GTK_FILE_CHOOSER_ACTION_OPEN;

   GtkWidget   *dialog;
   PIXBUF      *thumbnail;
   GtkWidget   *pvwidget = gtk_image_new();
   GSList      *gslist = 0;
   ch          *button1 = 0, *buttxx = 0;
   ch          *pdir, *pfile;
   int         ii, err, NF, setfname = 0;
   int         fcstat, bcode = 0, hide = 1;
   int         fcdes = 0;
   ch          *file1, *file2, **flist = 0;
   STATB       statB;

   if (! main_thread()) zappcrash("illegal call from thread");

   if (strmatch(action,"file")) {
      fcact = GTK_FILE_CHOOSER_ACTION_OPEN;
      button1 = TX("choose file");
   }

   else if (strmatch(action,"files")) {
      fcact = GTK_FILE_CHOOSER_ACTION_OPEN;
      button1 = TX("choose files");
   }

   else if (strmatch(action,"save")) {
      fcact = GTK_FILE_CHOOSER_ACTION_SAVE;
      button1 = TX("Save");
      setfname = 1;
   }

   else if (strmatch(action,"folder")) {
      fcact = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
      button1 = TX("choose folder");
   }

   else if (strmatch(action,"folders")) {
      fcact = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
      button1 = TX("choose folders");
   }

   else if (strmatch(action,"create folder")) {
      fcact = GTK_FILE_CHOOSER_ACTION_CREATE_FOLDER;
      button1 = TX("create folder");
      setfname = 1;
   }

   else zappcrash("zgetfiles() call error: %s",action);

   if (hidden) {
      buttxx = "hidden";
      bcode = 103;
   }

   dialog = gtk_file_chooser_dialog_new(title, parent, fcact,                          //  create file selection dialog
                              button1, GTK_RESPONSE_ACCEPT,                            //  parent added
                              "X", GTK_RESPONSE_CANCEL,
                              buttxx, bcode, null);

   gtk_file_chooser_set_preview_widget(GTK_FILE_CHOOSER(dialog),pvwidget);

   G_SIGNAL(dialog,"update-preview",zgetfile_preview,pvwidget);                        //  create preview for selected file
   G_SIGNAL(dialog,"key-press-event",zgetfile_KBkey,&fcdes);                           //  respond to special KB keys

   gtk_window_set_position(GTK_WINDOW(dialog),GTK_WIN_POS_MOUSE);                      //  put dialog at mouse position
   gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(dialog),0);                       //  default: no show hidden

   if (strmatch(action,"save"))                                                        //  overwrite confirmation
      gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog),1);

   if (strmatch(action,"files") || strmatch(action,"folders"))
      gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog),1);                //  select multiple files or folders

   if (initfile) {                                                                     //  pre-select filespec
      err = stat(initfile,&statB);
      if (err) {
         pdir = zstrdup(initfile,"zgetfiles");                                         //  non-existent file
         pfile = strrchr(pdir,'/');
         if (pfile && pfile > pdir) {
            *pfile++ = 0;                                                              //  set folder name
            gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),pdir);
         }
         if (setfname) {                                                               //  set new file name
            if (! pfile) pfile = (ch *) initfile;
            gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog),pfile);
         }
         zfree(pdir);
      }
      else if (S_ISREG(statB.st_mode))                                                 //  select given file
         gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dialog),initfile);
      else if (S_ISDIR(statB.st_mode))                                                 //  select given folder
         gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),initfile);
   }

   if (initfile) {
      thumbnail = get_thumbnail(initfile,256);                                         //  preview for initial file
      if (thumbnail) {
         gtk_image_set_from_pixbuf(GTK_IMAGE(pvwidget),thumbnail);
         gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(dialog),1);
         g_object_unref(thumbnail);
      }
      else gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(dialog),0);
   }

   gtk_widget_show_all(dialog);

   while (true)
   {
      fcstat = gtk_dialog_run(GTK_DIALOG(dialog));                                     //  run dialog, get status button

      if (fcstat == 103) {                                                             //  show/hide hidden files
         hide = 1 - hide;
         gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(dialog),hide);
         continue;
      }

      else if (fcstat == GTK_RESPONSE_ACCEPT)
      {
         gslist = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog));
         if (! gslist) continue;

         NF = g_slist_length(gslist);                                                  //  no. selected files
         flist = (ch **) zmalloc((NF+1)*sizeof(ch *),"zgetfiles");                     //  allocate returned list

         for (ii = 0; ii < NF; ii++)
         {                                                                             //  process selected files
            file1 = (ch *) g_slist_nth_data(gslist,ii);
            file2 = zstrdup(file1,"zgetfiles");                                        //  re-allocate memory
            flist[ii] = file2;
            g_free(file1);
         }
         flist[ii] = 0;                                                                //  EOL marker
         break;
      }

      else break;                                                                      //  user bailout
   }

   if (gslist) g_slist_free(gslist);                                                   //  return selected file(s)
   if (! fcdes) gtk_widget_destroy(dialog);                                            //  destroy if not already
   return flist;
}


//  zgetfile private function - get preview images for image files

void zgetfile_preview(GtkWidget *dialog, GtkWidget *pvwidget)
{
   PIXBUF      *thumbnail;
   ch          *filename;

   filename = gtk_file_chooser_get_preview_filename(GTK_FILE_CHOOSER(dialog));

   if (! filename) {
      gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(dialog),0);
      return;
   }

   thumbnail = get_thumbnail(filename,256);                                            //  256x256 pixels
   g_free(filename);

   if (thumbnail) {
      gtk_image_set_from_pixbuf(GTK_IMAGE(pvwidget),thumbnail);
      gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(dialog),1);
      g_object_unref(thumbnail);
   }
   else gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(dialog),0);

   return;
}


//  zgetfile private function - KB functions

int zgetfile_KBkey(GtkWidget *dialog, GdkEventKey *event, int &fcdes)
{
   int KBkey = event->keyval;

   if (KBkey == GDK_KEY_F1) {                                                          //  F1 = help
      KBevent(event);
      return 1;
   }

   if (KBkey == GDK_KEY_Escape) {                                                      //  escape = cancel
      gtk_widget_destroy(dialog);
      fcdes = 1;
      return 1;
   }

   return 0;
}


/**************************************************************************************/

//  select a folder (or create a new folder)
//  returns location (pathname) of selected or created folder.
//  returned location is subject for zfree().

ch * zgetfolder(ch *title, GtkWindow *parent, ch *initfolder)
{
   GtkWidget      *dialog;
   GtkFileChooser *chooser;
   int      nn;
   ch       *pp1, *pp2 = null;

   if (! main_thread()) zappcrash("illegal call from thread");

   dialog = gtk_file_chooser_dialog_new(title, parent,
                  GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
                  "X", GTK_RESPONSE_CANCEL,
                  "Open", GTK_RESPONSE_ACCEPT, NULL);

   chooser = GTK_FILE_CHOOSER(dialog);
   gtk_file_chooser_set_filename(chooser, initfolder);

   nn = gtk_dialog_run(GTK_DIALOG(dialog));
   if (nn != GTK_RESPONSE_ACCEPT) {
      gtk_widget_destroy(dialog);
      return null;
   }

   pp1 = gtk_file_chooser_get_filename(chooser);
   if (pp1) {
      pp2 = zstrdup(pp1,"zgetfolder");
      g_free(pp1);
   }

   gtk_widget_destroy(dialog);
   return pp2;
}


/**************************************************************************************

   print_image_file(GtkWidget *parent, ch *imagefile)

   Print an image file using the printer, paper, orientation,
   margins, and scale set by the user.

   HPLIP problem: Setting paper size was made less flexible.
   GtkPrintSettings paper size must agree with the one in the current
   printer setup. This can only be set in the printer setup dialog, not
   in the application. Also the print size (width, height) comes from
   the chosen paper size and cannot be changed in the application.
   Print margins can be changed to effect printing a smaller or shifted
   image on a larger paper size.

*********************************************************************************/

namespace print_image
{
   #define MM GTK_UNIT_MM
   #define INCH GTK_UNIT_INCH
   #define PRINTOP   GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG
   #define PORTRAIT  GTK_PAGE_ORIENTATION_PORTRAIT
   #define LANDSCAPE GTK_PAGE_ORIENTATION_LANDSCAPE
   #define QUALITY   GTK_PRINT_QUALITY_HIGH

   GtkWidget                  *parent = 0;
   GtkPageSetup               *priorpagesetup = 0;
   GtkPageSetup               *pagesetup;
   GtkPrintSettings           *printsettings = 0;
   GtkPrintOperation          *printop;
   GtkPageOrientation         orientation = PORTRAIT;
   PIXBUF                     *pixbuf;
   ch                         *printer = 0;
   int                        landscape = 0;                                           //  true if landscape
   double                     width = 21.0, height = 29.7;                             //  paper size, CM (default A4 portrait)
   double                     margins[4] = { 0.5, 0.5, 0.5, 0.5 };                     //  margins, CM (default 0.5)
   double                     imagescale = 100;                                        //  image print scale, percent
   double                     pwidth, pheight;                                         //  printed image size

   int   page_setup();
   int   margins_setup();
   int   margins_dialog_event(zdialog *zd, ch *event);
   void  get_printed_image_size();
   void  print_page(GtkPrintOperation *, GtkPrintContext *, int page);
}


//  user callable function to set paper, margins, scale, and then print

void print_image_file(GtkWidget *pwin, ch *imagefile)
{
   using namespace print_image;

   GtkPrintOperationResult  printstat;
   GError   *gerror = 0;
   int      err;

   if (! main_thread()) zappcrash("illegal call from thread");

   parent = pwin;                                                                      //  save parent window

   pixbuf = gdk_pixbuf_new_from_file(imagefile,&gerror);                               //  read image file
   if (! pixbuf) {
      zmessageACK(mainwin,gerror->message);
      return;
   }

   err = page_setup();                                                                 //  select size and orientation
   if (err) return;

   err = margins_setup();                                                              //  set margins and scale
   if (err) return;

   printop = gtk_print_operation_new();                                                //  print operation
   gtk_print_operation_set_default_page_setup(printop,pagesetup);
   gtk_print_operation_set_print_settings(printop,printsettings);
   gtk_print_operation_set_n_pages(printop,1);

   g_signal_connect(printop,"draw-page",G_CALLBACK(print_page),0);                     //  start print
   printstat = gtk_print_operation_run(printop,PRINTOP,0,0);

   if (printstat == GTK_PRINT_OPERATION_RESULT_ERROR) {
      gtk_print_operation_get_error(printop,&gerror);
      zmessageACK(mainwin,gerror->message);
   }

   g_object_unref(printop);
   return;
}


//  draw the graphics for the print page
//  rescale with cairo

void print_image::print_page(GtkPrintOperation *printop, GtkPrintContext *printcontext, int page)
{
   using namespace print_image;

   cairo_t           *cairocontext;
   double            iww, ihh, pww, phh, scale;

   pww = gtk_print_context_get_width(printcontext);                                    //  print context size, pixels
   phh = gtk_print_context_get_height(printcontext);

   iww = gdk_pixbuf_get_width(pixbuf);                                                 //  original image size
   ihh = gdk_pixbuf_get_height(pixbuf);

   scale = pww / iww;                                                                  //  rescale to fit page
   if (phh / ihh < scale) scale = phh / ihh;

   cairocontext = gtk_print_context_get_cairo_context(printcontext);                   //  use cairo to rescale
   cairo_translate(cairocontext,0,0);
   cairo_scale(cairocontext,scale,scale);
   gdk_cairo_set_source_pixbuf(cairocontext,pixbuf,0,0);
   cairo_paint(cairocontext);

   return;
}


//   Do a print paper format selection, after which the page width, height
//   and orientation are available to the caller. Units are CM.
//   (paper width and height are reversed for landscape orientation)

int print_image::page_setup()
{
   using namespace print_image;

   ch     printsettingsfile[200], pagesetupfile[200];

   snprintf(printsettingsfile,200,"%s/printsettings",zhomedir);
   snprintf(pagesetupfile,200,"%s/pagesetup",zhomedir);

   if (! printsettings) {                                                              //  start with prior print settings
      printsettings = gtk_print_settings_new_from_file(printsettingsfile,0);
      if (! printsettings)
         printsettings = gtk_print_settings_new();
   }

   if (! priorpagesetup) {                                                             //  start with prior page setup
      priorpagesetup = gtk_page_setup_new_from_file(pagesetupfile,0);
      if (! priorpagesetup)
         priorpagesetup = gtk_page_setup_new();
   }

   pagesetup = gtk_print_run_page_setup_dialog                                         //  select printer, paper, orientation
            (GTK_WINDOW(parent),priorpagesetup,printsettings);                         //  user cancel cannot be detected

   g_object_unref(priorpagesetup);                                                     //  save for next call
   priorpagesetup = pagesetup;

   orientation = gtk_print_settings_get_orientation(printsettings);                    //  save orientation
   if (orientation == LANDSCAPE) landscape = 1;
   else landscape = 0;

   gtk_print_settings_set_quality(printsettings,QUALITY);                              //  set high quality 300 dpi
   gtk_print_settings_set_resolution(printsettings,300);

   gtk_print_settings_to_file(printsettings,printsettingsfile,0);                      //  save print settings to file
   gtk_page_setup_to_file(pagesetup,pagesetupfile,0);                                  //  save print settings to file

   return 0;
}


//   Optionally set the print margins and print scale.
//   If canceled the margins are zero (or printer-dependent minimum)
//   and the scale is 100% (fitting the paper and margins).

int print_image::margins_setup()
{
   using namespace print_image;

   zdialog     *zd;
   int         zstat;

/***
       __________________________________________________
      | [x] (-) [_]   Margins                            |
      |                                                  |
      |  Margins   Top      Bottom     Left     Right    |
      |     CM   [ 0.50 ]  [ 0.50 ]  [ 0.50 ]  [ 0.50 ]  |
      |    Inch  [ 0.20 ]  [ 0.20 ]  [ 0.20 ]  [ 0.20 ]  |
      |                                                  |
      |  image scale [ 80 ] percent                      |
      |                                                  |
      |  image  width  height                            |
      |    CM    xx.x   xx.x                             |
      |   Inch   xx.x   xx.x                             |
      |                                      [ OK ] [X]  |
      |__________________________________________________|

***/

   zd = zdialog_new(TX("Margins"),parent,"OK","X",null);
   zdialog_add_widget(zd,"hbox","hbmlab","dialog");

   zdialog_add_widget(zd,"vbox","vbmarg","hbmlab",0,"homog|space=3");
   zdialog_add_widget(zd,"vbox","vbtop","hbmlab",0,"homog|space=3");
   zdialog_add_widget(zd,"vbox","vbbottom","hbmlab",0,"homog|space=3");
   zdialog_add_widget(zd,"vbox","vbleft","hbmlab",0,"homog|space=3");
   zdialog_add_widget(zd,"vbox","vbright","hbmlab",0,"homog|space=3");

   zdialog_add_widget(zd,"label","labmarg","vbmarg",TX("Margins"),"space=5");
   zdialog_add_widget(zd,"label","labcm","vbmarg","CM","space=5");
   zdialog_add_widget(zd,"label","labinch","vbmarg","Inch","space=5");

   zdialog_add_widget(zd,"label","labtop","vbtop",TX("Top"));
   zdialog_add_widget(zd,"zspin","mtopcm","vbtop","0|10|0.01|0");
   zdialog_add_widget(zd,"zspin","mtopin","vbtop","0|4|0.01|0");

   zdialog_add_widget(zd,"label","labbot","vbbottom",TX("Bottom"));
   zdialog_add_widget(zd,"zspin","mbottcm","vbbottom","0|10|0.01|0");
   zdialog_add_widget(zd,"zspin","mbottin","vbbottom","0|4|0.01|0");

   zdialog_add_widget(zd,"label","lableft","vbleft",TX("Left"));
   zdialog_add_widget(zd,"zspin","mleftcm","vbleft","0|10|0.01|0");
   zdialog_add_widget(zd,"zspin","mleftin","vbleft","0|4|0.01|0");

   zdialog_add_widget(zd,"label","labright","vbright",TX("Right"));
   zdialog_add_widget(zd,"zspin","mrightcm","vbright","0|10|0.01|0");
   zdialog_add_widget(zd,"zspin","mrightin","vbright","0|4|0.01|0");

   zdialog_add_widget(zd,"hbox","hbscale","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labscale","hbscale",TX("image scale"),"space=5");
   zdialog_add_widget(zd,"zspin","scale","hbscale","5|100|1|100");
   zdialog_add_widget(zd,"label","labpct","hbscale",TX("percent"),"space=5");

   zdialog_add_widget(zd,"hbox","hbsize","dialog",0,"space=3");
   zdialog_add_widget(zd,"vbox","vbunit","hbsize",0,"space=5");
   zdialog_add_widget(zd,"vbox","vbwidth","hbsize",0,"space=5");
   zdialog_add_widget(zd,"vbox","vbheight","hbsize",0,"space=5");

   zdialog_add_widget(zd,"label","space","vbunit",TX("Image"));
   zdialog_add_widget(zd,"label","labcm","vbunit","CM");
   zdialog_add_widget(zd,"label","labinch","vbunit","Inch");

   zdialog_add_widget(zd,"label","labwidth","vbwidth",TX("Width"));
   zdialog_add_widget(zd,"label","labwcm","vbwidth","xx.x");
   zdialog_add_widget(zd,"label","labwin","vbwidth","xx.x");

   zdialog_add_widget(zd,"label","labheight","vbheight",TX("Height"));
   zdialog_add_widget(zd,"label","labhcm","vbheight","xx.x");
   zdialog_add_widget(zd,"label","labhin","vbheight","xx.x");

   zdialog_load_inputs(zd);                                                            //  recall prior settings

   zdialog_fetch(zd,"mtopcm",margins[0]);
   zdialog_fetch(zd,"mbottcm",margins[1]);
   zdialog_fetch(zd,"mleftcm",margins[2]);
   zdialog_fetch(zd,"mrightcm",margins[3]);
   zdialog_fetch(zd,"scale",imagescale);

   get_printed_image_size();
   zdialog_stuff(zd,"labwcm",pwidth,"%.2f");                                           //  update image size in dialog
   zdialog_stuff(zd,"labhcm",pheight,"%.2f");
   zdialog_stuff(zd,"labwin",pwidth/2.54,"%.2f");
   zdialog_stuff(zd,"labhin",pheight/2.54,"%.2f");

   gtk_page_setup_set_top_margin(pagesetup,10*margins[0],MM);                          //  set page margins
   gtk_page_setup_set_bottom_margin(pagesetup,10*margins[1],MM);                       //  (cm to mm units)
   gtk_page_setup_set_left_margin(pagesetup,10*margins[2],MM);
   gtk_page_setup_set_right_margin(pagesetup,10*margins[3],MM);
   gtk_print_settings_set_scale(printsettings,imagescale);                             //  set image print scale %

   zdialog_run(zd,margins_dialog_event,"parent");                                      //  run dialog
   zstat = zdialog_wait(zd);                                                           //  wait for completion
   zdialog_free(zd);                                                                   //  kill dialog

   if (zstat == 1) return 0;
   return 1;
}


//  dialog event function
//  save user margin and scale changes
//  recompute print image size

int print_image::margins_dialog_event(zdialog *zd, ch *event)
{
   using namespace print_image;

   double   temp;

   if (strmatch(event,"escape")) zd->zstat = -2;                                       //  escape key

   if (strmatch(event,"mtopcm")) {                                                     //  get cm inputs and set inch values
      zdialog_fetch(zd,"mtopcm",margins[0]);
      zdialog_stuff(zd,"mtopin",margins[0]/2.54);
   }

   if (strmatch(event,"mbottcm")) {
      zdialog_fetch(zd,"mbottcm",margins[1]);
      zdialog_stuff(zd,"mbottin",margins[1]/2.54);
   }

   if (strmatch(event,"mleftcm")) {
      zdialog_fetch(zd,"mleftcm",margins[2]);
      zdialog_stuff(zd,"mleftin",margins[2]/2.54);
   }

   if (strmatch(event,"mrightcm")) {
      zdialog_fetch(zd,"mrightcm",margins[3]);
      zdialog_stuff(zd,"mrightin",margins[3]/2.54);
   }

   if (strmatch(event,"mtopin")) {                                                     //  get inch inputs and set cm values
      zdialog_fetch(zd,"mtopin",temp);
      margins[0] = temp * 2.54;
      zdialog_stuff(zd,"mtopcm",margins[0]);
   }

   if (strmatch(event,"mbottin")) {
      zdialog_fetch(zd,"mbottin",temp);
      margins[1] = temp * 2.54;
      zdialog_stuff(zd,"mbottcm",margins[1]);
   }

   if (strmatch(event,"mleftin")) {
      zdialog_fetch(zd,"mleftin",temp);
      margins[2] = temp * 2.54;
      zdialog_stuff(zd,"mleftcm",margins[2]);
   }

   if (strmatch(event,"mrightin")) {
      zdialog_fetch(zd,"mrightin",temp);
      margins[3] = temp * 2.54;
      zdialog_stuff(zd,"mrightcm",margins[3]);
   }

   zdialog_fetch(zd,"scale",imagescale);                                               //  get image scale

   get_printed_image_size();
   zdialog_stuff(zd,"labwcm",pwidth,"%.2f");                                           //  update image size in dialog
   zdialog_stuff(zd,"labhcm",pheight,"%.2f");
   zdialog_stuff(zd,"labwin",pwidth/2.54,"%.2f");
   zdialog_stuff(zd,"labhin",pheight/2.54,"%.2f");

   gtk_page_setup_set_top_margin(pagesetup,10*margins[0],MM);                          //  set page margins
   gtk_page_setup_set_bottom_margin(pagesetup,10*margins[1],MM);                       //  (cm to mm units)
   gtk_page_setup_set_left_margin(pagesetup,10*margins[2],MM);
   gtk_page_setup_set_right_margin(pagesetup,10*margins[3],MM);
   gtk_print_settings_set_scale(printsettings,imagescale);                             //  set image print scale %

   return 1;
}


//  compute printed image size based on paper size,
//    orientation, margins, and scale (percent)

void print_image::get_printed_image_size()
{
   using namespace print_image;

   double   iww, ihh, pww, phh, scale;

   pww = 0.1 * gtk_page_setup_get_paper_width(pagesetup,MM);                           //  get paper size
   phh = 0.1 * gtk_page_setup_get_paper_height(pagesetup,MM);                          //  (mm to cm units)

   pww = pww - margins[2] - margins[3];                                                //  reduce for margins
   phh = phh - margins[0] - margins[1];

   pww = pww / 2.54 * 300;                                                             //  convert to dots @ 300 dpi
   phh = phh / 2.54 * 300;

   iww = gdk_pixbuf_get_width(pixbuf);                                                 //  original image size, pixels
   ihh = gdk_pixbuf_get_height(pixbuf);

   scale = pww / iww;                                                                  //  rescale image to fit page
   if (phh / ihh < scale) scale = phh / ihh;

   scale = scale * 0.01 * imagescale;                                                  //  adjust for user scale setting

   pwidth = iww * scale / 300 * 2.54;                                                  //  dots to cm
   pheight = ihh * scale / 300 * 2.54;

   return;
}


/**************************************************************************************/

//  connect a user callback function to a drag-drop source widget

void drag_drop_source(GtkWidget *widget, drag_drop_source_func ufunc)
{
   void drag_drop_source2(GtkWidget *, GdkDragContext *, void *ufunc);
   void drag_drop_source3(GtkWidget *, GdkDragContext *, GtkSelectionData *, int, int, void *ufunc);

   if (! main_thread()) zappcrash("illegal call from thread");

   gtk_drag_source_set(widget,GDK_BUTTON1_MASK,null,0,GDK_ACTION_COPY);
   gtk_drag_source_add_text_targets(widget);
   gtk_drag_source_add_image_targets(widget);
   G_SIGNAL(widget, "drag-begin", drag_drop_source2, ufunc);
   G_SIGNAL(widget, "drag-data-get", drag_drop_source3, ufunc);
   return;
}


//  private function for "drag-begin" signal

void drag_drop_source2(GtkWidget *widget, GdkDragContext *context, void *ufunc)
{
   drag_drop_source_func  *ufunc2;

   GdkPixbuf   *pixbuf;
   GError      *gerror = 0;
   ch          *file = 0;

   if (! main_thread()) zappcrash("illegal call from thread");

   ufunc2 = (drag_drop_source_func *) ufunc;
   file = ufunc2();
   if (! file) goto cancel;

   pixbuf = gdk_pixbuf_new_from_file_at_size(file,128,128,&gerror);
   if (! pixbuf) {
      if (gerror) printf("*** %s \n",gerror->message);
      return;
   }

   gtk_drag_set_icon_pixbuf(context,pixbuf,64,64);                                     //  hot spot is middle of image
   return;

cancel:
   printf("drag canceled \n");
   return;
}


//  private function for "drag-data-get" signal

void drag_drop_source3(GtkWidget *widget, GdkDragContext *context, GtkSelectionData *data, int, int, void *ufunc)
{
   drag_drop_source_func  *ufunc2;

   ch        *file = 0;

   if (! main_thread()) zappcrash("illegal call from thread");

   ufunc2 = (drag_drop_source_func *) ufunc;
   file = ufunc2();
   if (! file) goto cancel;
   gtk_selection_data_set_text(data,file,-1);                                          //  drops text
   return;

cancel:
   printf("drag canceled \n");
   return;
}


//  connect a user callback function to a drag-drop destination widget

void drag_drop_dest(GtkWidget *widget, drag_drop_dest_func *ufunc)
{
   int  drag_drop_dest2(GtkWidget *, GdkDragContext *, int, int, void *, int, int time, void *);
   int  drag_drop_dest3(GtkWidget *, void *, int, int, int, void *);
   int  drag_drop_dest4(GtkWidget *, void *, int, void *);

   if (! main_thread()) zappcrash("illegal call from thread");

   gtk_drag_dest_set(widget,GTK_DEST_DEFAULT_ALL,null,0,GDK_ACTION_COPY);
   gtk_drag_dest_add_text_targets(widget);
   G_SIGNAL(widget, "drag-data-received", drag_drop_dest2, ufunc);
   G_SIGNAL(widget, "drag-motion", drag_drop_dest3, ufunc);
   G_SIGNAL(widget, "drag-leave", drag_drop_dest4, ufunc);

   return;
}


//  private function for "drag-data-received" signal
//  get dropped file, clean escapes, pass to user function
//  passed filespec is subject for zfree()

int drag_drop_dest2(GtkWidget *, GdkDragContext *context, int mpx, int mpy, void *sdata, int, int time, void *ufunc)
{
   ch  * drag_drop_unescape(ch *escaped_string);
   drag_drop_dest_func  *ufunc2;

   ch       *text, *text2, *file, *file2;
   int      cc;

   if (! main_thread()) zappcrash("illegal call from thread");

   text = (ch *) gtk_selection_data_get_data((GtkSelectionData *) sdata);

   ufunc2 = (drag_drop_dest_func *) ufunc;

   if (strstr(text,"file://"))                                                         //  text is a filespec
   {
      file = zstrdup(text+7,"drag_drop");                                              //  get rid of junk added by GTK
      cc = strlen(file);
      while (file[cc-1] < ' ') cc--;
      file[cc] = 0;
      file2 = drag_drop_unescape(file);                                                //  clean %xx escapes from Nautilus
      zfree(file);
      ufunc2(mpx,mpy,file2);                                                           //  pass file to user function
   }

   else                                                                                //  text is text
   {
      text2 = zstrdup(text,"drag_drop");
      ufunc2(mpx,mpy,text2);
   }

   gtk_drag_finish(context,1,0,time);
   return 1;
}


//  private function for "drag-motion" signal
//  pass mouse position to user function during drag

int drag_drop_dest3(GtkWidget *, void *, int mpx, int mpy, int, void *ufunc)
{
   drag_drop_dest_func  *ufunc2;
   ufunc2 = (drag_drop_dest_func *) ufunc;
   if (! ufunc2) return 0;
   ufunc2(mpx,mpy,null);
   return 0;
}


//  private function for "drag-leave" signal
//  pass mouse position (0,0) to user function

int  drag_drop_dest4(GtkWidget *, void *, int, void *ufunc)
{
   drag_drop_dest_func  *ufunc2;
   ufunc2 = (drag_drop_dest_func *) ufunc;
   if (! ufunc2) return 0;
   ufunc2(0,0,null);
   return 0;
}


//  private function
//  Clean %xx escapes from strange Nautilus drag-drop file names

ch * drag_drop_unescape(ch *inp)
{
   int  drag_drop_convhex(ch ch1);

   ch       inch, *out, *outp;
   int      nib1, nib2;

   out = (ch *) zmalloc(strlen(inp)+1,"drag_drop");
   outp = out;

   while ((inch = *inp++))
   {
      if (inch == '%')
      {
         nib1 = drag_drop_convhex(*inp++);
         nib2 = drag_drop_convhex(*inp++);
         *outp++ = nib1 << 4 | nib2;
      }
      else *outp++ = inch;
   }

   *outp = 0;
   return out;
}


//  private function - convert character 0-F to number 0-15

int drag_drop_convhex(ch ch1)
{
   if (ch1 >= '0' && ch1 <= '9') return  ch1 - '0';
   if (ch1 >= 'A' && ch1 <= 'F') return  ch1 - 'A' + 10;
   if (ch1 >= 'a' && ch1 <= 'f') return  ch1 - 'a' + 10;
   return ch1;
}


/**************************************************************************************
   Miscellaneous GDK/GTK functions
*********************************************************************************/

//  Get thumbnail image for given image file.
//  Returned thumbnail belongs to caller: g_object_unref() is necessary.

PIXBUF * get_thumbnail(ch *fpath, int size)
{
   PIXBUF      *thumbpxb;
   GError      *gerror = 0;
   int         err;
   ch          *bpath;
   STATB       statB;

   if (! main_thread()) zappcrash("illegal call from thread");

   err = stat(fpath,&statB);                                                           //  fpath status info
   if (err) return 0;

   if (S_ISDIR(statB.st_mode)) {                                                       //  if folder, return folder image
      bpath = (ch *) zmalloc(500,"get_thumbnail");
      *bpath = 0;
      strncatv(bpath,499,zimagedir,"/folder.png",null);
      thumbpxb = gdk_pixbuf_new_from_file_at_size(bpath,size,size,&gerror);
      zfree(bpath);
      return thumbpxb;
   }

   thumbpxb = gdk_pixbuf_new_from_file_at_size(fpath,size,size,&gerror);
   return thumbpxb;                                                                    //  return pixbuf to caller
}


//  make a cursor from a graphic file in application folder
//  (see initz_appfiles()).

GdkCursor * zmakecursor(ch *imagefile)
{
   GError         *gerror = 0;
   PIXBUF         *pixbuf;
   GdkDisplay     *display;
   GdkCursor      *cursor = 0;
   ch             imagepath[200];

   if (! main_thread()) zappcrash("illegal call from thread");

   display = gdk_display_get_default();
   *imagepath = 0;
   strncatv(imagepath,199,zimagedir,"/",imagefile,null);
   pixbuf = gdk_pixbuf_new_from_file(imagepath,&gerror);
   if (pixbuf && display)
      cursor = gdk_cursor_new_from_pixbuf(display,pixbuf,0,0);
   else printf("*** %s \n",gerror->message);
   return cursor;
}


/**************************************************************************************/

//  strip the alpha channel from a pixbuf
//  returns 0 if no alpha channel or fatal error

PIXBUF * gdk_pixbuf_stripalpha(PIXBUF *pixbuf1)
{
   PIXBUF      *pixbuf2;
   GDKCOLOR    color;
   int         ww, hh, rs1, rs2;
   uint8       *ppix1, *ppix2, *pix1, *pix2;
   int         nch, ac;
   int         px, py;

   if (! main_thread()) zappcrash("illegal call from thread");

   ac = gdk_pixbuf_get_has_alpha(pixbuf1);
   if (! ac) return 0;
   nch = gdk_pixbuf_get_n_channels(pixbuf1);
   color = gdk_pixbuf_get_colorspace(pixbuf1);
   ww = gdk_pixbuf_get_width(pixbuf1);
   hh = gdk_pixbuf_get_height(pixbuf1);

   pixbuf2 = gdk_pixbuf_new(color,0,8,ww,hh);                                          //  create output pixbuf2
   if (! pixbuf2) return 0;

   ppix1 = gdk_pixbuf_get_pixels(pixbuf1);                                             //  input pixel array
   ppix2 = gdk_pixbuf_get_pixels(pixbuf2);                                             //  output pixel array
   rs1 = gdk_pixbuf_get_rowstride(pixbuf1);
   rs2 = gdk_pixbuf_get_rowstride(pixbuf2);

   for (py = 0; py < hh; py++)
   {
      pix1 = ppix1 + py * rs1;
      pix2 = ppix2 + py * rs2;

      for (px = 0; px < ww; px++)
      {
         memcpy(pix2,pix1,nch-1);
         pix1 += nch;
         pix2 += nch-1;
      }
   }

   return pixbuf2;
}


/**************************************************************************************/

//  Create a pixbuf containing text with designated font and attributes.
//  Text is white on black. Widget is ultimate display destination.

PIXBUF * text_pixbuf(ch *text, ch *font, int fontsize, GtkWidget *widget)
{
   ch                      font2[60];
   PangoFontDescription    *pfont;
   PangoLayout             *playout;
   cairo_surface_t         *surface;
   cairo_t                 *cr;
   PIXBUF                  *pixbuf;
   uint8                   *pixels, *cairo_data, *cpix, *pix2;
   int                     ww, hh, rs, px, py;

   if (! main_thread()) zappcrash("illegal call from thread");

   if (! font) font = zfuncs::appfont;                                                 //  default font

   snprintf(font2,60,"%s %d",font,fontsize);                                           //  combine font and size

   pfont = pango_font_description_from_string(font2);                                  //  make layout with text
   playout = gtk_widget_create_pango_layout(widget,text);
   pango_layout_set_font_description(playout,pfont);

   pango_layout_get_pixel_size(playout,&ww,&hh);
   ww += 2 + 0.2 * fontsize;                                                           //  compensate bad font metrics
   hh += 2 + 0.1 * fontsize;

   surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24,ww,hh);                     //  cairo output image
   cr = cairo_create(surface);
   pango_cairo_show_layout(cr,playout);                                                //  write text layout to image

   cairo_data = cairo_image_surface_get_data(surface);                                 //  get text image pixels

   pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB,0,8,ww,hh);
   rs = gdk_pixbuf_get_rowstride(pixbuf);
   pixels = gdk_pixbuf_get_pixels(pixbuf);

   for (py = 0; py < hh; py++)                                                         //  copy text image to PXB
   for (px = 0; px < ww; px++)
   {
      cpix = cairo_data + 4 * (ww * py + px);
      pix2 = pixels + py * rs + px * 3;
      pix2[0] = pix2[1] = pix2[2] = cpix[3];
   }

   pango_font_description_free(pfont);                                                 //  free resources
   g_object_unref(playout);
   cairo_destroy(cr);
   cairo_surface_destroy(surface);

   return pixbuf;
}


/**************************************************************************************/

//  move the mouse pointer to given position in given window
//  widget must be realized

int move_pointer(GtkWidget *widget, int px, int py)
{
   int         rpx, rpy;
   GdkWindow   *window;

   if (! main_thread()) zappcrash("illegal call from thread");
   window = gtk_widget_get_window(widget);
   gdk_window_get_root_coords(window,px,py,&rpx,&rpy);
   gdk_device_warp(mouse,screen,rpx,rpy);
   return 1;
}


/**************************************************************************************/

//  move a window to the mouse position
//  widget is a GtkWindow, which may or may not be realized

void window_to_mouse(GtkWidget *window)
{
   using namespace zfuncs;

   int      px, py;

   if (! main_thread()) zappcrash("illegal call from thread");
   gdk_device_get_position(mouse,&screen,&px,&py);                                     //  get mouse position
   gtk_window_move(GTK_WINDOW(window),px,py);
   return;
}



