/*
 * Program: Fractal Economy Program [fep.c]
 * Programmer: Robert John Morton UK-YE572246C
 * Date: Mon 30 May 2022 - Belo Horizonte-MG

This source code is exclusively the intellectual property of the programmer.
The listing is copiously commented in order to serve as a self-teaching aid
for programmers new to the 'C' programming language. Different techniques of
achieving the same functionality are included deliberately for illustration.
The code has not been tightened too much in order to leave the program easily
adaptable to facilitate future modifications and additions.

Project:
  Phase 1: Tab 0: PEOPLE: 30 May 2022 to 09 Jul 2022
  [Tue 21 Jun 2022 changed program name from gem.c to fep.c]
  Phase 2: LAND & ENERGY:  11 Jul to 15 Aug 2022
  Phase 3: PROJ & TRADE: 14 Dec 2022 to 06 May 2023 [not full time]
  Phase 4: MAINT & EQUPT: 07 to 13 May 2023
  Phase 5: Full functionality check + listing clean-up 15 to 31 May 2023

From the directory in which 'fep.c' resides: 

                       location of the X-windows X11 graphics library
                       |                name of the executable output file
                       |                |
to compile: gcc fep.c -L/usr/X11R6/lib -o fep -lX11 -lm
                                               |     |
  link to X-windows X11 graphics objects library     |
  link to mathematics objects library for sin, cos etc.

to run: ./fep  [runs program as a self-contained unit]

The following data files must be present in the 'data' directory [at the
same level as 'fep' executable]: batt.dat, cons.dat, crop.dat, diary.dat,
fin.dat, land.dat, nad.dat

Discourse on this program at:
  https://robmorton.website/software/fep.html */

#include <X11/Xlib.h>    // contains the X11 graphical drawing functions
#include <stdio.h>       // input output to/from the console and keyboard
#include <stdlib.h>      // standard functions for the 'C' language
#include <string.h>      // for initialising strings as character arrays
#include <unistd.h>      // contains the function usleep()
#include <math.h>        // sin() cos() asin() atan() atan2() etc.
#include <time.h>
#include <X11/XKBlib.h>  // for Xkb [keyboard input]

#define WHITE 0xFFFFFF   // colour white for 'selected item' lettering
#define GREY 0x999999    // colour grey for 'unselected item' lettering
#define BLEY 0xBBCCDD    // colour blue-grey for 'instructions' lettering
#define DARK 0x333333    // colour for menu item background
#define BROWN 0x889966   // colour for menu item background
#define BLACK 0x000000   // colour black for clearing graph areas
#define YELLOW 0xFFFF00  // colour yellow for menu titles
#define YELL 0xBBBB00    // duller yellow for scroll bars
#define GREEN 0x00FF00   // colour green for graphical traces and messages
#define GRN 0x00AA00     // dark green for scope trace
#define BLEEN 0xAADDBB   // greeny grey
#define GLEEN 0x88EE99   // greeny grey
#define PREEN 0x449955   // greeny grey
#define RED 0xFF0000     // colour red for graphical traces and messages
#define GRED 0xFF6666    // colour reddish for graphical traces and messages
#define BLU 0xAA0000     // colour bluish for graphical traces and messages
#define AMBER 0xFFA500   // colour for graphical traces and messages

#define pi 3.14159265358979323846264338327950288  // circular constant π
#define RPD 0.01745329252  // number of radians per degree
#define SPR 206264.806247  // seconds of arc per radian 648000/Ï€

Display *XD;  // pointer to an X11 display device
Window XW;    // reference for the window created for this program
GC XG;        // X11 graphics context within which to write and draw


FILE       // FILE HANDLES
  *FA,     // for 'nad.dat'   1024-byte records 262144 [0x40000] bytes
  *FB,     // for 'diary.dat' 16-byte records + 16-byte index records
  *FC,     // for 'crop.dat'  64-byte records  65536 [0x10000] bytes
  *FD,     // for 'fin.dat'   32-byte records + 16-byte index records
  *FS;     // general file handle used in index sorts and searches

int
  // VARIABLES PERTAINING TO Tab 7: THE MAINTENANCE TAB
  a7 = 1,  // Tab 7 MAINT button states
  b7 = 0,
  c7 = 0,
  d7 = 0,
  e7 = 0,
  f7 = 0,
  g7 = 0,
  h7 = 0,

  // VARIABLES PERTAINING TO TAB 6: THE TRADE TAB
  a6 = 1,  /* the current states of Tab 6's horizontal buttons

   0x100   256              DOC HELP button         128 0x80
   0x200   512            DATE| |STATUS button       64 0x40
   0x400  1024         CLIENT|| ||LIST button        32 0x20
   0x800  2048          CROP||| |||NEW button        16 0x10
  0x1000  4096        PRICE|||| ||||EDIT button       8
  0x2000  8192    QUANTITY||||| |||||PREV button      4
  0x4000 16384      VALUE|||||| ||||||NEXT button     2
  0x8000 32768     PAID?||||||| |||||||FIND button    1
                       |||||||| ||||||||
  b6 00000000 00000000 00000000 00000000
  |          */
  b6 = 256,  // vertical button state in TRANS sub-tab
  g6 = 0,    // vertical bias for lines display on SALES ledger
  r6 = 1,    // current transaction record number
  f6 = 0,    // field number of the invoice in TRANS sub-tab
  C6 = 0,    // colour for payment deadline in TRADE->TRANS->LIST sub-sub-tab 
  H6 = 0,    // dummy: highest occupied transaction record [not used]
  l6 = 0,    // index record number of currently highlighted list item
  N6 = 0,    // current total number of transactions on-file
  o6 = 0,    // listing order for invoices [default = DOCument number order]
  S6 = 0,    // start byte of current index within the invoice index file
  V6 = 0,    // invoice overdue alert flag
  W6 = 0,    // Byte 3: transaction 'index re-sort needed' flag bits 
  X6 = 200,  // left start coord of invoice data fields
  Y6 = 99,   // base line of first invoice data field

  // VARIABLES PERTAINING TO TAB 5: THE PROJECTS TAB

  a5 = 1,    /* show CROPS sub-tab by default

   0x100   256            trawl HELP button        128 0x80
   0x200   512           amber| |LIST button        64 0x40
   0x400  1024        PLANTED|| ||KILL button       32 0x20
   0x800  2048       HARVEST||| |||NEW button       16 0x10
  0x1000  4096         DAYS|||| ||||PREV button      8
  0x2000  8192         LOC||||| |||||NEXT button     4
  0x4000 16384        REF|||||| ||||||TRAWL button   2
  0x8000 32768      CROP||||||| |||||||FIND button   1
                       |||||||| ||||||||
  b5 00000000 00000000 00000000 00000000
  |          */
  b5 = 0,    // vertical button states in CROPS sub-tab
  N5 = 0,    // get number of occupied crop records
  H5 = 0,    // get highest occupied record
  r5 = 1,    // crop number [number of current crop record]
  l5 = 0,    // index record number of currently highlighted list item
  L5[] = {31,8,8,8,8,8,8,8,8,8},  // field lengths for crops data
  f5 = 0,    // current field number within crop data display
  p5 = 0,    // start byte of CROP field currently being edited
  q5 = 0,    // start byte of current crop record
  g5 = 0,    // initial scroll position on Crops Sub-tab
  C5 = 0,    // colour for harvesting date in the CROPS sub-tab
  c5 = 0,    // planting date
  d5 = 0,    // growing time
  e5 = 0,    // harvest date
  j5 = 0,    // elapsed time
  B5 = 0,    // full content of the above byte
  R5 = 0,    // number of a 'prospectively' new crops record
  I5 = 0,    // byte num of Record 0 of crop.dat containing new record's bit  
  X5 = 256,  // x-coordinate of start of after-notes
  o5 = 4,    // listing order for crops [default = REFerence number order]
  S5 = 0,    // start byte of current index within the crop index file
  V5 = 0,    // crop harvesting alert flag
  W5 = 0,    // crop list 're-sort required' flags [not used at the moment]

  // VARIABLES PERTAINING TO TAB 4: THE EQUIPMENT TAB
  a4 = 1,    // Tab 4 control button states [bottom row of buttons]
  b4 = 0,    // states of the vertical buttons of the UDT tab
  c4 = 0,    // states of the vertical buttons of the FARM tab
  d4 = 0,    // states of the vertical buttons of the NAV tab
  e4 = 0,    // states of the vertical buttons of the SPARE tab
  f4 = 0,    // states of the vertical buttons of the SPARE tab
  g4 = 0,    // states of the vertical buttons of the SPARE tab
  h4 = 0,    // states of the vertical buttons of the SPARE tab
  i4 = 0,    // Tab 4 horizontal button number

  // VARIABLES PERTAINING TO TAB 3: THE POWER TAB
  a3 = 1,    // Tab 3 control button states
  H3 = 194,  // horizontal start position of graph area
  V3 = 82,   // vertical start position of graph area
  b3[8][4];  // average power values for different sources and sinks
double
  c3 = 0,    // solar declination [in radians]
  l3 = 0;    // latitude of landshare [in radians]
int
  d3 = 0,    // solar azimuth [bearing]
  e3 = 0,    // day number of today within current year [01Jan is Day 0]
  h3 = 0,    // current hour of the day [sun time]
  k3 = 0,    // 2 * pi / 365 or 2 * pi / 365 [in radians]
  p3 = 0,    // number of the current 5-minute period of the day
  q3 = 0,    // seconds since midnight [local time]
  s3 = 0,    // sun time [time of day according to the position of the sun]
  t3 = 0,    // Equation of Time offset [seconds]
  u3 = 0,    // landshare's longitudinal time off-set from GMT [seconds]
  w3 = 0,    // current power [in watts] being produced by the PV array
  x3 = 0,    // solar angle of elevation [in degrees]
  lq = 0,    // charge percentage of the battery for the Left-Hand Bus
  rq = 0,    // charge percentage of the battery for the Right-Hand Bus
  y3 = 30,   // average electricity consumption of the landshare in MJ/day
  z3 = 35,   // average green generating capacity per side [right/left]


  // VARIABLES PERTAINING TO TABS 2: THE WATER TAB
  a2 = 1,    // Tab 2 control button states: 1,2,4,8,16,32,64,128
  v2 = 1,    // state of rain graph's vertical buttons
  H2 = 132,  // horizontal start point of monthly graphs
  V2 = 82,   // vertical start position of graph area


  // VARIABLES PERTAINING TO TAB 1: THE LAND TAB
  a1 = 0,  // Tab 1 control button states
  b1 = 0,  // latitude of landshare's central reference point [seconds of arc]
  c1 = 0,  // longitude of landshare's central reference point
  d1 = 0,  // height of landshare's central reference point [metres]
  r1[360], // radius [in centimetres] to periphery for each bearing degree
  h1[360], // height [in centimetres] of boundary for each bearing degree
 

  /* VARIABLES PERTAINING TO TAB 0: THE PEOPLE TAB

   0x100   256            trawl HELP button     128 0x80
   0x200   512     search mode| |NOTES button    64 0x40
   0x400  1024 Ref entry mode|| ||DIARY button   32 0x20
   0x800  2048 selector mode||| |||LIST button   16 0x10
  0x1000  4096        INTEG|||| ||||PREV button   8
  0x2000  8192       RESET||||| |||||NEXT button  4
  0x4000 16384      REGEN|||||| ||||||KILL button 2
                        ||||||| |||||||NEW button 1
                        ||||||| ||||||||
  a0 00000000 00000000 00000000 00000000
  |        */
  a0 = 0,  // Tab 0 control button and switch states
  D0 = 0,  // scrolling dispatcher switch 0=NAD, 1=LIST, 2=DIARY, 3=SUPP

  // NAD variables
  b0 = 0,  // colour states of indexed field names
  f0 = 0,  // current name & address field number [0 to 12]
  g0 = 0,  // field number of search term found by trawler
  h0 = 0,  // index of start of search term found by trawler
  i0 = 0,  // number of the latest found index record
  I0 = 0,  // number of the 1st index record of search range
  p0 = 0,  // start byte of current name & address field
  q0 = 0,  // start byte of current name & address record
  r0 = 1,  // record number of currently displayed name & address
  N0 = 0,  // total number of occupied PEOPLE records
  H0 = 0,  // highest occupied PEOPLE record
  S0 = 0,  // start byte of current index within 'nad.idx'
  V0 = 0,  // dummy: 'people alert' flag [not used at the moment]

  // LIST variables
  W0 = 0,  // LIST 'index re-sort required' flag bits [not used at the moment]
  c0 = 0,  // LIST column heading button states: 1,2,4,8,16
  d0 = 3,  // column heading number 0 to 3 [default is 3: REF]
  k0 = 0,  // mouse bias [record number of nad.idx at first line of display
  l0 = 0,  // selected line in whole index for the PEOPLE->LIST sub-tab

  // DIARY variables [regarded as a virtual Tab 8]
  e0 = 0,  // mouse scroll bias for DIARY
  x0 = 0,  // column heading number 0 to 3 [default is 0: REF]
  m0 = 1,  // DIARY heading button states 1,2,4,8,16,32 [default DAYS]
  v0 = 0,  // states of vertical buttons in DIARY sub-tab 1,2,4,8,16,32,64,128
  N8 = 0,  // current number of items in the DIARY file
  H8 = 0,  // dummy: highest occupied DIARY record [not used]
  u0 = 0,  // selected line in whole diary for the PEOPLE->DIARY sub-tab
  V8 = 0,  // diary alert flag
  W8 = 0;  // DIARY 'index re-sort required' bits


/* Record size 1024 bytes: 561 occupied, 463 for future use. Search indexes
   are built for fields 08,09,10,11 only. For each name & address field: the
   1st character is the number of bytes of content, followed by up to 47, 24
   or 37 characters of the content itself.

                     field content       ALPHANUMERIC                   field
                    offset length          CONTENT                     number
                         | |                  |                             |
  FULL NAME             00 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 00
  HOUSE/BUILDING        48 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 01
  STREET/ROAD           96 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 02
  DISTRICT/ZONE        144 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 03
  CITY/TOWN            192 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 04
  COUNTY/MUNICIPALITY  240 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 05
  STATE/ETHNIC COUNTRY 288 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 06
  SOVEREIGN COUNTRY    336 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 07
  POSTAL CODE          384 NAAAAAAAAAAAAAAAAAAAAAAAA 08 [24]
  FAST SEARCH NAME     409 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 09 [37]
  PERSONAL IDENTIFIER  447 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 10
  COUNTRY CODE [CC]    485 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 11
  SELECTOR KEY         523 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 12

  L0[] = field lengths of the fields in a name & address [nad] record
  O0[] = field offsets of the NAD fields from the beginning of the a record
  P0[] = outer box-widths for the yellow or green boxes that mark a NAD field

  FIELD NUMBER     00  01  02  03  04  05  06  07  08  09  10  11  12 */
  char  L0[13] = { 47, 47, 47, 47, 47, 47, 47, 47, 24, 37, 37, 37, 37};
  short O0[13] = { 00, 48, 96,144,192,240,288,336,384,409,447,485,523},
        P0[13] = {288,288,288,288,288,288,288,288,150,228,228,228,228};

/*record number of 'nad.dat' to which this index entry pertains 'r0'
  |length of the currently valid content of this field
  ||field content [up to 14 bytes] This is the field that is sorted.
  ||--------------
  0000000000000000  16-bit index record [same for all indexes]*/


int  tb = 0,  // number of the currently displayed Tab [0 to 7]

// VARIABLES PERTAINING TO THE KEYBOARD
     ks = 0,  // keyboard's key scan code
     ka = 0,  // ASCII value of current keypress
     kr = 0,  // carriage-return [enter]

// VARIABLES PERTAINING TO THE MOUSE
     mx = 0,  // x-coord of mouse click 
     my = 0,  // y-coord of mouse click
     mw = 0,  // mouse wheel variable: 1:scroll up, 2:scroll down

// VARIABLES PERTAINING TO THE LINE EDITOR
     ea = 0,  // 1: editor active; 0: editor inactive
     ey = 0,  // line editor's current vertical lettering base coordinate
     ex = 0,  // line editor's current horizontal lettering coordinate
     en = 0,  // current length of the line editor's field content
     ec = 0;  // current cursor position [line editor]
char le[48],  // line editor array
     st[32],  // search term array for T0findNAD(). Saves much stack pushing.

// VARIABLES PERTAINING TO TIME AND DATE
     dm[] = {31,28,31,30,31,30,31,31,30,31,30,31};  // days in each month
time_t et;     // elapsed time [in seconds since 00:00:00 UDT 01 Jan 1970]
struct tm tm;  // structure for local time parameters





// ANCILLARY FUNCTIONS YE8BB56C ----------------------------------------------

void helpPage(char *S) {  // DISPLAY THE HELP PAGE IN THE DEFAULT WEB BROWSER
  int i, s = strlen(S);   /* length of 'fep.html' file path

                Opens a file or a URL in the user's preferred application:
                |  in this case, in the default web browser.
                |
                |        base URL of my web site
                |        |                            */
  char C[64] = "xdg-open https://robmorton.website/"; // 35 characters

  for(i = 0; i < s; i++)  // add the file path for 'fep.html'
    C[i+35] = S[i];       // onto the end of the base URL
  C[i+35] = ' ';          // space after the end of the command
  C[i+36] = '&';          // place command in 'background' mode
  C[i+37] = 0;            // string-terminating NULL character
  printf("%s\n",C);       // verification print in terminal window
  system(C);              // issue the command to the system
}




/* GET AND VALIDATE THE RECORD NUMBER, THE SEARCH TERM LENGTH AND THE SEARCH
   TERM TEXT FROM AN INDEX RECORD. It is assumed that the file pointer of
   file 'F' has been pre-set to the start byte of the required index record.
   This function stores the data record number and index content length in
   the first two bytes of the returned string. */

char *getIdxRec(FILE *F) {
  static char S[17];       // to hold the search term text
  int k;
  for(k = 0; k < 17; k++)  // clear the output array
    S[k] = 0;

  /* Please note well that standard function fgetc() returns a SIGNED integer
     formed by treating the lower 8 bits it gets from the char on disk as an
     UNSIGNED [positive only] quantity in the range 0 to 255. */

  int a = fgetc(F);       // get data record number from disk [unsigned char]
  if(a < 1)               // if for some reason the record number is < 1
    a = 1;                // default it to 1: record 1 is the first record
  if(a > 255)             // if the record number is beyond permitted maximum
    a = 255;              // default it to maximum: only 255 records maximum
  S[0] = a;               // set the record number of the main data record

  int K = fgetc(F);       // get length of field content [an unsigned char]
  if(K > 14) K = 14;      // error content length beyond end of field
  S[1] = K;               // length of search term

  K += 2;                 // text is stored starting at S[2]
  for(k = 2; k < K; k++)  // load index search string into S[]
    S[k] = fgetc(F);
  return S;               // return the pointer to search term text
}




int ratSysDate() {       // RETURNS THE CURRENT SYSTEM DATE YYYYMMDD
  et = time(NULL);       // time in seconds since 00:00:00 UDT 01 Jan 1970
  tm = *localtime(&et);  /* get the local time parameters

                number of years, counted from the year 1900
                |      add 1900 to get the complete year number
                |      |       shift the year to the left 4 places
                |      |       |              month number counting from zero 
                |      |       |              |                            */
  return (tm.tm_year + 1900) * 10000 + (tm.tm_mon + 1) * 100 + tm.tm_mday; /*
                                                    |      |         |
  add 1 to get the true month number [so January = 01]     |         |
                      shift the month number left two places         |
                    add in the day of the month [which starts at day 1]    */
}




void clearTab() {  // CLEAR THE TAB AREA
  XSetForeground(XD,XG,BLACK);
  XFillRectangle(XD,XW,XG,17,38,484,383);

}




/* CONVERT A STRING 'S' OF LENGTH 'I' TO UPPER CASE ONLY. Called from 2 places
   in T0trawl() and 1 place each in T0search() and T0edField(). */

void SUC(int I, char *S) {
  int c,i;
  for(i = 0; i < I; i++)            // for each character within the string
    if((c = S[i]) > 96 && c < 123)  // if it is a lower case letter
      S[i] -= 32;                   // convert it to its upper case equivalent
}




/* [RE]DISPLAY LINE EDITOR'S CURSOR: called from 1 place each in
   showLE() and lfrt() and from 2 places each in bksp() and hEnd(). */

void doCursor(int sw) {

       /* line editor's current horizontal letter coordinate [start of field] 
          |    less 1 because ???
          |    |   cursor's current character position in line editor array
          |    |   |    six horizontal pixels per character
          |    |   |    |   */
  int x = ex - 1 + ec * 6,  // compute horizontal position of cursor
      c = GREEN;            // show the cursor in bright green
  if(sw == 0) c = BLACK;    // use background colour to erase the cursor

       /* cursor position within line editor array
          |    current length of content in line editor array
          |    |            */
  else if(ec > en) ec = en; // ensure cursor is not beyond content length
  XSetForeground(XD,XG,c);  // set colour for the cursor

                    /* line editor's current vertical lettering base
                       |   top [y-coord] of cursor line
                       |   |      bottom [y-coord] of cursor line
                       |   |      |    */
  XDrawLine(XD,XW,XG,x,ey-11,x,ey+1);  // display the cursor
}




void showButs() {  // SHOW THE 8 BLANK BUTTONS ACROSS THE BOTTOM OF THE TAB
  int h = 17;      // set 'h' to left side of first button
  XSetForeground(XD,XG,DARK);   // shade for buttons' background
  for(int i = 0; i < 8; i++) {  // shade the background area for each button
    XFillRectangle(XD,XW,XG,h,362,53,21);
    h += 59;  // move across to the next button
  }
}




/* GET AN INT FROM A 4-BYTE TRAIN IN A FILE.
   Called from a large number of places throughout the program */

int getInt(FILE *F) {
  unsigned int x      // need to shift the sign bit too
    = fgetc(F) << 24  // shift first byte to left of int
    | fgetc(F) << 16  // shift second byte to 2nd from left
    | fgetc(F) << 8   // shift 3rd byte to 2nd from right
    | fgetc(F);       // leave 4th byte where it is at far right
  return (int)x;      // return as a signed integer
}




/* PUT AN INT TO A 4-BYTE TRAIN IN A FILE: NO FLUSH.
   Called from a large number of places throughout the program */

void putInt(int X, FILE *F) {
  unsigned int x = (unsigned int)X;  // need to shift the sign bit too
  fputc(x >> 24 & 0xFF, F);  // Store the new number of records
  fputc(x >> 16 & 0xFF, F);  // as a 4 byte int in the first
  fputc(x >>  8 & 0xFF, F);  // 4 bytes of the records file.
  fputc(x & 0xFF, F);
}




/* GET A TWO-BYTE [16-BIT] QUANTITY FROM DISK FILE. Note that the retrieved
   quantity is unsigned but it is placed into a signed 32-bit integer. */

int getShort(FILE *F) { return fgetc(F) << 8 | fgetc(F); }




/* PUT A TWO-BYTE [16-BIT] QUANTITY TO DISK FILE. Note that the signed
   integer 'x' is stored as an unsigned 16-bit quantity in the range +0 to
   +0xFFFF [+65535]. */

void putShort(int x, FILE *F) { fputc(x >> 8 & 0xFF,F); fputc(x & 0xFF,F); }




/* CONVERT AN 'int x' TO AN l-DIGIT NUMERIC STRING
   WITH LEADING SPACES OR ZEROS IN ARRAY S[].*/

char *showInt(int x, int l, int y) {
  int f = 0;          // indicates that the presented number 'x' is positive
  static char S[33];  // to hold the string version of the number
  char s = ' ';       // for leading spaces
  if(y) s = '0';      // for leading zeros
  if(l > 11) l = 11;  // safety precaution
  for(int i = 0; i < l; i++)
    S[i] = s;         // fill the whole field with spaces or zeros
  S[l - 1] = '0';     // the least-significant char must be a '0'
  S[l] = (char)0;     // terminating NULL character
  if(x < 0) {         // if the presented number is negative
    x = -x;           // reverse its sign
    f = 1;            // indicate that 'x' as presented was negative
  }
  if(x > 0) {         // if the number is non-zero:
    int i = l;        // total of 'l' possible digits in the number

    /* While there're still more digits to process, work back-
    wards from [8] to [0], storing the remainder after dividing
    the number by 10 as a numeric character in array S[]. Then
    divide the number by 10 [not rounded] and loop back. */

    while(x > 0 && i > 0) {
      S[--i] = (char)(x % 10 + 48);
      x /= 10;
    }
    if(f == 1)     // if x was negative,
      S[0] = '-';  // make the first character a minus sign
  }
  return S;  // return the address of start of char array
}            // containing the formatted number.




/* CHECK THAT THE YEAR, MONTH AND DAY OF A DATE ARE WITHIN VALID RANGE
   Called one by endDate(), and twice by getET(). */

int monthCheck(int y, int m, int d) {
  int n = dm[m];  // get the number of days in the given month

  // if it's February, add an extra day if 'y' is a leap year
  if(m == 2 && (y % 4 == 0 && y % 100 != 0 || y % 400 == 0)) n++;

  // check that the year, month and day numbers are within their valid ranges
  if(y < 999 || y > 9999 || m < 1 || m > 12 || d < 1 || d > n) return 1;
  return 0;
}



/* GET THE NUMBER OF DAYS IN THE GIVEN MONTH [INCLUDING THE LEAP YEAR TEST
   if it's February] AND { (the year is divisible by 4 but not by 100)
   OR (it is divisible by 400) } then it is a leap year so add an extra day.
   Called twice from endDate() and once from getET(). */

int daysInMonth(int y, int m) {
   int n = dm[m];  /* get the number of days in the given month

          if the month is February
          |         and the year divides exactly by 4
          |         |             but does not divide exactly by 100
          |         |             |               
          |         |             |                                */
  if(m == 2 && (y % 4 == 0 && y % 100 != 0 || y % 400 == 0)) n++;  /*
                                                  |          |
                       OR it divides exactly by 400          |
        then it's a LEAP YEAR, so add an extra day to February     */

  return n;  // return the correct number of days for the month
}




/* COMPUTE THE END DATE THAT IS A GIVEN NUMBER OF DAYS AFTER A GIVEN START
   DATE. Called from 1 place in T5crops().

       start date      number of days the end date is into the future
                |      | */
int endDate(int s, int t) {
  if(t < 0 || t > 1000) return 0;  // exit if elapsed time is out of range

  int x = s % 10000,  // the month and day numbers as a single integer
      y = s / 10000,  // the year number as an integer
      m = x / 100,    // month number as an integer
      d = x % 100;    // day number as an integer

  if(monthCheck(y,m,d)) return 0;  // check integrity of the given date

  int n = daysInMonth(y,m);  // get number of days in this month

  while(t > 1) {      // while there are still more extra days left:
    if(++d > n) {     // If on incrementing the day number it overshoots the
      d = 1;          // number of days in the current month, reset it to 1
      if(m++ > 12) {  // if incremented month is now beyond 12 [December]
        m = 1;        // set the month to 1 [January]
        y++;          // advance to the new year
      }
      n = daysInMonth(y,m);
    }
    t--;  // decrement the number of days to go
  }
  return y * 10000 + m * 100 + d;  // return the rationalised end date
}




/* COMPUTE THE ELAPSED TIME [IN DAYS] BETWEEN TWO DATES.
   Called from 1 place in T6showTrans() and 2 places in T5crops().

     start date       end date
              |       |                             */
int getET(int D1, int D2) { if(D1 == D2) return 0;  // if dates are the same

  /* If the 'end' date is before the 'start' date, then
  swap the dates over and set the negative flag 'f' */
  int f = 0, x; if(D1 > D2) {x = D1; D1 = D2; D2 = x; f = 1; }

  /* unpack the start date 'D1' into year 'y', month 'm' and day 'd'
  and the end date 'D2' into year 'Y', month 'M' and day 'D' */
  x = D1 % 10000; int y = D2 / 10000, m = x / 100, d = x % 100;
  x = D2 % 10000; int Y = D2 / 10000, M = x / 100, D = x % 100;

  // check that the years, months and days are within valid range
  if((monthCheck(y,m,d)) || (monthCheck(Y,M,D))) return 0;  // invalid

  int t = -d;  // set elapsed time initially to -(start date's day number)
  while(1) {   // permanent but breakable loop

    /* If the 'start' month = the 'end' month and the 'start' year = the 'end'
    year, the elapsed time is the difference between day numbers; so add the
    number of days to date in the 'end' month and exit. */
    if(m == M && y == Y) { t += D; break; }

    t += daysInMonth(y,m-1);  // add the number of days in this month

    /* if, on incrementing the month number, it is now beyond month 12 [Dec-
    ember], reset it to month 1 [January] and increment the year number. */
    if(++m > 12) { m = 1; y++; }

    /* if the elapsed time goes beyond 10000 days, set
    it to zero and break out of the the while() loop. */
    if(t > 9999) { t = 0; break; }
  }
  if(f == 1) t = -t;  // if start date is future, invert sign of elapsed time
  return t;           // return the elapsed time
}




/* SWAP A PAIR OF RECORDS OF THE INDEX SECTIONS OF THE 'nad.dat',
   'diary.dat', 'crop.dat' or 'fin.dat' FILES.

                  start byte of first record
                  |      start byte of second record
                  |      |      record length
                  |      |      |  */
void sortSwap(int i, int j, int K) {
  char I[K],  // to hold content of i-th record
       J[K];  // to hold content of j-th record
  int k;
  fseek(FS,i,SEEK_SET);   // start byte of the i-th record
  for(k = 0; k < K; ++k)  // put content of i-th record into array I[]
    I[k] = fgetc(FS);

  fseek(FS,j,SEEK_SET);   // start byte of the j-th record
  for(k = 0; k < K; ++k)  // put content of j-th record into array J[]
    J[k] = fgetc(FS);

  fseek(FS,i,SEEK_SET);   // start byte of the i-th record
  for(k = 0; k < K; ++k)  // put content of J[] into i-th record
    fputc(J[k],FS);

  fseek(FS,j,SEEK_SET);   // start byte of the j-th record
  for(k = 0; k < K; ++k)  // put content of I[] into j-th record
    fputc(I[k],FS);
}




/* COMPARE THE CONTENT OF INDEX RECORD 'i0' WITH THAT OF A REFERENCE TERM 'r'
Compare each character of the reference term in st[] with each corresponding
character of the element retrieved from the disk record. The first pair of
characters that are unequal completes the test. If the reference-term is
greater than the retrieved element, return a value < 0. If the retrieved
element is greater than the reference-term, return a value > 0. Otherwise,
if the elements are equal, return 0. Called twice from idxSort(), 1 place
each in T0slice(), T0step() and T0firstExact() and 2 places in T0FHNM(). */

int strComp(int i) {  // i = record number of the current index record

     /* file handle for the current index file
        |   record number of current index record [range: 1 to N0 inclusive]
        |   |    multiply by 16 to get its current start byte
        |   |    |    add a further 2 to get start byte of index string
        |   |    |    |             */
  fseek(FS,(i << 4) + 2,SEEK_SET);  // seek start byte of index string

  for(int i = 0; i < 14; i++) {  // for each of the 14 possible characters
    int r = st[i],               // get next char from the reference string 
        x = fgetc(FS);           /* get the next one from the index file

    if the current character of the element retrieved from the index file
       | is below
       | | the corresponding character of the reference element in array st[]
       | | |              */
    if(x < r) return -1;  /* then return a value less than zero

    if the current character of the element retrieved from the index file
       | is above the
       | | the corresponding character of the reference element in array st[]
       | | |              */
    if(x > r) return +1;  /* then return a value greater than zero

    If we've reached here then both the current character of retrieved element
    and the corresponding character of the reference element must be the same,
    so if either is zero, we've finished and can return immediately. */ 

    if(x == 0) return 0;  // returning zero says that both terms are equal
  }
  return 0;  /* If we finish the entire loop, it means that all 30 characters
             of the retrieved element and the reference element are the same,
             so we exit with zero to indicate that they're identical. */
}




/* SORTS AN INDEX WITHIN 'xxx.dat' INTO ALPHABETICAL [OR RATHER, ASCII
   CASTING] ORDER. This function is based on the Hoare Quick-Sort. It
   works with an index in the index section of the file whose handle is
   within the global file-pointer variable 'FS'. Called from 2 places
   within itself and from from 1 place in T0regen(), T0diarySort(),
   T5cropSort() and T6transSort().

                 lowest occupied element of the index to be sorted
                 |      highest occupied element of the index to be sorted
                 |      |     */
void idxSort(int L, int H) {
  int
    l = L,     // set moving low to LOW end of partition
    h = H;     // set moving high to HIGH end of partition
  if(H > L) {  // if partition contains anything:

    /* PUT THE CONTENT OF THE MIDWAY RECORD INTO st[]
          file handle of the index file being sorted 'xxx.dat'
          |    lowest element of the sort range
          |    |   highest element of the sort range
          |    |   |    divide by 2 to get the midway element
          |    |   |    |     multiply by 16 to get the element's start byte
          |    |   |    |     |    add 2 to skip record number & field length
          |    |   |    |     |    | */
    fseek(FS,((L + H >> 1) << 4) + 2,SEEK_SET);
    for(int i = 0; i < 14; i++)  // copy the 14 byte text field
      st[i] = fgetc(FS);         // into the 'sort term' array st[]

    while(l <= h) {  /* loop through file until the indexes cross

            while the current low element
            | is below
            | | the upper boundary of the current partition
            | | | AND   the content of the lower element
            | | | |     |       is less than
            | | | |     |       | the content of the partition's mid-point
            | | | |     |       | |  move the 'low element' up by one place
            | | | |     |       | |  |     */
      while(l < H && strComp(l) < 0) l++;  /*

            while the current high element
            | is above
            | | the lower boundary of the current partition
            | | | AND   the content of the high element
            | | | |     |       is greater than
            | | | |     |       | the content of the partition's mid-point
            | | | |     |       | |  move the 'high element' down by one place
            | | | |     |       | |  |     */
      while(h > L && strComp(h) > 0) h--;

      if(l <= h) {  /* IF THE LOW ELEMENT IS AT OR BELOW
                       THE LEVEL OF THE HIGH ELEMENT:

                 number of the moving 'low' element
                 |    multiply by 16 [the number of bytes per record]
                 |    | number of the moving 'high' element
                 |    | |    multiply by 16 [the number of bytes per record]
                 |    | |    |
                 |    | |    |          THEN SWAP THE CONTENTS OF */
        sortSwap(l << 4,h << 4,16);  // THE HIGHER & LOWER ELEMENTS.

        l++;  // push the lower sort boundary up by one element
        h--;  // pull the upper sort boundary down by one element
      }
    }
    if(L < h) idxSort(L,h);  // sort the lower partition
    if(l < H) idxSort(l,H);  // sort the upper partition
  }
}




/* DISPLAY ADVICE AND ERROR MESSAGES.
   Called from a large number of places throughout the program. */

void showMsg(int m) {
  const int clr[] = {
    DARK,GREEN,YELLOW,YELLOW,RED,RED,YELLOW,GREEN,RED,RED,YELLOW,RED,
    GREEN,GREEN,GREEN,GREEN,YELLOW,YELLOW,YELLOW,YELLOW,YELLOW,RED,
    GREEN,GREEN,YELLOW,GREEN,GREEN,GREEN,YELLOW
  };
  const char msg[] = { 0,18,27,31,32,32,37,56,31,53,14,36,18,18,19,
                      27,53,41,24,28,30,24,20,41,40,25,15,14,62};
  const char *MSG[] = {
    "",
    "Exact match found.",
    "Nearest higher match found.",
    "Nearest lower match only found.",
    "Search term beyond end of index.",
    "No more spare records available.",
    "Trawling for the entered search term.",
    "Search term found. Click NEXT button to continue search.",
    "Search term could not be found.",
    "Couldn't find this record in the index. See terminal.",
    "RECORD DELETED",
    "INDEX INTEGRITY ERROR. See terminal.",
    "INDEX INTEGRITY OK",
    "DATABASE WAS RESET",
    "INDEXES REGENERATED",
    "Landshare data regenerated.",
    "This field's content is computed: it can't be edited.",
    "The requested crop record does not exist.",
    "There is no next record.",
    "There is no previous record.",
    "Re-sorting the selected index.",
    "No diary entry selected.",
    "Diary entry deleted.",
    "The edited field content has been stored.",
    "Editing of the field has been cancelled.",
    "Transaction record found.",
    "Data backed up.",
    "Data Restored.",
    "This function is not implemented in the demonstration version."
  };
  if(m == 0) {
    XSetForeground(XD,XG,GREY);
    XDrawString(XD,XW,XG,17,345,"MESSAGE",7);
  }
  XSetForeground(XD,XG,DARK);  // clear the message area
  XFillRectangle(XD,XW,XG,65,331,418,21);
  XSetForeground(XD,XG,clr[m]); 
  XDrawString(XD,XW,XG,71,345,MSG[m],msg[m]);
}




/* IMPLEMENTS LIST SCROLLING WITH THE MOUSE WHEEL AND DISPLAYS THE SCROLL BAR
   Called from T0listScroll(), T0diaryScroll(), T5cropScroll(), T6transList().

                 position in list of first displayed line before scrolling
                 |      current number of items in the full list [file]
                 |      |     */
int Scroller(int g, int s) {

  /* MOUSE CONTROL FOR LIST SCROLLING
           just scrolled down the list by one position
           |    current offset of start of display from record 1 of file
           |    |   current number of items in the whole list [file]
           |    |   |   number of items at a time displayed on screen
           |    |   |   |         */
  if(mw == 1 && g < s - 11) g++;  // if Mouse Wheel 1 scroll down the list
  else if(mw == 2 && g > 0) g--;  // if Mouse Wheel 2 scroll back up list
  mw = 0;                         // Mouse wheel event now dealt with

  // IF MORE THAN 11 LINES IN THE FILE WE NEED TO DISPLAY A SCROLL BAR

  if(s > 11) {  /* if there are more than '11' records in the file

            total height of the scroll-bar area: 209 pixels
            |     times '11' data lines on the screen
            |     |    current number of occupied records
            |     |    |   */
    int h = 209 * 11 / s;  /* height of highlighted part of the scroll bar

        height of the dull part of the scroll area above the highlighted part
        |   number of the first record in the list
        |   |     total height of the scroll bar area
        |   |     |    height of highlighted part of the scroll bar
        |   |     |    |     current number of occupied records
        |   |     |    |     |   number of lines in the displayed list
        |   |     |    |     |   |     */
    int v = g * (209 - h) / (s - 11);

    XSetForeground(XD,XG,DARK);  // show full length scroll bar in grey
    XFillRectangle(XD,XW,XG,475,84,8,209);
    XSetForeground(XD,XG,PREEN);  // show highlighted part of scroll bar
    XFillRectangle(XD,XW,XG,475,84+v,8,h);
  }
  return g;  /* returns the position within the whole list [file]
                of the first displayed line after scrolling */
}




/* HANDLES A CLICK ON ONE OF THE LIST ITEMS. Called from T0listClick(),
   T0diaryClick(), T5cropListClick(), T6transListClick(). */

int listClick() {
  int i = 0;                 // loop variable
  if(mx > 16 && mx < 483) {  // if click within width of a list data line
    int
      a = 85,   // top of the clickable area of the top data line
      b = 103;  // botton of the clickable area of the top data line

    for(i = 1; i < 12; i++) {  // for each of the 11 data lines
      if(my > a && my < b) {   // if click in this line's vertical limits
        break;                 // set number of selected line; break loop
      } a += 19; b += 19;      // otherwise drop to the next line down
    }                          // NOTE: clicking anywhere else on the
  }                            // sub-tab de-selects all lines.
  if(i > 0 && i < 12)          // provided click was on one of the 11 lines
    return i;                  // return the number of the clicked line
  return 0;                    // return that click was not on a list item
}                              // the highlighed list item





// FUNCTIONS PERTAINING TO TAB 6: TRADE -------------------------------------


/* DRAW HORIZONTAL ROW OF CONTROL BUTTONS ACROSS BOTTOM OF TRADE TAB
   Called from only one place in T6show() and T6click(). */

void T6showHorizButs(){
  showButs();  // show 8 blank buttons across the bottom of the tab area
  const char   // annotate the horizontal row of control buttons
    *A[] = {"CONSU","FABRI","ARTS","CRAFTS","SPARE","SPARE","TAX","HELP"};
  const int 
    a[] = {12,12,15, 9,12,12,18,15},  // horizontal offsets for word starts
    b[] = { 5, 5, 4, 6, 5, 5, 3, 4};  // number of letters in each word
  int h = 17;                         // x coordinate of button block
  for(int i = 0; i < 8; i++) {        // for each of the 8 buttons:
    if(a6 & 1 << i)                   // if this button is bright [on]
      XSetForeground(XD,XG,WHITE);    // set colour for bright lettering
    else
      XSetForeground(XD,XG,GREY);     // set colour for dull lettering
    XDrawString(XD,XW,XG,h + a[i],377,A[i],b[i]);
    h += 59;                          // move across to the next button
  }
}




/* DISPLAY THE VERTICAL COLUMN OF BUTTONS IN THE RAIN SUB-TAB. Called from
   T2showGraph(), T2showRainGraph(), T2showRainTank() */

void T6showVertiButs() {
  char  // annotate the vertical row of control buttons
    *A[] = {"FIND","NEXT","PREV","EDIT","NEW","LIST","STATUS","HELP"};
  int 
    a[] = {15,15,15,15,18,15, 9,15},  // horizontal offsets for word starts
    b[] = { 4, 4, 4, 4, 3, 4, 6, 4},  // number of letters in each word
    h = 99;                           // y coordinate of button block
  for(int i = 0; i < 8; i++) {        // for each of the 8 buttons:
    XSetForeground(XD,XG,DARK);       // shade for buttons' background
    XFillRectangle(XD,XW,XG,17,h-15,53,21);
    if(b6 & 1 << i)                   // if this button is bright [on]
      XSetForeground(XD,XG,WHITE);    // set colour for bright lettering
    else                              // otherwise
      XSetForeground(XD,XG,GREY);     // set colour for dull lettering
    XDrawString(XD,XW,XG,17+a[i],h,A[i],b[i]);
    h += 27;                          // move across to the next button
  }
}




/* TAB 6 EXPOSURE FUNCTION. Called only from 1 place in showTab(). */

void T6status() {
  if(tb != 6) return;  // bail out if TRADE tab not currently visible
  int
    h = 87,            // horizontal start coord of text line 
    v = 117,           // vertical start coord of top text line
    l = 20,            // inter-line drop
    i = 0,             // loop variable
    I = 9,             // loop size
    D[] = {16573,      // total value to date of sales invoices
           13298,      // total value to date of purchase bills
               0,      // profit [calculated herein]
            8000,      // value of work in progress
           20000,      // current account balance
            1150,      // current outstanding debt
           15290,      // current outstanding credit
            1000,      // average spending per day
               0},     // survival time in days [calculated herein]
    d[] = {5,5,5,5,5,5,5,5,3},           // data field lengths
    s[] = {61,61,51,54,44,45,47,45,60};  // text field lengths

  char *S[] ={         // annotation text lines
    "A SALES          +  value of sales invoices so far this year.",
    "B PURCH          -  value of purchase bills so far this year.",
    "C PROFIT         +  profit to date this year [A-B].",
    "D WIP               current value of work in progress.",
    "E BALANCE        +  current account balance.",
    "F DEBT           -  current outstanding debt.",
    "G CREDIT         -  current outstanding credit.",
    "H OUTGOINGS      -  average spending per day.",
    "I SURVIVAL      days survival time on zero income [(E-F)/H].",
  };

  XSetForeground(XD,XG,BLACK);  // show column headings in yellow
  XFillRectangle(XD,XW,XG,17,53,466,270);
  XSetForeground(XD,XG,YELLOW);  // colour for the lettering
  XDrawString(XD,XW,XG,17,70,"TRADE STATUS SUMMARY",20);

  XDrawString(XD,XW,XG,h,95,"VALUE:      "
  "some may still be awaiting materialisation as cash.",63);
  XDrawString(XD,XW,XG,h,207,"CASH:       "
  "in possession [some may be still owed to suppliers].",64);

  XSetForeground(XD,XG,BLEEN);   // colour for the lettering
  for(i = 0; i < I; i++) {       // for each data annotation text line
    XDrawString(XD,XW,XG,h,v,S[i],s[i]);
    if(i == 3) v += 32;          // jump down to first line of CASH data
    v += l;                      // drop down to next data line
  }
  D[2] = D[0] - D[1];            // profit to date this year
  D[8] = (D[4] - D[5]) / D[7];   // survival time on zero future income
  h = 158;                       // horizontal start coord of data fields
  v = 117;                       // vertical start coord of top data field
  XSetForeground(XD,XG,WHITE);   // colour for the lettering
  for(i = 0; i < I; i++) {       // for each data item
    int x = d[i];                // length of data field
    XDrawString(XD,XW,XG,h,v,showInt(D[i],x,1),x);
    if(i == 3) v += 32;          // jump down to first line of CASH data
    v += l;                      // drop down to next data line
  }
  a6 = 1;      // set first sub-tab button bright 
  T6showVertiButs();    // re-display the vertical control buttons
  showMsg(0);  // display blank message field
}




/* GET THE NUMBER OF DAYS THAT HAS ELAPSED SINCE THE INVOICE DATE OR
   THE NUMBER OF DAYS FROM INVOICE TO PAYMENT. The file pointer [handle]
   FD for 'fin.dat' must have been pre-set to point to the invoice's
   DATE field. Called from 1 place each in T6showTrans(), T6transList() and
   T6transSort(). */

int T6getDays(int B) {   // B=start byte of of 32-bit invoice record
  int ET = 0;            // set number of days before payment deadline to zero
  fseek(FD,B,SEEK_SET);  // seek start byte of invoice DATE field
  int id = getInt(FD);   // get INVOICE date
  fseek(FD,B + 24,SEEK_SET);  // seek start byte of payment DATE field
  int pd = getInt(FD);   // get PAID date
  C6 = WHITE;            // set default display colour to white
  if(id == 0) {  // if no invoice date exists [invoice date is zero]
    pd = 0;      // set the payment date to zero
    return 0;    // exit with the elapsed time as zero
  }
  /* Otherwise, an invoice date has been specified, so the invoice is either
  still unpaid or it has already been paid. If the 'date PAID', as specified
  in the transaction record is zero, it means that the invoice is still with-
  in the payment deadline but has not yet been paid. */

  if(pd == 0) {         // if the payment has still not been received
    pd = ratSysDate();  // set the 'DATE PAID' to the current system date
    ET = getET(id,pd);  // compute the elapsed time from invoice date to now

    if(ET < 0)     // if the elapsed time is negative, it means that the
      C6 = GREEN;  // payment was made in advance, so show it in GREEN 
    else
    if(ET < 30)    // if the elapsed time still less than 30 days
      C6 = AMBER;  // show the anticipated payment date in AMBER
    else {         // else the payment of this invoice is overdue
      C6 = RED;    // so show the anticipated payment date in RED
      V6 = 1;      // and set the 'payment alert' flag
    } pd = endDate(id,30);  // compute the anticipated payment date
  }
  /* Otherwise, the payment has already been received, so return the time 
  between the invoice date and the date it was paid. */

  else ET = getET(id,pd);
  return ET;
}




/* DISPLAY THE SCROLLABLE INVOICE LIST. Called only from one place each in
   T6transList() and T6transUpDn().

   fin.dat record format: 32-byte records
   0000 transaction date
   0000 ref number of client in 'people.dat'
   0000 ref number of product in 'crop.dat'
   0000 price 
   0000 quantity
   0000 value
   0000 date paid
   0000 spare */

void T6transScroll() {
  int v = 93,  // vertical coordinate of field
      i;       // main loop variable

  XSetForeground(XD,XG,BLACK);  // clear the list area
  XFillRectangle(XD,XW,XG,86,75,466,220);

  g6 = Scroller(g6,N6);  /* adjust scroll off-set if necessary

      START BYTE OF THE FIRST DATA RECORD TO BE DISPLAYED
      |   scroll off-set of records before the first on to be displayed
      |   |    |    times the number of bytes per record
      |   |    |    |   */
  int B = g6 + 1 << 5;

  int o = o6 - 1;  // one less because the DOCument number is not indexed

  /*  START BYTE OF THE FIRST INDEX RECORD TO BE DISPLAYED
      |
      |     Size of the transactions data section of 'fin.dat'
      |     |  in equivalent 16-byte records.
      |     |
      |     |      index number of the selected list column
      |     |      |    times 256 16-bytes records per index
      |     |      |    |
      |     |      |    |    Scroll off-set: the number of index records 
      |     |      |    |    |  before the first one to be displayed.
      |     |      |    |    |
      |     |      |    |    |    +1 more to get to the 1st displayed line
      |     |      |    |    |    |    all times 16 bytes per index record
      |     |      |    |    |    |    |   */
  int I = 0x200 + (o << 8) + g6 + 1 << 4;
  int R;  // data record number within 'fin.dat'

  for(i = 1; i < 12; i++) {  // for each of the next 11 records
    if(o6 > 0) {             // if displaying in index order
      fseek(FD,I,SEEK_SET);  // seek start byte of [next] index record
      R = fgetc(FD);         // number of required main data record
      B = R << 5;            // start byte of the required main record
    }
    fseek(FD,B,SEEK_SET);  // seek start byte of main data record
    int a = getInt(FD),    // transaction date
        b = getInt(FD),    // transaction type
        c = getInt(FD),    // ref number of client in 'people.dat'
        d = getInt(FD),    // ref number of product in 'crop.dat' 
        e = getInt(FD),    // price
        f = getInt(FD),    // quantity
        g = getInt(FD),    // date paid
        h = g6 + i;        // document number for listing in document order
    if(o6 > 0) h = R;      /* document number when listing in indexed order

            default colour for a line within the transaction list
            |         index record number of currently highlighted list item
            |         |
            |         |     Number of lines in the data or index file prior 
            |         |     | to the first one currently being displayed.
            |         |     |
            |         |     |    Line number of highlighted line within the
            |         |     |    | 11 that are currently visible on screen.
            |         |     |    |        
            |         |     |    |                          */
    int C = BLEEN; if(l6 == g6 + i) { C = WHITE; r6 = h; }  /*
                                          |      |    |
              colour for a highlighted line      |    |
              record number of current data record    |
                document number of line being displayed     */

    XSetForeground(XD,XG,C);  // colour for the data in the list
    XDrawString(XD,XW,XG,89,v,showInt(h,3,1),3);   // DOC number
    XDrawString(XD,XW,XG,113,v,showInt(a,8,1),8);  /* invoice DATE

          file handle for 'nad.dat', which contains the client names
          |   get the client number from the transaction record
          |   |    times 1024 bytes per record
          |   |    |      plus offset of client's short name field
          |   |    |      |              */
    fseek(FA,(c << 10) + 409,SEEK_SET);  // seek short CLIENT NAME
 
    char S[13];             // to hold client or product name
    int K = fgetc(FA), k;   // get the 1st byte of the short CLIENT NAME
    if(K < 0) K = 0;        // invalid line length
    if(K > 12) K = 12;      // upper safety limit
    for(k = 0; k < K; k++)  // for each character of the CLIENT NAME
      S[k] = fgetc(FA);     // get the byte from the disk record
    XDrawString(XD,XW,XG,165,v,S,K);  /* display the CLIENT NAME

          file handle for 'crop.dat', which contains the CROP NAME
          |  get the crop type number from the crop record
          |  |    times 64 bytes per crop record
          |  |    |             */
    fseek(FC,d << 6,SEEK_SET);  // start byte of crop record

    K = fgetc(FC);          // get the first byte of current record
    if(K < 0) K = 0;        // invalid line length
    if(K > 13) K = 13;      // upper safety limit
    for(k = 0; k < K; k++)  // for each character of the product name
      S[k] = fgetc(FC);     // get it from the disk record
    XDrawString(XD,XW,XG,243,v,S,K);  // display the CROP NAME

    XDrawString(XD,XW,XG,327,v,showInt(e,5,1),5);      // show price
    XDrawString(XD,XW,XG,363,v,showInt(f,5,1),5);      // show quantity
    XDrawString(XD,XW,XG,402,v,showInt(e * f,5,1),5);  // show value
    int ET = T6getDays(B);     // get the elapsed time and its display colour
    XSetForeground(XD,XG,C6);  // set display colour for the days to run
    XDrawString(XD,XW,XG,443,v,showInt(ET,4,1),4);

    v += 20;     // drop down to the next line to be displayed
    if(o6 == 0)  // if displaying in order of document number
      B += 32;   // advance to the start byte of the next data record
    else         // else, we're displaying in the order of a column index
      I += 16;   // advance to start byte of next index record
  }
}




/* Colour of the whole line: RED=unpaid, YELLOW = due, GREEN = paid [settled
   The column space at the left is for the scroll bar. */

void T6transList() {
  XSetForeground(XD,XG,BLACK);  // clear the list area of the tab
  XFillRectangle(XD,XW,XG,17,53,466,270);

  // DISPLAY THE COLUMN HEADING BUTTONS
  XSetForeground(XD,XG,YELLOW);  // colour for the lettering
  XDrawString(XD,XW,XG,17,70,"INVOICES:",9);
  XSetForeground(XD,XG,DARK);   // colour for the title buttons
  XFillRectangle(XD,XW,XG, 86,56,22,19);  // DOC button
  XFillRectangle(XD,XW,XG,112,56,49,19);  // DATE button
  XFillRectangle(XD,XW,XG,163,56,75,19);  // NAME button
  XFillRectangle(XD,XW,XG,241,56,81,19);  // PRODUCT button
  XFillRectangle(XD,XW,XG,325,56,33,19);  // PRICE button
  XFillRectangle(XD,XW,XG,361,56,33,19);  // QUANT button
  XFillRectangle(XD,XW,XG,397,56,39,19);  // COST button
  XFillRectangle(XD,XW,XG,439,56,33,19);  // PAID button

  char *S[] = {"DOC","DATE","NAME","PRODUCT","PRICE","QUANT","VALUE","PAID?"};
  int s[] = {3,4,4,7,5,5,5,5};
  int n[] = {89,124,189,261,327,363,402,441};
  int C;
  for(int i = 0; i < 8; i++) {
    if(b6 & 0x100 << i) C = WHITE; else C = GREY;
    XSetForeground(XD,XG,C);  // colour for the lettering
    XDrawString(XD,XW,XG,n[i],70,S[i],s[i]);
  }
  XSetForeground(XD,XG,BLEEN);  // colour for the lettering
  XDrawLine(XD,XW,XG,384,301,430,301);
  XDrawString(XD,XW,XG, 87,317,"TOTAL",5);
  int B = 48, i,             // start byte of PRICE field of Record 1
      T = 0;                 // total value of all invoices
  for(i = 1; i < N6; i++) {  // sum all the costs
    fseek(FD,B,SEEK_SET);    // start byte of price field
    T += getInt(FD) * getInt(FD);  // times the value of the sale or purchase
    B += 32;                 // advance to next record
  }
  XDrawString(XD,XW,XG,384,317,showInt(T,8,1),8);
  T6showVertiButs();
  showMsg(0);
  T6transScroll();
}




/* CLEARS THE [sets to data-box background colour] the INSIDES OF THE
   DATA BOXES OF THE TRADE->TRANS SUB-TAB. Called from T6clearInsides()
   and T6transNEW(), */

void T6clearInsides() { 
  XSetForeground(XD,XG,DARK);  // colour for the display field background
  int d, v = Y6 - 12, x = X6 + 54;
  for(int i = 0; i < 10; i++) {
    XFillRectangle(XD,XW,XG,X6-3,v,53,15);  // paint background
    if(i == 2)                              // if it's a document type
      XFillRectangle(XD,XW,XG,x,v,101,15);  // paint background 16 chars wide
    else if(i > 2 && i < 5)                 // if it's a trader or product
      XFillRectangle(XD,XW,XG,x,v,202,15);  // paint background 32 chars wide
    v += 20;
  }
}




/* FETCH AND DISPLAY A FINANCIAL TRANSACTION RECORD [SALES OR PURCHASE
   INVOICE OR CREDIT NOTE. Called from T6show(), T6horizButClick(). */

void T6showTrans() {
  char        // customer/supplier name in identifier order
    *S[] = {  // field names
      "DOCUMENT NUMBER.","DATE OF ISSUE...","TRANSACTION TYPE",
      "TRADER IDENTITY ","PRODUCT TYPE....","PRICE PER UNIT..",
      "QUANTITY........","COST [TOTAL]....","PAID?...........",
      "DELAY..........."
    },
    *T[] = {  // transaction type in numbered order
      "CANCELLED       ", "SALES INVOICE   ", "PURCHASE INVOICE",
      "SALES CREDIT    ", "PURCHASE CREDIT "
    }, D[32];
  clearTab();                    // clear the tab area
  T6clearInsides();              // clear the insides of all data boxes
  XSetForeground(XD,XG,YELLOW);  // colour for the title lettering
  XDrawString(XD,XW,XG,17,70,
  "FINANCIAL TRANSACTION [SALES/PURCHASE INVOICE OR CREDIT NOTE]",61);

  // DISPLAY THE FIELD NAMES OF THE TRADE DOCUMENT
  XSetForeground(XD,XG,BLEEN);
  int X = 87,  // horizontal start position of the data block
      l = 20,  // inter-line spacing within data block
      y = Y6;  // vertical coordinate of current data line
  for(int i = 0; i < 10; i++) {  // for each data value
    XDrawString(XD,XW,XG,X,y,S[i],16);
    y += l;    // drop down to next data line
  }
  // DISPLAY SUNDRY ANNOTATIONS
  X = X6 + 56; y = Y6 + (l << 2) + l;
  XDrawString(XD,XW,XG,X,y,"VALS/KG",7);
  XDrawString(XD,XW,XG,X,y + l,"KG",2);
  XDrawString(XD,XW,XG,X,y + l + l,"VALS",4);
  XDrawString(XD,XW,XG,X,y + (l << 2),"DAYS",4);
  XDrawString(XD,XW,XG,17,316,"Note: tax can only be levied on"
  " productive gain: never on sales or purchases.",77);

  // GET AND DISPLAY THE TRANSACTION RECORD DATA
  y = Y6 - l;  // vertical coordinate of top line of data block
  XSetForeground(XD,XG,WHITE);  // default colour for display of data items
  int B = r6 << 5;        // start byte of transaction data record
  fseek(FD,B,SEEK_SET);   // start byte of record 'r6'

  int a = getInt(FD),  // trans date
      b = getInt(FD),  // trans type
      c = getInt(FD),  // trader number type
      d = getInt(FD),  // crop type number
      e = getInt(FD),  // price
      f = getInt(FD),  // quantity
      g = getInt(FD),  // date paid

      ET = T6getDays(B);  // elapsed time from invoice date

  XDrawString(XD,XW,XG,X6,y+=l,showInt(r6,8,0),8);  // document number
  XDrawString(XD,XW,XG,X6,y+=l,showInt(a,8,0),8);   // transaction date
  XDrawString(XD,XW,XG,X6,y+=l,showInt(b,8,0),8);   // trans type
  XDrawString(XD,XW,XG,X6,y+=l,showInt(c,8,0),8);   // trader num
  XDrawString(XD,XW,XG,X6,y+=l,showInt(d,8,0),8);   // crop num
  XDrawString(XD,XW,XG,X6,y+=l,showInt(e,8,0),8);   // price
  XDrawString(XD,XW,XG,X6,y+=l,showInt(f,8,0),8);   // quantity
  XDrawString(XD,XW,XG,X6,y+=l,showInt(e * f,8,0),8);  // total cost
  XDrawString(XD,XW,XG,X6,y+=l,showInt(g,8,0),8);   // payment date
  XSetForeground(XD,XG,C6);  // show ELAPSED TIME in appropriate colour
  XDrawString(XD,XW,XG,X6,y+=l,showInt(ET,8,0),8);

  XSetForeground(XD,XG,WHITE);  // colour for the names on the right
  y = Y6 + l;                   // reset to y-coord of text fields

  if(b > -1 && b < 5)  // if record contains a valid transaction type number
    XDrawString(XD,XW,XG,X,y+=l,T[b],16);  // display TRANSACTION TYPE NAME

  if(c > 0 && c < 256) {         // if Trader Ref Num is within valid range
    fseek(FA,c << 10,SEEK_SET);  // first byte of trader's NAD record
    int I = fgetc(FA);           // get length of TRADER'S NAME
    if(I < 0) I = 0;             // make length zero if negative [corrupt]
    if(I > 32) I = 32;           // limit the name length to 32 characters
    for(int i = 0; i < I; i++)   // for each character in TRADER'S NAME
      D[i] = fgetc(FA);          // put character in character array
    XDrawString(XD,XW,XG,X,y+=l,D,I);
  } 
  if(d > 0 && d < N5) {          // if 'e' is a valid crop record number
    fseek(FC,d << 6,SEEK_SET);   // start byte of crop record
    int I = fgetc(FC);           // length of CROP NAME
    if(I < 0) I = 0;             // make length zero if negative [corrupt]
    if(I > 32) I = 32;           // limit the name length to 32 characters
    for(int i = 0; i < I; i++)   // for each character in CROP NAME
      D[i] = fgetc(FC);          // put character in character array
    XDrawString(XD,XW,XG,X,y+=l,D,I);
  }
  XSetForeground(XD,XG,GREY);    // End note
  X += 60; y = Y6 + 100;
  y += 20; XDrawString(XD,XW,XG,X,y,"GREEN: paid within 30 days.",27);
  y += 16; XDrawString(XD,XW,XG,X,y,"AMBER: unpaid but is still ",27);
  y += 16; XDrawString(XD,XW,XG,X,y,"within the 30 day deadline,",27);
  y += 16; XDrawString(XD,XW,XG,X,y,"or paid but paid late. RED:",27);
  y += 16; XDrawString(XD,XW,XG,X,y,"unpaid beyond 30 day limit.",27);
  T6showVertiButs();
  T6showHorizButs();
  showMsg(0);
}




void T6showSpare() {
  char *S[] = {
    "The TRADE tab of this program facilitates only the mode of trade of a",
    "future community-based world of Landshare units, using a non-infating",
    "non-interest-attracting unit of exchange called the 'Val'.           ", 

    "Notwithstanding, for at least a considerable time, Landshare communi-",
    "ties will have to operate within present-day Capitalist and Socialist", 
    "jurisdictions. For this reason, each trading coterie, within the con-",
    "federation of Landshare communities, must become a Limited Liability ",
    "entity within the present-world jurisdiction in which it is located. ", 

    "It is therefore necessary for each Landshare unit to have a financial", 
    "interface with its respective present-world jurisdiction. Such is im-", 
    "plemented within these two SPARE sub-tabs to suit the requirements of",
    "the jurisdiction in which each particular application site lies.     "
  };
  int y = 80;
  XSetForeground(XD,XG,BLEEN);
  for(int i = 0; i < 12; i++) {
    if(i == 3 || i == 8) y+=10;
    XDrawString(XD,XW,XG,40,y+=19,S[i],69);
  }
}




void T6showSubTab1() {
  XSetForeground(XD,XG,YELLOW);  // colour for the lettering
  XDrawString(XD,XW,XG,17,70,"SUB-TAB 1",9);
  char *S[] = {
    "The FABRI sub-tab has exactly the same functionality as the CONSU sub",
    "tab but records the trade of fabricated goods instead of consumables.",
    "However, it must be accounted in a different currency from the Val.  ", 

    "This is because, in Landshare World, fabricated items are not regard-",
    "ed as consumable. Neither are their expendable components. Fabricated", 
    "items are regarded as permanent. Their expendable components are not ",
    "produced on an on-going consumer basis but instead they are made only",
    "as and when they are needed, using a process such as 3-D printing.   ", 

    "This removes the motive to fabricate products with a limited life and", 
    "uses vastly less materials and energy in the long term, which is much", 
    "better for the conservation and health of the planet's biosphere.    "
  };
  int y = 80;
  XSetForeground(XD,XG,BLEEN);
  for(int i = 0; i < 11; i++) {
    if(i == 3 || i == 8) y+=10;
    XDrawString(XD,XW,XG,40,y+=19,S[i],69);
  }
}




void T6showSubTab2() {
  XSetForeground(XD,XG,YELLOW);  // colour for the lettering
  XDrawString(XD,XW,XG,17,70,"SUB-TAB 2",9);
  char *S[] = {
    "Likewise the ARTS sub-tab has the same functionality as the CONSU sub",
    "tab but accounts the exchange of works of art instead of consumables.",
    "The ARTS sub-tab must work in yet another currency of exchange.      ", 

    "This is because, in Landshare World, works of art too aren't regarded",
    "as consumable. Each work or art has a unique and permanent existence.", 
    "Its creator is not dependent on the sale of his works of art in order",
    "to live and survive economically. It is a passtime activity motivated",
    "by his creativity: not by any lust for gain.                         ",

    "This raises the issue of whether or not works of art should be traded", 
    "in a currency at all. Can they not be simply created for the love of ", 
    "the art and given to others as presents?                             "
  };
  int y = 80;
  XSetForeground(XD,XG,BLEEN);
  for(int i = 0; i < 11; i++) {
    if(i == 3 || i == 8) y+=10;
    XDrawString(XD,XW,XG,40,y+=19,S[i],69);
  }
}




void T6showSubTab3() {
  XSetForeground(XD,XG,YELLOW);  // colour for the lettering
  XDrawString(XD,XW,XG,17,70,"SUB-TAB 3",9);
  char *S[] = {
    "Likewise also, the CRAFTS sub-tab contains the same functionality as ",
    "the CONSU sub-tab but exchanges its goods for yet a 4th type of curr-",
    "ency. Crafts include the production of goods like clothes and shoes. ",  

    "Unlike food and fuel, which are of the same form for everybody, and  ",
    "are consumed at a steady rate, crafted items, like shoes and clothes,",
    "should be bespokely crafted for each individual in each instance. The", 
    "present obsession with standardization in production is all too pain-",
    "ful with the impossibility of finding a pair of trowsers that fit.   ",

    "I have never been able to understand why it is not blindingly obvious", 
    "that steady-state on-going consumer economics is systemically incomp-",
    "atible with permanent items and those requiring individualisation.   "
  };
  int y = 80;
  XSetForeground(XD,XG,BLEEN);
  for(int i = 0; i < 11; i++) {
    if(i == 3 || i == 8) y+=10;
    XDrawString(XD,XW,XG,40,y+=19,S[i],69);
  }
}




void T6showSubTab4() {
  XSetForeground(XD,XG,YELLOW);  // colour for the lettering
  XDrawString(XD,XW,XG,17,70,"SUB-TAB 4",9);
  T6showSpare();
}

void T6showSubTab5() {
  XSetForeground(XD,XG,YELLOW);  // colour for the lettering
  XDrawString(XD,XW,XG,17,70,"SUB-TAB 5",9);
  T6showSpare();
}




void T6showSubTab6() {
  XSetForeground(XD,XG,WHITE);  // colour for the lettering
  XDrawString(XD,XW,XG,17, 70,"                 DUE       PAID    ",35);
  XDrawString(XD,XW,XG,17, 90,"TAXES ON PROFIT  00000.00  00000.00",35);
  XDrawString(XD,XW,XG,17,110,"VALUE ADDED TAX  00000.00  00000.00",35);
}




/* SHOW TAB 6 [TRADE] ON INITIAL EXPOSURE AND WHEN RE-EXPOSURE EVENTS OCCUR
   Called only from showTab(). */

void T6show() {
  if(tb != 6) return;    // bail out if TRADE tab not currently visible
  if(a6 &  1) {          // if the TRANS tab were previously on screem
    if(b6 & 0x20)        // if the TRANS->LIST was being displayed
      T6transList();     // re-display the TRANS->LIST sub-sub-tab
    else if(b6 & 0x40)   // else if the TRANS->STATUS was being displayed
      T6status();        // re-display the TRANS->STATUS sub-sub-tab
    else T6showTrans();  // otherwise re-display the normal TRANS sub-tab
  }
  else if(a6 &  2) T6showSubTab1();
  else if(a6 &  4) T6showSubTab2();
  else if(a6 &  8) T6showSubTab3();
  else if(a6 & 16) T6showSubTab4();
  else if(a6 & 32) T6showSubTab5();
  else if(a6 & 64) T6showSubTab6();
  T6showHorizButs();  // display the tab's buttons
}




void T6clearBoxes() {            // CLEAR THE INVOICE DATA BOXES
  int v = 86;                    // y-coord of top of box area
  XSetForeground(XD,XG,BLACK);   // colour to clear all boxes
  for(int i = 0; i < 10; i++) {  // for each field of data
    XDrawRectangle(XD,XW,XG,X6-3,v,53,16);  // clear data box
    v += 20;                     // drop down to next field
  }
}




void T6drawBox(int c) {     // draw box in YELLOW or GREEN
  XSetForeground(XD,XG,c);  /* set the specified colour for the box

        vertical top corner of box to be displayed
                               |     outer box width of field 'f6'
                               |     |        */
  XDrawRectangle(XD,XW,XG,X6-3,ey-13,53,16);  // draw the box
}




void T6unClick() {
  b6 &= ~0xFF;        // dim all buttons
  T6showVertiButs();  // show the new states of the buttons
  T6showTrans();      // re-display the clean transaction record
}




/* BOX A FINANCIAL TRANSACTION FIELD TO EDIT OR UPON WHICH TO SEARCH.
   Called from T6transFieldClick(), T6transUpDn() and T6transNEW(). */

void T6transEdit() {
  if(f6 < 0 || f6 > 9) {  // click below bottom field so kill click and exit
    T6unClick(); return;  // dim buttons, unbox fields and exit
  } ex = X6;              /* x-coord of start of field

       specified screen field number [0 to 13]
       |    times 18 because there are 20 pixels per line
       |    |    base height of lettering for the top field
       |    |    |    */
  ey = f6 * 20 + Y6;  /* base height of lettering for the specified field
  | 
  global variable set for use elsewhere  */

  int i, C = YELLOW;  /* loop variable + box colour for an EDIT

  Set up Line Editor ready for the user to EDIT a previously selected field of
  a transaction record or the first/next field of a NEW transaction record. */

  if(b6 & 8) {    /* IF EDITING AN EXISTING ENTRY:

          file handle for 'fin.dat'
          |   record number of currently displayed transaction
          |   |     multiply by 32 bytes per record
          |   |     |     number of the field being edited
          |   |     |     |     multiply by 4 bytes per field [32-bit integer]
          |   |     |     |     |              */
    fseek(FD,(r6 << 5) + (f6 << 2),SEEK_SET);  /* start byte of selected field

                       load the field content as an integer
                       |     from the transactions file 'fin.dat'
                       |     |   into an 8-character array
                       |     |   | with leading spaces
                       |     |   | |     */
    char *s = showInt(getInt(FD),8,0);   // convert it to character form

    for(i = 0; i < 8; i++)  // copy it into Line Editor array
      le[i] = *(s+i);
    en = 8;                 // set Line Editor content length to 8 characters

  } else {        // ELSE, IT MUST BE A NEW ENTRY OR A FIND
    for(i = 0; i < 8; i++)
      le[i] = 0;  // so clear the Line Editor array
    en = 0;       // and set its content length to zero
    if(b6 & 1) {  // if the FIND button was clicked
      f6 = 1;     // set to box Field 1 [the second field]
      C = GREEN;  // colour for a FIND operation
    } else        // otherwise we must be entering a NEW record
      C = BLU;    // so box its filed in blue
  }
  T6drawBox(C);   // draw coloured box around the field
  ec = 0;         // set line editor's cursor position to start of field
  doCursor(1);    // display the cursor
  ea = 1;         // activate the line editor
}




/* MOUSE CLICKED ON A NON-BUTTON PART OF THE TAB. The click is therefore
   presumed to be an intentional click within a transaction data field.
   Called from 1 place in T6click(). */

void T6transFieldClick() {
  if(!(b6 & 0x18)) return;    // provided the EDIT or NEW button is on
  showMsg(0);                 // clear the message line
  if(mx < 198 || mx > 249) {  // if click wasn't in a data field
    T6unClick(); return;      // dim buttons, unbox fields and exit
  }
  int i, y = 87;              // y-coordinate of top field area
  for(i = 0; i < 10; i++) {   // for each of the 10 data fields
    if(my > y && my < y+14)   // if mouse in vertical limits of this field
      break;                  // break out of loop: i = field number
    y += 20;                  // drop down to the next field
  } if(i > 9) return;         // click was not in one of the 10 fields
  f6 = i;                     // set the current field number
  T6transEdit();              // go edit selected field
}




void T6transUpDn() {    // MOVES TO NEXT OR PREVIOUS FIELD IN TRANS SUB-TAB
  if(b6 & 0x20)         // if the LIST button is on
    T6transScroll();    // scroll the transactions list
  else if(b6 & 0x19) {  // if FIND, EDIT or NEW button on 00011001
    doCursor(0);        // clear the cursor
    do {
          /* if we did a mouse-wheel 'up' or hit an 'up-arrow' key
             |    and on incrementing the field number
             |    |      it overshot the last editable field
             |    |      |           */
    if(mw == 1 && ++f6 > 8) f6 = 0;  // loop back down to the first field
    else
    /*       if we did a mouse-wheel 'down' or hit a 'down-arrow' key
             |    and on decrementing the field number
             |    |      it undershot the first field
             |    |      |           */
    if(mw == 2 && --f6 < 0) f6 = 9;  // loop back up to the last Field

    } while(f6 == 1 || f6 == 7);  // skip fields 1 & 7, which is not editable  

    T6showTrans();      // display blank form with new transaction number
    T6transEdit();  // set up and box the new field
  }
  mw = 0;           // show that the mouse or key up/down operation is done
}




/* THE MOUSE WHEEL WAS MOVED WHILE WITHIN TAB 6
   Called from only one place in MEH() the Mouse Event Handler. */

void T6mouseWheel() {
  if(a6 & 1) T6transUpDn();
}




/* THE ENTER [CARRIAGE-RETURN] KEY HAS BEEN PRESSED AFTER EDITING ONE OF THE
   TRANSACTION RECORD FIELDS. Called from CR(). */

void T6transCR() {
  le[8] = 0;         // terminate the line editor string for atoi()
  int x = atoi(le);  // convert the Line Editor content to an integer
  if(b6 & 1) {       // if we're doing a FIND
    r6 = x;          // set record number as Line Editor content
    b6 &= ~0xFF;     // dim all buttons
    T6showTrans();       // go display the retrieved transaction record
    showMsg(25);     // show 'Transaction record found.' message
  } else {           /* else we're doing an EDIT or NEW, so:

          file handle for 'fin.dat'
          |   record number of currently displayed transaction
          |   |     multiply by 32 bytes per record
          |   |     |     number of the field being edited
          |   |     |     |     multiply by 4 bytes per field [32-bit int]
          |   |     |     |     |              */
    fseek(FD,(r6 << 5) + (f6 << 2),SEEK_SET);  // start byte of clicked field

    putInt(x,FD);     // store the edited content as an integer
    fflush(FD);       // make sure it is physically stored to disk
    T6showTrans();        // re-display the transaction record
    if(b6 & 0x10) {   // if the NEW button is lit
      mw = 1;         // set to do a mouse-down [or down-arrow]
      T6transUpDn();  // do a mouse-down
    } showMsg(23);    // 'the edited field content has been stored.'
  }
}




/* THE NEW BUTTON ON THE TRADE->TRANS SUB-TAB HAS BEEN CLICKED. Finds a new
   vacant transaction record in the transactions file 'fin.dat'. 
   Called only from one place in T6transButClick(). */

void T6transNew() {
  int i, j;                  // loop variables
  if(b6 & 0x10)              // if the NEW button is on [bright]
    T6unClick();             // dim all buttons and unbox all fields
  else {                     // else the NEW button is off [dim], so
    b6 &= ~0xFF;             // dim all the vertical TRANS buttons
    b6 |= 0x10;              // and light the NEW button
    if(N6 < 255) {           // if unused records still exist in 'fin.dat'
      fseek(FD,0,SEEK_SET);  // seek byte zero of 'fin.dat'
      fputc(++N6,FD);        // put incremented number of transaction records
    } else {                 // otherwise 'fin.dat' is full [N6 = 255], so:
      char s[32];            // for content of one transaction record
      int B = 64;            // start byte of Record 2 of 'fin.dat'

      for(i = 2; i < 256; i++) {  // for records 2 to 255 of 'fin.dat'
        fseek(FD,B,SEEK_SET);     // seek the start byte of the record
        for(j = 0; j < 32; j++)   // get the record's 32 bytes of content
          s[j] = fgetc(FD);
        fseek(FD,B-32,SEEK_SET);  // seek the start byte of the record below
        for(j = 0; j < 32; j++)   // and store the content in that record
          fputc(s[j],FD);
        B += 32;             // then advance to start byte of next record
      }
    }
    fseek(FD,N6 << 5,SEEK_SET);   // seek the start byte of the new record
    for(i = 0; i < 32; i++)       // set all of its 32 bytes to zero
      fputc(0,FD);
    fflush(FD);     // make sure the new state of 'fin.dat' is on disk
    r6 = N6;        // make the highest occupied record the current record
    f6 = 0;         // set to edit the first field of the new record
    T6showTrans();      // display a clear form with transaction number only
    T6transEdit();  // draw yellow box around selected field and edit it
  }
  T6showVertiButs();    // to show the new state of the NEW button
}




/* MOUSE CLICK HANDLER FOR TAB 6. Used to determine which button in the vert-
   ical column was clicked. Called only from T6click() below. */

int T6transButClick() {
  if(mx < 17 || mx > 69)
    return(0);               // exit if outside horizontal buttons limits
  int i, y = 84;             // height [y-coord] of top of top button
  for(i = 0; i < 8; i++) {   // for each of the 8 vertical buttons
    if(my > y && my < y+21)  // if mouse within vertical limits of this
      break;                 // button, break out of loop: i = button number
    y += 27;                 // drop down to the next button
  } if(i > 7) return(0);     // click was above top or below bottom button

  switch(i) {             // SWITCH ON BUTTON NUMBER 0 TO 7

    case 0:               // FIND key was clicked
      if(!(b6 & 0x60)) {  // provided the LIST and STATUS buttons are off
        T6clearBoxes();   // clear any other yellow box that may exist
        b6 &= ~0x18;      // dim the FIND and EDIT buttons ~00011000
        if(b6 & 1)        // the FIND button is bright, so
          T6unClick();    // dim ll buttons and unbox all fields
        else {            // else the FIND button off [dim]
          b6 |= 1;        // so switch it on [make it bright]
          f6 = 1;         // set the current field number to Field 1
          T6transEdit();  // go edit selected field
        } T6showVertiButs();  // to show the new state of the EDIT button
      } break;

    case 1:               // NEXT key was clicked
      if(!(b6 & 0x60)) {  // provided the LIST and STATUS buttons are off
        b6 &= ~0x19;      // dim FIND, EDIT and NEW buttons ~00011001
        if(r6 < N6) r6++; // increment record number if less than highest
        else r6 = 1;      // otherwise wrap back down to Record 1
        T6showTrans();    // re-display the transaction record
      } break;

    case 2:               // PREV key was clicked
      if(!(b6 & 0x60)) {  // provided the LIST and STATUS buttons are off
        b6 &= ~0x19;      // dim the FIND, EDIT and NEW buttons ~00011001
        if(r6 > 1) r6--;  // decrement record number if greater than 1
        else r6 = N6;     // else wrap back up to the top of the file
        T6showTrans();    // re-display the transaction record
      } break;

    case 3:               // EDIT key was clicked
      if(!(b6 & 0x60)) {  // provided the LIST and STATUS buttons are off
        T6clearBoxes();   // clear any other yellow box that may exist
        b6 &= ~0x11;      // dim FIND and NEW buttons ~00010001
        if(b6 & 8)        // if the EDIT button on
          T6unClick();    // dim all buttons and unbox all fields
        else b6 |= 8;     // else EDIT button is off, so switch it on
        T6showVertiButs();  // to show the new state of the EDIT button
      } break;

    case 4:
      if(!(b6 & 0x60))    // provided the LIST and STATUS buttons are off
        T6transNew();     // the NEW key was clicked
      break;

    case 5:               // the LIST button was clicked
      if(!(b6 & 0x19)) {  // provided the FIND, EDIT and NEW buttons are off
        b6 &= ~0x59;      // dim FIND, EDIT, NEW and STATUS buttons ~01011001
        if(b6 & 0x20) {   // if the LIST button is bright
          b6 &= ~0x20;    // make it dim
          T6showTrans();      // and re-display the transaction record
        } else {          // else the LIST button is dim, so
          b6 |= 0x20;     // brighten the LIST button
          T6transList();  // display the transaction list
        }} break;

    case 6:               // The STATUS button was clicked
      if(!(b6 & 0x19)) {  // provided the FIND, EDIT and NEW buttons are off
        b6 &= ~0x39;      // dim the FIND, EDIT, NEW & LIST buttons ~00111001
        if(b6 & 0x40) {   // if the STATUS button is bright
          b6 &= ~0x40;    // make it dim
          T6showTrans();      // and re-display the transaction record
        } else {          // else the STATUS button is dim, so
          b6 |= 0x40;     // brighten the STATUS button
          T6status();     // display the status information
        }} break;

    case 7:     // the HELP key was clicked
      helpPage("software/fep.html#consu");

  } return(1);  // return that a button was clicked
}




/* STORE THE SEARCH TERM IN THE APPROPRIATE INDEX RECORD'S SEARCH TERM FIELD
   Called 3 times from T6transSort() below. */

void T6transPutIdx(char *s, int i, int I) {
  fseek(FD,I,SEEK_SET);        // start byte of next index record
  fputc(i,FD);                 // put main record number in index record
  fputc(C6,FD);                // put colour [not used for content length]
  for(int k = 0; k < 14; k++)  // put character array in the search term
    fputc(*(s+k),FD);          // field of the index record
}




void T6putIdxRec(FILE *F, int B, int i, int I) {
  char S[15];
  fseek(F,B,SEEK_SET);    // seek short CLIENT NAME
  int K = fgetc(F), k;    // get 1st byte content length [unsigned char]
  if(K > 14) K = 14;      // limit the name length to 14 characters
  for(k = 0; k < K; k++)  // for each character of the CLIENT NAME
    S[k] = fgetc(F);      // get the byte from the disk record
  while(k < 14) {         // if there is less than 14 characters in
    S[k] = 0; k++;        // the name, fill in the remaining characters
  }                       // up to 14 with null characters
  T6transPutIdx(S,i,I);   // store it in the index record
}




/* SORT THE INDEX FILE CORRESPONDING TO FIELD HEADING 'o6'. NOTE: this func-
   tion is not entered if o6=1 DOC. Called only by T6transSet(0).

   'o6' is the column heading number 0 to 7 [including the DOC column o6=1].
   But only 6 of the columns are indexed. DOC is simply the data record number
   in 'fin.dat'. To get the index number in the 'crop.idx' file, we have to
   miss out the DOC column o6=1 and make NAME [o6=2] the 2nd index as follows: 

   FORMAT OF TRANSACTION DATA SECTION OF 'fin.dat'
   o6  o OS BYTE [OS=offset in bytes of 
   00 -- 04 0000 document number [not indexed]
   01 00 00 0000 transaction date
   02 01 08 0000 ref number of client in 'nad.dat'
   03 02 12 0000 ref number of product in 'crop.dat'
   04 03 16 0000 price 
   05 04 20 0000 quantity
   06 05 24 0000 value
   07 06 30 0000 date paid

    int a = getInt(FD),    // transaction date
        b = getInt(FD),    // transaction type
        c = getInt(FD),    // ref number of client in 'nad.dat'
        d = getInt(FD),    // ref number of product in 'crop.dat' 
        e = getInt(FD),    // price
        f = getInt(FD),    // quantity
        g = getInt(FD);    // date paid


   FORMAT OF THE 7 TRANSACTION INDEXES SECTION OF 'fin.dat'
   1) DATE, 2) NAME, 3) PRODUCT, 4) PRICE, 5) QUANT, 6) VALUE, 7) PAID
   0  transaction record reference [16 bytes each]
   0  content length [but used in this case cor filed display colour]
   AAAAAAAAAAAAAA  TEXTUAL CONTENT */

void T6transSort() {
  showMsg(20);     // show the 'sorting' message
  int o = o6 - 1,  // index number [0 to 6]
      x,
      W[] = {0,2,3,4,5,4,6},  /* number of data field required

      START BYTE OF THE REQUIRED DATA FIELD IN 'Record 1' of 'fin.dat'
      |     start byte of Record 1 of the transactions data section
      |     |  [each transaction record has 32 [0x20] bytes]
      |     |     number of the data column within the LIST
      |     |     | index number of the index within 'fin.dat' to be sorted
      |     |     | |     times 4 to get start byte of required field
      |     |     | |     |    */
      B = 0x20 + (W[o] << 2),  /*

      START BYTE OF THE CURRENT INDEX 'o' WITHIN 'fin.dat'
      |     skip over the 256 32-byte records of the transaction data section
      |     | plus a further 16 bytes to start of 'Record 1' of first index
      |     | |     index number of the index to be sorted
      |     | |     |    times 4096 bytes per index [256 16-byte records]
      |     | |     |    |    */
      I = 0x2010 + (o << 12),

      i;  // loop variable: index record number within selected index

  for(i = 1; i <= N6; i++) {  // BUILD THE UNSORTED INDEX FILE
    fseek(FD,B,SEEK_SET);     // start byte of field in next data record
    switch(o) {               // switch on index number 0,1,2,3,4,5,6

      case 0: case 3: case 4:  /* DATE, PRICE, QUANT or VALUE

               store in the index record of start byte 'I'
               |          as a string of numeric characters
               |          |      the integer value in the field
               |          |      |   from the current 'fin.dat' data record
               |          |      |   |   as a 14 character string
               |          |      |   |   |  with leading spaces
               |          |      |   |   |  |  number of fin.dat data record
               |          |      |   |   |  |  | start byte of index record
               |          |      |   |   |  |  | |           */
        T6transPutIdx(showInt(getInt(FD),14,0),i,I);
        break;

      case 1: /* CLIENT NAME
                     file handle for 'nad.dat' [contains client names]
                     |      get client number from transaction record
                     |      |   file handle for transactions file 'fin.dat'
                     |      |   |      times 1024 bytes per record
                     |      |   |      |     search NAME field offset
                     |      |   |      |     |   data record number
                     |      |   |      |     |   | index record start byte
                     |      |   |      |     |   | |           */
         T6putIdxRec(FA,(getInt(FD) << 10) + 409,i,I);
         break;

      case 2:  /* PRODUCT NAME
                    file handle for 'crop.dat' [contains CROP NAME]
                    |     get the crop type number from the crop record
                    |     |   handle for transactions file 'fin.dat'
                    |     |   |      times 64 bytes per record
                    |     |   |      | number of 'fin.dat' data record
                    |     |   |      | | start byte of the index record
                    |     |   |      | | |           */
        T6putIdxRec(FC,getInt(FD) << 6,i,I);
        break;

      case 5:  // VALUE
        x = getInt(FD) * getInt(FD);  // value = price x quantity
        T6transPutIdx(showInt(x,14,0),i,I);  // see case 0:
        break;

      case 6:  /* PAID?
        If the elapsed time 'x' is negative [i.e. the payment has been made
        in advance]then make it into a negative 10s complement of the value.
        This will cause it to fall into the correct casting order for neg-
        ative elapsed times when they are formed into a sign + 3 digit string
        to put in the index record in place of the actual value.
            |
            |             number of current 'fin.dat' data record
            |             |    times 32 to get its start byte
            |             |    |   */
        if((x = T6getDays(i << 5)) < 0) x = -(x + 999);  /*

        14-char numeric with sign  with leading zeros
                   elapsed time |     number of 'fin.dat' data record
                              | |  |  | start byte of the index record
                              | |  |  | |        */
        T6transPutIdx(showInt(x,14,1),i,I);
        break;
    }
    I += 16;  // advance to start byte of next index record in 'fin.dat'
    B += 32;  // advance to start byte of next data record in 'fin.dat'
  }
  FS = FD;    /* set file handle for the Hoare sort
  
        number of equivalent 16-byte records in data section of 'fin.dat'
        |      number 0 to 6 of the index being sorted
        |      |    times 256 16-byte records per index
        |      |    |    */
  i = 0x200 + (o << 8);  /* record zero of the index to be sorted

           number of the first included record of the sort range
           |   number of the last included record in the sort range
           |   |      */
  idxSort(i+1,i+N6);  // sort the newly-built index file
  fflush(FD);         // make sure everything is written to disk
}




/* REGISTER THAT A TRANSACTION FIELD HAS BEEN MODIFIED [EDITED OR NEW]. The
   'field modified' bits are stored in Byte 34 of 'fin.dat'. A field's
   'modified' bit is set every time a field's content is new or has been ed-
   ited, indicating that its corresponding index field needs to be re-sorted.
   A field's 'modified' bit is unset every time the index file for that field
   has just been re-sorted. Called from T6cropListClick(), T6transNameSave(),
   T6cropCR().

   o6=7  PAID?                            When a bit is set, it means that
   o6=6  |VALUE                           the corresponding column's index 
   06=5  ||QUANTITY kilograms             needs to be sorted before being
   06=4  |||PRICE per kilogram            displayed. If the bit is not set,
   06=3  ||||PRODUCT [crop]               the column's index doesn't need
   06=2  |||||NAME of client              resorting so can be displayed
   06=1  ||||||DATE of transaction
   06=0  |||||||DOCument number [not indexed]
         ||||||||
   W6 =  11111110  stored in Byte 3 of record zero of 'fin.dat' */

void T6transSet(int s) {  // s=0 unset bit; s=1 set bit; 3=sort if bit set
  if(o6 == 0) return;     // DOCument number does not have an index file
  int d = 1 << o6;        // set bit in position corresponding to field 'o6'
  switch(s) {
    case 0: W6 &= ~d; break;  // unset the 'field modified' bit
    case 1: W6 |= d; break;   // set the 'field modified' bit
    case 2:
      T6transSort();  // sort if 'field modified' bit set
      T6transSet(0);  // re-enter to unset the field's 'field modified' bit
      return;         // return to re-enter this function with s=0
  }
  fseek(FD,3,SEEK_SET);  // seek byte 3 of 'fin.dat'
  fputc(W6,FD);          // set bit for modified field and restore the byte
  fflush(FD);            // make sure it is actually put to disk
}




/* HANDLES MOUSE CLICKS ON THE TRANSACTIONS LIST COLUMN HEADINGS
   Called from only one place in T6click(). */

void T6transListClick() {
  int i = -1;
  if(my > 54 && my < 76) {                // headings buttons height limits
         if(mx >  85 && mx < 108) i = 0;  // DOC heading clicked
    else if(mx > 111 && mx < 160) i = 1;  // DATE heading clicked
    else if(mx > 162 && mx < 238) i = 2;  // CLIENT NAME heading clicked
    else if(mx > 240 && mx < 322) i = 3;  // CROP NAME heading clicked
    else if(mx > 324 && mx < 358) i = 4;  // PRICE heading clicked
    else if(mx > 360 && mx < 394) i = 5;  // QUANTITY heading clicked
    else if(mx > 396 && mx < 436) i = 6;  // VALUE heading clicked
    else if(mx > 438 && mx < 472) i = 7;  // PAID? heading clicked

    if(i != -1) {     // provided the click was on a heading button
      l6 = 0;         // clear index number of highlighted item
      b6 &= ~0xFF00;  // switch off all the heading column highlighting
      switch(i) {
        case 0: if(!(b6 & 0x0100)) { b6 |= 0x0100; } break;  // DATE
        case 1: if(!(b6 & 0x0200)) { b6 |= 0x0200; } break;  // DOC number
        case 2: if(!(b6 & 0x0400)) { b6 |= 0x0400; } break;  // CLIENT
        case 3: if(!(b6 & 0x0800)) { b6 |= 0x0800; } break;  // PRODUCT
        case 4: if(!(b6 & 0x1000)) { b6 |= 0x1000; } break;  // PRICE
        case 5: if(!(b6 & 0x2000)) { b6 |= 0x2000; } break;  // QUANTITY
        case 6: if(!(b6 & 0x4000)) { b6 |= 0x4000; } break;  // VALUE
        case 7: if(!(b6 & 0x8000)) { b6 |= 0x8000; }         // PAID?
      }
      o6 = i;  // only update o6 if a column heading has actually been clicked
    }
  }
  /* Else the click could have been on one of the 11 list items. So if indeed
     the click was on one of the list items, compute the record number 'lo',
     within the index file, of the highlighted item. */

  else if((i = listClick()) > 0) l6 = i + g6;
  T6transSet(2);   // sort index if field o6's 'field modified' bit is set
  T6transList();   // list the [sorted] crops
}




/* MOUSE CLICK HANDLER FOR TAB 6. Deals with clicks within the TRADE tab.
   Used to determine which button in the vertical column was clicked
   or which of the invoice data fields was clicked.
   Called from only one place in the Mouse Events Handler MEH().*/

void T6click() {
  if(tb != 6) return;    // safety exit
  if(!(a6 & 1)) return;  // exit if not in the TRANS sub-tab
  if(T6transButClick()) return;
  if(b6 & 0x20)          // if the LIST button is on
    T6transListClick();
  else                   // else must be in normal TRADE->TRANS sub-tab
    T6transFieldClick();
}




/* HANDLES A 'PgUp' OR 'PgDp' KEY STROKE. Eleven lines of the CROPS list is
   displayed on the screen. A 'PgDn' keystroke causes the list to advance
   by only 10 lines, leaving what was the bottom line of the list as its new
   top line. This gives better user confidence and sense of continuity. */

void T6PgUpDn(int s) {       // 's' = +10 for PgDn and -10 for PgUp
  if(a6 & 1 && b6 & 0x20) {  // switch on button logic
    g6 += s;                 // increment/decrement mouse bias by 10 lines
    if(s < 0) {           // if it was a 'Page-Up' key-stroke
      if(g6 < 0) g6 = 0;  // if undershot beginning of list set to beginning
    } else {              // if it was a 'Page-Down' key-stroke
      int y = N6 - 11;    // if we've overshot the end of the list
      if(g6 > y) g6 = y;  // set to display the final 11 lines
    } T6transScroll();    // re-display the diary list
  }
}




void T6transEsc() {  // HANDLES WHEN THE 'Esc' KEY HAS BEEN PRESSED IN TAB 6
  T6showTrans();     // re-display the transaction record
  showMsg(24);       // 'Editing of the field has been cancelled'.
}




/* HANDLES CLICKS TO THE 8 BOTTOM BUTTONS OF THE TRADE TAB [TAB 6]
   Called from only one place in MEH(). */

void T6horizButClick(int i) {  // called from only 1 place in MEH()
  clearTab(); 
  a6 &= ~0xFF;  // kill all buttons
  switch(i) {
    case 0: if(!(a6 &    1)) { a6 |=    1; T6showTrans();   } break;  // TRANS
    case 1: if(!(a6 &    2)) { a6 |=    2; T6showSubTab1(); } break;  // SPARE
    case 2: if(!(a6 &    4)) { a6 |=    4; T6showSubTab2(); } break;  // SPARE
    case 3: if(!(a6 &    8)) { a6 |=    8; T6showSubTab3(); } break;  // SPARE
    case 4: if(!(a6 & 0x10)) { a6 |= 0x10; T6showSubTab4(); } break;  // SPARE
    case 5: if(!(a6 & 0x20)) { a6 |= 0x20; T6showSubTab5(); } break;  // SPARE
    case 6: if(!(a6 & 0x40)) { a6 |= 0x40; T6showSubTab6(); } break;  // TAXES
    case 7: if(!(a6 & 0x80)) { a6 |= 0x80;                            // HELP
      helpPage("software/fep.html#trade");
    }
  } T6showHorizButs();  // re-display the buttons
}





/* FUNCTIONS PERTAINING TO TAB 5: PROJECTS -----------------------------------


/* DISPLAY THE VERTICAL COLUMN OF BUTTONS IN THE CROPS SUB-TAB. Called from
   T5crops(), T5cropList(), T5cropButClick(). */

void T5cropShowButs() {  // below are the names of the button annotations
  char *A[] = {"FIND","TRAWL","NEXT","PREV","NEW","KILL","LIST","HELP"};
  int c,
    a[] = {15,12,15,15,18,15,15,15},  // horizontal offsets for word starts
    b[] = { 4, 5, 4, 4, 3, 4, 4, 4},  // number of letters in each word
    h = 99;                           // y coordinate of button block
  for(int i = 0; i < 8; i++) {        // for each of the 8 buttons:
    XSetForeground(XD,XG,DARK);       // shade for buttons' background
    XFillRectangle(XD,XW,XG,17,h-15,53,21);

    if(b5 & 1 << i)   // If this button is currently 'on'
      c = WHITE;      // make its lettering bright WHITE
    else              // otherwise:
    if(i == 6 && V5)  // if it is the LIST button and the 'crop alert'
      c = RED;        // flag is set, make its lettering bright RED
    else c = GREY;    // otherwise make its lettering GREY.

    XSetForeground(XD,XG,c);  // set the lettering colour
    XDrawString(XD,XW,XG,17+a[i],h,A[i],b[i]);
    h += 27;                  // move across to the next button
  }
}




/* CLEAR THE CROP DATA BOXES. Called by T5crops(), T5cropFind(),
   T5cropTrawl(), T5cropNew(), T5cropFieldClick(). */

void T5cropBoxesClear() {
  int v = 66;                  // y-coord of top of box area
  XSetForeground(XD,XG,DARK);  // colour to clear all boxes
  for(int i = 0; i < 10; i++)  // for each field of data
    XDrawRectangle(XD,XW,XG,X6-3,v+=20,53,16);  // clear data box
  XDrawRectangle(XD,XW,XG,X5-3,86,191,16);  // clear product name box
}




/* CLEAR THE INSIDES OF ALL CROP DATA BOXES. Called from one place each in
   T5crops(), T5cropFind, T5cropTrawl() and T5cropNew(). */

void T5cropFieldsClear() {
  int v = 67;
  XSetForeground(XD,XG,DARK);  // colour for the display field background
  for(int i = 0; i < 10; i++)  // for each field of data
    XFillRectangle(XD,XW,XG,X6-1,v+=20,51,15);  // clear data box
  XFillRectangle(XD,XW,XG,X5-2,87,190,15);  // clear crop name box
}




/* CLEAR INSIDE OF A GIVEN CROP DATA BOX 'f5'. Called from one place each in
   T5cropCR() and T5cropEsc().  */

void T5cropFieldClear() {
  int v = 87;                  // y-coord of top of box area
  XSetForeground(XD,XG,DARK);  // colour to clear all boxes
  if(f5 > 0 || b5 & 1)
    XFillRectangle(XD,XW,XG,X6-1,v += f5 * 20,51,15);  // clear data box
  else
    XFillRectangle(XD,XW,XG,X5-2,87,190,15);  // clear crop name box
}




/* SHOW THE CROP EDIT BOX. Called from 1 place each in T5cropFind(),
   T5cropTrawl(), T5cropNew(), T5cropFieldClick(), T5cropCR(),
   T5cropEsc(). */

void T5cropShowBox(int b, int c) {
  int v = 86;               // y-coord of top of box area
  XSetForeground(XD,XG,c);  // colour to clear all boxes
  if(b == 0 && !(b5 & 1))   // if it's the product name field
    XDrawRectangle(XD,XW,XG,X5-3,86,191,16);  // show crop name box
  else
    XDrawRectangle(XD,XW,XG,X6-3,v += 20 * b,53,16);  // show data box
}




/* DRAW HORIZONTAL ROW OF CONTROL BUTTONS ACROSS BOTTOM OF PROJS TAB
   Called from 1 place each in T5crops(), T5cropList(), T5horizButClick()
   and T5show(). */

void T5showButs() {
  if(tb != 5) return;  // bail out if PRODS tab not currently visible
  showButs();  // show 8 blank buttons across the bottom of the tab area
  const char   // annotate the horizontal row of control buttons
    *A[] = {"CROPS","ANIMALS","FABRI","R&D","CRAFT","ARTS","SPARE","HELP"};
  const int 
    a[] = {12, 6,12,18,12,15,12,15},  // horizontal offsets for word starts
    b[] = { 5, 7, 5, 3, 5, 4, 5, 4};  // number of letters in each word
  int h = 17, c;                      // x coordinate of button block
  for(int i = 0; i < 8; i++) {        // for each of the 8 buttons:

    if(a5 & 1 << i)   // if this button is 'on'
      c = WHITE;      // make it lettering bright WHITE
    else              // otherwise
    if(i == 0 && V5)  // if it is the CROPS button and the 'crops alert'
      c = RED;        // flag is set, make its letting RED
    else c = GREY;    // otherwise make its lettering GREY

    XSetForeground(XD,XG,c);  // colour for the lettering
    XDrawString(XD,XW,XG,h + a[i],377,A[i],b[i]);
    h += 59;  // move across to next button
  }
}




/* GET THE NUMBER OF DAYS THAT HAS ELAPSED SINCE PLANTING OR THE NUMBER OF
   DAYS FROM PLANTING TO HARVEST. The file pointer [handle] for crop.dat
   must have been pre-set to point to the crop's DATE PLANTED field.
   Called from 1 place each in T5crops() & T5cropList() and from two
   places in T5cropSort(). */

void T5getDays(int B) {  // B=start byte of main crop record
  fseek(FC,B+44,SEEK_SET);
  c5 = getInt(FC);  // Bytes 44 to 47 PLANTED date
  d5 = getInt(FC);  // Bytes 48 to 51 number of days needed to grow the crop
  e5 = getInt(FC);  // Bytes 52 to 55 date HARVESTED
  j5 = 0;           // set number of days before harvest to zero
  C5 = WHITE;       // set display colour to white
  if(c5 == 0) {     // if no crop has been planted yet [plant date is zero]
    e5 = 0;         // set the harvest date to zero
    return;         // exit with the elapsed time 'j5' as zero
  }
  /* Otherwise, a planting date has been specified so the crop is either still
  growing in the field or it has already been harvested. 

  If the 'DATE HARVESTED' as specified in the crop record is zero, it means
  that the crop is still growing: it has not yet been harvested. */

  if(e5 == 0) {           // if the specified harvest date is zero
    e5 = ratSysDate();    // set 'DATE HARVESTED' to the current system date
    j5 = getET(c5,e5);    // compute the elapsed time from planting to now

    if(j5 < d5)    // if elapsed time still less than required growing time
      C5 = AMBER;  // show anticipated harvest date in AMBER
    else {         // else we're late with harvesting this crop
      C5 = RED;    // so show the anticipated harvest date in RED
      V5 = 1;      // and set the 'crop alert' flag
    }
    e5 = endDate(c5,d5);  // compute the anticipated harvest date
  }
  /* Otherwise, the crop has already been harvested, so return the time 
  between the planting date and the date it was harvested. */

  else j5 = getET(c5,e5);
}




/* Product data is stored in 'crop.dat', which has 256 records of 64 bytes.
   The file is opened for random read/write at program start and closed at
   program termination. Called from 2 places in T5trawl(), and from one place
   each in T5cropNext(), T5cropPrev(), T5cropButClick(), T5cropFieldClick(),
   T5horizButClick(), T5cropCR(), T5cropEsc(), T5show().

 0 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Crop name [length+content] 32 bytes max
 1 IIII [4-byte integer]            Number of units in stock
 2 IIII                             Number of units growing
 3 IIII                             Location [field]
 4 IIII                             Date when production or growing started
 5 IIII                             Number of days production or growing time
 6 IIII                             Date harvested
 9 IIII                             exchange value
   IIII                             unallocated */

void T5crops() {
  char
    *M[] = {                                   // f5
      "                                    ",  // 0    after-notes
      "UNITS [KILOGRAMS]                   ",  // 1
      "UNITS [ANTICIPATED KILOGRAMS]       ",  // 2
      "FIELD OR INDOOR PLANTING FACILITY   ",  // 3
      "ZERO MEANS NOT YET PLANTED/FLOWERED ",  // 4
      "DAYS [ANTICIPATED GROWING TIME]     ",  // 5
      "DAYS [TOTAL TIME TAKEN IF HARVESTED]",  // 6
      "DAYS TO GO TO ANTICIPATED HARVEST   ",  // 7
      "AMBER MEANS ANTICIPATED HARVEST DATE",  // 8
      "VALS/KG PRICE FOR INTERNAL TRADING  "}, // 9
    *N[] = {                                   // field names
      "CROP NAME        ","QUANTITY IN STOCK", // f5=0,1
      "QUANTITY GROWING ","LOCATION [FIELD] ", //    2,3
      "PLANTED/FLOWERED ","GROWING TIME     ", //    4,5
      "TIME ELAPSED     ","TIME REMAINING   ", //    6,7
      "DATE HARVESTED   ","EXCHANGE VALUE   "  //    8,9
    },
    S[32];       // to hold the product name
  int
    I = 10, i,   // loop control variables
    l = 20,      // inter-line drop [leading]
    Y = Y6 - l,  // set height base to first line
    y = Y;       // set current line to first line
  clearTab();    // clear the whole tab area to BLACK
  showMsg(0);    // display a blank message field
  XSetForeground(XD,XG,YELLOW); // set colour for bright lettering
  XDrawString(XD,XW,XG,17,70,"CROP DETAILS",12);
  T5cropBoxesClear();
  T5cropFieldsClear();
  XSetForeground(XD,XG,BLEEN);      // set colour for bright lettering
  XDrawString(XD,XW,XG,X6,70,"CLICK INSIDE A FIELD TO EDIT IT",31);
  XDrawString(XD,XW,XG,87,309,
    "PRESS 'Enter' TO SAVE NEW CONTENT OR 'Esc' TO  REVERT TO ORIGINAL",65);
  for(int i = 0; i < I; i++)        // display the product FIELD NAMES
    XDrawString(XD,XW,XG,87,y+=l,N[i],17);
  y = Y;                            // reset current line to first line
  for(int i = 0; i < I; i++)        // display the AFTER NOTES
    XDrawString(XD,XW,XG,X5,y+=l,M[i],36);
  XSetForeground(XD,XG,WHITE);      // set colour for bright lettering
  fseek(FC,q5 = r5 << 6,SEEK_SET);  // seek first byte of record 'r5'
  XDrawString(XD,XW,XG,X6,Y6,showInt(r5,8,0),8);  // display CROP NUMBER
  y = Y6;                           // set base line to 'line' before 1st line
  I = fgetc(FC);                    // get the first byte of current record
  if(I < 0) I = 0;                  // invalid line length
  if(I > 31) I = 31;                // upper safety limit
  for(int i = 0; i < I; i++)        // for each character of the product name
    S[i] = fgetc(FC);               // get it from the disk record
  XDrawString(XD,XW,XG,X5,y,S,I);   // display PRODUCT NAME

  /* The crop name field [above] is a variable length field, consequently,
     we need to seek the start byte of the crop's numeric data which starts
     at byte 32, which is half way through the 64-byte crop data record. */

  fseek(FC,q5 + 32,SEEK_SET);  // seek first byte of numeric data

  XDrawString(XD,XW,XG,X6,y+=l,showInt(getInt(FC),8,0),8);  // in stock
  XDrawString(XD,XW,XG,X6,y+=l,showInt(getInt(FC),8,0),8);  // growing
  XDrawString(XD,XW,XG,X6,y+=l,showInt(getInt(FC),8,0),8);  // location

  T5getDays(q5);  // get anticipated harvest date & time remaining to harvest

  XDrawString(XD,XW,XG,X6,y+=l,showInt(c5,8,0),8);  // plant date
  XDrawString(XD,XW,XG,X6,y+=l,showInt(d5,8,0),8);  // days required
  XDrawString(XD,XW,XG,X6,y+=l,showInt(j5,8,0),8);  // days gone
  XDrawString(XD,XW,XG,X6,y+=l,showInt(d5-j5,8,0),8);  // days remaining
  XSetForeground(XD,XG,C5);  // C5 can be can be WHITE, AMBER or RED
  XDrawString(XD,XW,XG,X6,y+=l,showInt(e5,8,0),8);  // harvest date
  XSetForeground(XD,XG,WHITE);  // set colour for bright lettering
  XDrawString(XD,XW,XG,X6,y+=l,showInt(getInt(FC),8,0),8);  // price
  T5cropShowButs();  // display the vertical column of control buttons
  T5showButs();      // display the bottom row of buttons
}




void T5text() {
  char *S[] = {
    "This sub-tab is not implemented in this demonstration  ",
    "version of the program. However, it's functionality is,",
    "for the most part, essentially the same as that of the ",
    "CROPS sub-tab. The names of the fields and some of the ",
    "interactions between them are, of course, different to ",
    "suit the context of this sub-tab. Implementation of it ",
    "is thus fairly straight-forward should it be required  ",
    "for any particular site.                               "
  };
  int y = 110;
  XSetForeground(XD,XG,BLEEN);
  for(int i = 0; i < 8; i++)
    XDrawString(XD,XW,XG,80,y+=19,S[i],55);
}





/* DISPLAY THE TITLES OF THE PROJECTS TABS. Note that all the following
   sub-tabs are not yet implemented and will only be implemented as and
   when required. Called from only 1 place each in showTab(). */

void T5anims() {
  XSetForeground(XD,XG,YELLOW);  // colour for the lettering
  XDrawString(XD,XW,XG,17,70,"ANIMAL REARING",14);
  T5text();
}

void T5fabri() {
  XSetForeground(XD,XG,YELLOW);  // colour for the lettering
  XDrawString(XD,XW,XG,17,70,"FABRICATED GOODS",16);
  T5text();
}

void T5RandD() {
  XSetForeground(XD,XG,YELLOW);  // colour for the lettering
  XDrawString(XD,XW,XG,17,70,"RESEARCH & DEVELOPMENT PROJECTS",31);
  T5text();
}

void T5craft() {
  XSetForeground(XD,XG,YELLOW);  // colour for the lettering
  XDrawString(XD,XW,XG,17,70,"CRAFT PRODUCTS",14);
  T5text();
}

void T5arts() {
  XSetForeground(XD,XG,YELLOW);  // colour for the lettering
  XDrawString(XD,XW,XG,17,70,"WORKS OF ART",12);
  T5text();
}

void T5spare() {
  XSetForeground(XD,XG,YELLOW);  // colour for the lettering
  XDrawString(XD,XW,XG,17,70,"SPARE SUB-TAB",13);
  T5text();
}




/* FIND A NEW [SPARE, UNOCCUPIED] CROP RECORD. The first 256 bits in record
   zero of 'crop.dat' are used to indicate whether or not a given crops
   record is occupied. For instance if the tenth bit is set, it means that
   record 10 of 'crop.dat' is occupied with a current valid crop entry. If
   the tenth bit is not set, it means that record No. 10 of 'crop.dat' is
   unoccupied [spare] and thus may be used for a new crop's data. This func-
   tion finds the number of the first available spare record in which to put
   a new crop's details. Called only once from T5cropNew(). */

int T5unocc() {
  fseek(FC,0,SEEK_SET);  // seek the start byte of 'crop.dat'
  int
    f = 0,  // 'spare record found' flag
    B,      // content of byte number 'i' of record zero of 'crop.dat'
    b,      // byte for the moving test bit
    i,      // number of the current byte within the 32-byte block
    j;      // bit number 0 to 7 counting from left to right

  for(i = 0; i < 32; i++) {  /* for each of the 32 bytes [of 8 bits each]

        get the content of byte number 'i' from record '0' of 'crop.dat'
        |         file handle for 'crop.dat'
        |         |        if the byte is full of '1' bits
        |         |        |   skip to the next byte
        |         |        |   | */
    if((B = fgetc(FC)) == 255) continue;

    b = 128;  // set the moving test bit to the most significant position

    for(j = 0; j < 8; j++) {  // for each of the 8 bits of the current byte
      if(B & b) b >>= 1;      // if the bit is set, shift to next lower bit
      else { f = 1; break; }  // set 'spare record found' flag; break j-loop
    } if(f) break;            // if spare record found, break the i-loop
  }

  if(f) {  // if a spare record has been found:

    I5 = i;  /* Preserve the number of the byte, within record zero of
             'crop.dat', that contains the new record's 'occupied' bit.

    Global variable holds the content of Byte 'i'
    |  with the new record's 'occupied' bit set.
    |
    |    Original content of Byte 'i' with the new record's bit unset.
    |    |
    |    |   All bits are 0 except for the one representing the new record.
    |    |   |   */
    B5 = B | b;  /* This content is copied to disk by T5cropCR() when CR is
                    pressed after the new crop name has been entered.

    Global variable to hold the new record's number temporarily
    |  until it is copied to 'r5' once CR has been pressed after
    |  the new crop name has been entered.
    |
    |     Byte number, within record zero of 'crop.dat', that contains the
    |     |  'occupied bit pertaining to the new record.
    |     |
    |     |    Multiplied by 8 to give the bit number of start of Byte 'i'.
    |     |    |
    |     |    |    Bit number, 0 to 7 counting left to right, of the new
    |     |    |    |  record's 'occupied' bit within Byte 'i'. 
    |     |    |    |                                                  */
    R5 = (i << 3) + j; return 1;  // return 'new spare record found'
  }
  showMsg(5); return 0;  // display 'no spare records available' message
}




/* REGISTERS THAT THE NEW RECORD IS NOW OCCUPIED. Sets the new record's
   'occupied' bit in record zero of 'crop.dat'. increments the total number
   of occupied records and, if necessary, increments the highest occupied
   record, both in record zero of 'crop.dat'. Called from T5cropCR(). */

void T5setocc() {
  fseek(FC,I5,SEEK_SET);  // re-seek byte 'i' containing the changed bit
  fputc(B5,FC);           // re-write the byte to disk with new bit set
  fseek(FC,32,SEEK_SET);  // N5 is stored in byte 32 of record zero
  fputc(++N5,FC);         // increment & store 'number of occupied records'
  r5 = R5;                // number of the newly created 'crop.dat' record
  q5 = r5 << 6;           // start byte of this 64-byte 'crop.dat' record
  p5 = q5;                // set start byte of field = start byte of record

  if(H5 < r5) {  // If new record is beyond current highest occupied
    H5 = r5;     // record, make it the new highest occupied record.

    fseek(FC,33,SEEK_SET);  // H5 is stored in byte 33 of record zero, store
    fputc(H5,FC);           // incremented number of highest occupied record
  }
}




/* TEST WHETHER A SPECIFIED CROP RECORD IS CURRENTLY OCCUPIED OR NOT
   Returns 1 if the crop record is currently occupied, 0 if not.
   Called only once from T5cropCR(). 

   The bits that indicate which of the 256 records of the 'crop.dat' file
   are currently occupied are in the first 32 bytes of record zero.

   Example: suppose the record we want to check is record 53: so r = 53.

   bit 0    bit 8    bit 16   bit 24   bit 32   bit 40   bit 48
   |        |        |        |        |        |        |     
   00000000 00000000 00000000 00000000 00000000 00000000 00000100 00000000
    Byte 0   Byte 1   Byte 2   Byte 3   Byte 4   Byte 5       |    Byte 7
                                                              bit 53

   The byte number we want B = r >> 3 = 53/8 = 6. The number of places 's'
   we must shift the top bit of the byte [value 128] right to arrive at the
   record's 'occupied' bit is 53 - 48 = 5, which is r - B * 8, which is
   better coded as s = r - (B << 3). We must therefore shift the mask bit
   '128' rightwards by 's' places for it to arrive in the position of r's
   'occupied' bit by doing 128 >> s. Substituting the expression for 's' we
   get m = 128 >> (r - (B << 3)), where 'm' is the value of the shifted mask
   bit. We now need to test to see if r's 'occupied' bit is set or not by AND-
   ing it with the mask bit 'm' and then return the value of this bit-wise
   AND, which is '1' if r's 'occupied' bit is set and '0' if it is not. */

int T5isocc(int r) {
  int B = r >> 3;        // byte of record zero containing r's occupancy bit
  fseek(FC,B,SEEK_SET);  // seek byte B

      /* get byte B containing record r's 'occupied' bit
         |         AND it with the mask bit 'm', which is:
         |         |  the top bit of a byte
         |         |  |   shifted left by the number of places computed as:
         |         |  |   |   number r of record whose occupied state we need
         |         |  |   |   | minus the bit number of byte B's top bit
         |         |  |   |   | |  which is the byte number B 
         |         |  |   |   | |  |    multiplied by 8 bits per byte
         |         |  |   |   | |  |    |            */
  return fgetc(FC) & (128 >> (r - (B << 3)));  // 1=yes, 0=no
}




/* SET UP A TRAWL SEARCH. This searches crop name field of all the crop rec-
   ords for the occurrence of the entered search term. Before comparing, it
   converts both the search term and the retrieved field content to upper
   case letters only. Called only from 1 place in T5cropNext() & T5cropCR().*/

void T5trawl() {
  int B = r5 << 6;  // start byte of record 'r5' [record number times 64]
  b5 |= 0x100;      // set the 'trawler active' bit
  showMsg(6);       // show message "Trawling for search term."
  SUC(en,st);       // convert search term to upper case only

  /* The following loop is merely to make sure that the trawl does not loop
  more than once round the file 'crop.dat'. The index 'i' plays no part in-
  side the loop. The active stepping variable is 'r5' the number of the rec-
  ord currently being tested for containing the trawl search term. */

  for(int i = 0; i < H5; i++) {

    if(T5isocc(r5)) {  // provided record 'r5' is occupied
      int  K,          // length of the content of the current field
           k;          // individual character retrieved from disk record
      char S[32],      // to hold the field content read from the disk record
           *s;         /* return pointer from the string comparator strstr()

            file handle for 'crop.dat'
            |  start byte of current record 'r5'
            |  |             */
      fseek(FC,B,SEEK_SET);  /* seek start byte of current record 'r5'

          if the length of this field's content
          |   as retrieved from disk
          |   |            is more than the permitted maximum of 31 bytes
          |   |            |       truncate it to the permitted 31 bytes
          |   |            |       |    */
      if((K = fgetc(FC)) > 31) K = 31;
      if(K < 0) K = 0;  // zero it if negative

      // copy crop name from disk into array S[] and terminate it with a null
      for(k = 0; k < K; k++) S[k] = fgetc(FC); S[K] = 0;

      SUC(K,S);  /* convert the crop name in S[] to upper case only

          points to character within field at which found search term begins
          |          content of the current crop name field
          |          | contains the entered search term
          |          | |                */
      if((s = strstr(S,st)) != NULL) {  // if the search term has been found

        T5crops();  // display crop data record with the found search term
        XSetForeground(XD,XG,GREEN);  /* highlight for trawl search term

                                   points to start of search term in S[]
             x-coord of start      |   points to start of crop name array S[]
            of crop name field     |   |    times 6 pixels per character
                             |     |   |    |               */
        XDrawString(XD,XW,XG,X5 + (s - S) * 6, Y6, s, en);  /*
                                     |         |   |  |
        number of chars along crop name field  |   |  number of characters
        on screen to where search term starts  |   |  in the search term
                                               |   |
          lettering base line of crop name field   |
          pointer to the 1st char of the search term        */

        showMsg(7);    // display the "search term found" message in green
        return;        // can return immediately without breaking the loops
      }
    }    // end of 'provided record 'r5' is occupied'

    /* If, on incrementing record number r5, it has overshot the last occu-
    pied record H5, reset it to record 1 and reset B to record 1's start
    byte 64, otherwise, increment B to the start byte of the next record. */
    
    if(++r5 > H5) { r5 = 1; B = 64; } else B += 64;

  }  // end of the for() loop

  showMsg(8);    // show message that the search term could not be found
  b5 &= ~0x100;  // kill the trawl mode
  r5 = 1;        // set back to first record
  T5crops();     // and display it
}




/* THE 'FIND' BUTTON WAS CLICKED. So clear the crop data fields and box the
   crop number field in green ready for entering the required crop number
   that you want to find. Called once in T5cropButClick(). */

void T5cropFind() {
  b5 |= 1;           // set the crop FIND bit
  b5 &= ~0x100;      // kill trawl mode
  showMsg(0);        // kill the trawl message
  T5cropBoxesClear();   // clear any possible yellow boxes
  T5cropFieldsClear();  // clear the new crop display record ready for in-fill
  f5 = 0;            // set to first field of new record [record zero]
  for(int i = 0; i < 48; i++) le[i] = 0;  // clear the line editor array
  ey = 99;           // line editor's vertical letter base y-coordinate
  ex = 200;          // line editor's field start position x-coordinate
  ec = 0;            // cursor's character position within field
  en = 0;            // number of characters currently in the field
  T5cropShowBox(f5,GREEN);  // display the clicked box in yellow
  doCursor(1);       // show the cursor in green at beginning of field
  ea = 1;            // activate [switch on] the line editor
}



/* SET UP A TRAWL SEARCH FOR AN ENTERED SEARCH TERM
   Called from only one place in T5cropButClick(). */

void T5cropTrawl() {
  b5 |= 2;           // set the crop TRAWL bit
  r5 = 1;            // set to first record in the file
  b5 |= 0x100;       // set the 'trawler active' bit
  showMsg(0);        // kill any lingering message
  T5cropBoxesClear();   // clear any possible yellow boxes
  T5cropFieldsClear();  // clear the new crop display record ready for in-fill
  for(int i = 0; i < 48; i++) le[i] = 0;  // clear the line editor array
  ey = 99;           // line editor's vertical letter base y-coordinate
  ex = 256;          // line editor's field start position x-coordinate
  ec = 0;            // cursor's character position within field
  en = 0;            // number of characters currently in the field
  f5 = 0;            // set to crop name entry field
  T5cropShowBox(f5,GREEN);  // display the clicked box in green
  doCursor(1);       // show the cursor in green at beginning of field
  ea = 1;            // activate [switch on] the line editor
}




/* DISPLAY THE SCROLLABLE LIST OF CROP DATA LINES.
   Called from T5cropList(), T5index(), MWup() and MWdown(). */

void T5cropScroll() {
  XSetForeground(XD,XG,BLACK);  // clear the scrolling list area
  XFillRectangle(XD,XW,XG,87,84,397,210);

  /* o5 is the column heading number 0 to 5 [including the REF column o5=4]
     But only 4 of the columns are indexed. REF is simply the record number
     within 'crop.dat'. To get the index number within the 'crop.idx' file,
     we have to miss out the REF column o5=4 and make CROP NAME [o5=5] the
     4th index as follows: 

           PLANTED  HARVEST  DAYS  LOC  REF  CROP NAME
     o5 =     0        1       2    3    4       5
     o  =     0        1       2    3            4  */

  int o = o5;            // set index number to column number
  if(o == 5)             // if column number is 5 [CROP NAME]
    o = 4;               // CROP NAME is index 4 [REF, column 4, has no index]
  S5 = o * 4096;         // start byte of current index 'o' within 'crop.idx'
  g5 = Scroller(g5,N5);  // update mouse wheel position & display scroll bar
  int w = l5 - g5,       // line number of currently selected person
      v = 100, i;        // print base of first data line, loop variable

  // LIST THE CURRENT SCREEN-FULL OF CROPS
  for(i = 1; i < 12; i++) {  // for each of the 12 lines in the list
    int C, x,                // colours for the data line & harvest date
        r = i + g5;          // record number of current 1st line in list
    if(o5 != 4) {            /* if not listing in REFerence number order

      SEEK THE START BYTE OF RECORD 'r' WITHIN THE INDEX FILE 'crops.idx'
            index file handle for 'crop.dat'
            |    skip over the crop data records
            |    |      start byte number for current index within crops.idx
            |    |      |     record number within current index
            |    |      |     |    multiplied by 16 bytes per record
            |    |      |     |    |             */
      fseek(FC,0x4000 + S5 + (r << 4),SEEK_SET);
      r = fgetc(FC);  /* 'crop.dat' record number from index file

      GET RECORD NUMBER IN 'nad.dat' OF THE CURRENTLY HIGHLIGHTED LINE
         number of the first currently-displayed line
         |   list line number of first display line
         |   |     line number within complete total list in file
         |   |     |   current 'nad.dat' record number of highlighted line
         |   |     |   |    'nad.dat' record number from index file
         |   |     |   |    |  */
      if(i + g5 == l5) r5 = r;
    } 
    else        // else we are listing in plain REF number order, so
      r5 = l5;  // the 'crop.dat' record number is simply the line number

    if(r > H5) break;            // break if r as beyond last occupied record
    if(!(T5isocc(r))) continue;  // skip if this record is unoccupied
    int B = r << 6;              // start byte of the current record
    C = BLEEN;                   // colour for normal data line
    if(i == w) C = WHITE;        // colour for the highlighted data line
    XSetForeground(XD,XG,C);     // colour for this data line

    T5getDays(B);  // get anticipated harvest date & time remaining to harvest

    /* If the PLANTED date is zero, show it as 8 dashes;
       otherwise display it as an 8-digit integer. */

    char *s; if(c5 > 0) s = showInt(c5,8,0); else s = "--------";
    XDrawString(XD,XW,XG,87,v,s,8);

    /* If the HARVEST date is zero, show it in normal colour as 8 dashes;
       else display it in colour 'C5' as an 8-digit integer. 'C5' is WHITE
       if the crop has already been harvested and AMBER or RED if not. */

    if(e5 > 0) { s = showInt(e5,8,0); x = C5; }
    else { s = "--------"; x = C; }
    XSetForeground(XD,XG,x);
    XDrawString(XD,XW,XG,141,v,s,8);

    /* If the crop has not yet been harvested, display, in AMBER, the days
       left before the anticipated harvest date; otherwise display it as 3
       dashes in normal colour. */

    if(C5 == AMBER) s = showInt(d5-j5,3,0); else s = "---";
    XSetForeground(XD,XG,C);
    XDrawString(XD,XW,XG,201,v,s,3);

    /* If the specified location of the crop [field or planting area] is
       non-zero, display it as an up to 3-digit number; otherwise, show it
       as 3 dashes, signifying that the location has not been specified. */

    fseek(FC,B+40,SEEK_SET);  // start byte of LOCation
    if((x = getInt(FC)) > 0) s = showInt(x,3,0); else s = "---";
    XDrawString(XD,XW,XG,234,v,s,3);

    XDrawString(XD,XW,XG,264,v,showInt(r,3,0),3);  // display the REF number

    // FINALLY, GET AND DISPLAY THE CROP NAME

    char S[32];                 // to contain the crop name
    fseek(FC,B,SEEK_SET);       // start byte of crop name
    int l = fgetc(FC);          // get the first byte of current record
    if(l < 0) l = 0;            // invalid line length
    if(l > 31) l = 31;          // upper safety limit
    for(int i = 0; i < l; i++)  // for each character of the product name
      S[i] = fgetc(FC);         // get it from the disk record
    XDrawString(XD,XW,XG,294,v,S,l);  // display crop NAME

    v += 19;  // drop down to the next line in the list
  }
}




/* DISPLAY THE SCROLLABLE LIST OF ALL CROPS. Called from 1 place each in:
   T5planted(), T5harvest(), T5days(), T5loc(), T5ref(), T5name(), 
   T5cropButClick() and T5show(). */

void T5cropList() { 
  clearTab();  // clear the whole tab
  showMsg(0);  // kill the trawl message
  int v = 70;  // vertical coord of the lettering base of the title line

  // DISPLAY THE COLUMN HEADING BUTTONS
  XSetForeground(XD,XG,YELLOW);  // colour for the lettering
  XDrawString(XD,XW,XG,17,v,"CROPS:",6);
  XSetForeground(XD,XG,DARK);   // colour for the title buttons
  XFillRectangle(XD,XW,XG, 87,55, 47,21);  // PLANTED button
  XFillRectangle(XD,XW,XG,141,55, 47,21);  // HARVEST button
  XFillRectangle(XD,XW,XG,195,55, 29,21);  // DAYS button
  XFillRectangle(XD,XW,XG,231,55, 23,21);  // LOCation button
  XFillRectangle(XD,XW,XG,261,55, 23,21);  // REF button
  XFillRectangle(XD,XW,XG,291,55,192,21);  // NAME button

  // DISPLAY ALL COLUMN HEADINGS AND HIGHLIGHT THE SELECTED ONE
  if(!(b5 & 0xFC00))  // if none of the column headings is selected 
    b5 |= 0x4000;     // select the default column heading: 'REF'
  char *G[] = {"PLANTED","HARVEST","DAYS","LOC","REF","CROP NAME"};
  int H[] = {90,144,198,234,264,360},  // horiz pixel start points of headings
      L[] = {7,7,4,3,3,9};             // number of letters in each heading
  for(int i = 0; i < 6; i++) {         // for each of the 6 heading buttons
    int c = GREY;                      // colour for dim button [default]
    if(b5 & 0x400 << i) c = WHITE;     // colour for highlighted button
    XSetForeground(XD,XG,c);           // colour for the lettering
    XDrawString(XD,XW,XG,H[i],v,G[i],L[i]);  // show heading lettering
  }
  v = 100;           // base line of the 1st data row's lettering
  XSetForeground(XD,XG,GLEEN);  // colour for the data list lettering
  XDrawString(XD,XW,XG,17,318, "Click a column heading to list the crops "
                               "in the casting order of that column.",77);
  T5cropScroll();    // display the scrollable list of crop data lines
  T5cropShowButs();  // display the vertical buttons of the crop sub-tab
  T5showButs();      // display the horizontal buttons of the PROJS tab
}




/* THE CROP SUB-TAB'S NEXT BUTTON WAS CLICKED
   Called only from one place in T5cropButClick(). */

void T5cropNext() {

  // if trawl button is on, trawl for next occurrence of search term
  if(b5 & 2) { if(++r5 > H5) r5 = 1; T5trawl(); return; }

  /* Loop up to as many times as there are records from the first
     [record 1] to the highest occupied record [H5] inclusive. */

  for(int i = 0; i < H5; i++) {

    /* if after incrementing the record number r5, it overshoots the number
    of the highest occupied record H5, then loop round back to record 1. */

    if(++r5 > H5) r5 = 1;
    if(T5isocc(r5)) {     // If record number r5 is currently occupied,
      T5crops(); return;  // display the record and exit.
    }
  }
  showMsg(18);  // otherwise show the 'no next record' message
}




/* THE CROP SUB-TAB'S PREV BUTTON WAS CLICKED
   Called only from one place in T5cropButClick(). */

void T5cropPrev() {

  /* Loop up to as many times as there are records from the first
  [record 1] to the highest occupied record [H5] inclusive. */

  for(int i = 0; i < H5; i++) {

    /* If after decrementing the record number r5, it now undershoots
    record 1, loop round back up to the highest occupied record H5. */

    if(--r5 < 1) r5 = H5; 

    if(T5isocc(r5)) {     // If record number r5 is currently occupied,
      T5crops(); return;  // display the record and exit.
    }
  }
  showMsg(19);  // otherwise show the 'no previous record' message
}




/* THE 'NEW' BUTTON WAS CLICKED. So create a new blank record ready for
   editing. Called once in T5cropButClick(). */

void T5cropNew() {
  b5 |= 0x10;          // set the crop NEW button bit
  b5 &= ~2;            // kill trawl mode
  showMsg(0);          // kill the trawl message
  if(T5unocc()) {      // if an unoccupied [spare] record [q5] was found
    T5cropBoxesClear();   // clear any possible yellow boxes
    T5cropFieldsClear();  // clear the new crop display fields
    f5 = 0;            // set to first field of new record [record zero]
    for(int i = 0; i < 48; i++) le[i] = 0;  // clear the line editor array
    ey = 99;           // line editor's vertical letter base y-coordinate
    ex = 256;          // line editor's field start position x-coordinate
    ec = 0;            // cursor's character position within field
    en = 0;            // number of characters currently in the field
    T5cropShowBox(f5,YELLOW);  // display the clicked box in yellow
    doCursor(1);       // show the cursor in green at beginning of field
    ea = 1;            // activate [switch on] the line editor
  }
}




/* THE 'KILL' BUTTON WAS CLICKED. Deletes the currently displayed crop record
   'r5'. Called once in T5cropButClick(). */

void T5cropKill(){
  b5 |=  0x20;           // set the CROP kill bit 
  b5 &= ~2;              // kill trawl mode
  showMsg(0);            // kill the trawl message
  int B = r5 >> 3;       // byte of record zero containing r5's 'occupied' bit
  fseek(FC,B,SEEK_SET);  // seek byte B within the file 'FC' [crop.dat]
  int x = fgetc(FC);     // get the byte containing record r5's 'occupied' bit
  fseek(FC,B,SEEK_SET);  // seek byte B again because fgetc() stepped it along

  /* RESTORE BYTE 'B' TO DISK WITH RECORD r5's 'OCCUPIED' BIT UNSET:

        content of byte B, which includes r5's 'occupied' bit
        | AND it with the
        | | INVERSE of the bit pattern of the mask bit byte, which is the
        | | |  top bit of the mask byte
        | | |  |  shifted right by the amount required to bring it to the
        | | |  |  |   position within the mask byte of r5's 'occupied' bit:
        | | |  |  |
        | | |  |  |   record number of the record being deleted
        | | |  |  |   |     number of the byte containing r5's 'occupied' bit
        | | |  |  |   |     |    multiplied by 8 bits per byte
        | | |  |  |   |     |    |          */
  fputc(x & ~(128 >> (r5 - (B << 3))),FC);
  fflush(FC);  // make sure the data is immediately stored on the disk
}




/* THE VERTICAL HELP KEY OF THE CROPS SUB-TAB WAS CLICKED
   [this function is not currently implemented] */

void T5cropHelp(){ printf("HELP\n"); }




/* STORE THE SEARCH TERM IN THE APPROPRIATE INDEX RECORD'S SEARCH TERM FIELD
   Called 3 times from T5cropSort() below. */

void T5cropPutIdx(int I, int i, char *s, int J) {
  int j;
  fseek(FC,I,SEEK_SET);   // start byte of field in next index record
  fputc(i,FC);            // put main record number in index record
  fputc(14,FC);           // put field length = 14 bytes
  for(j = 0; j < J; j++)  // put character array in the search term
    fputc(*(s+j),FC);     // field of the index record
  while(j < 14) {         // fill the remainder of the index record's
    fputc(0,FC); j++;     // search field with null characters
  }
}




/* SORT THE INDEX FILE CORRESPONDING TO FIELD HEADING 'o5'. NOTE: this fun-
   ction is not entered if 05 = 4 REF. Called only by T5cropSet(2). */

void T5cropSort() {
  showMsg(20);       // show the 'sorting' message
  int
    F[] = {44,44,44,40,0,0},  // field offsets within main 'crop.dat' records
    B = 64 + F[o5],  // start byte of requ'd field in record '1' of 'crop.dat'
    i,j,x;           // loop variables

  /* o5 is the column heading number 0 to 5 [including the REF column o5=4]
     But only 4 of the columns are indexed. REF is simply the record number
     within 'crop.dat'. To get the index number within the 'crop.idx' file,
     we have to miss out the REF column o5=4 and make CROP NAME [o5=5] the
     4th index as follows: 

           PLANTED  HARVEST  DAYS  LOC  REF  CROP NAME
     o5 =     0        1       2    3    4       5
     o  =     0        1       2    3            4  */

  int o = o5; if(o == 5) o = 4;  /* Ref [o5=4] does not have an index

  START BYTE OF RECORD 1 OF CURRENT INDEX 'o' WITHIN 'crop.dat'
  |      skip over the 256 64-byte records of crop data
  |      | and a further 16 bytes to the start byte of Record 1 of the index
  |      | |     index number of the selected index
  |      | |     |    times 256 * 16 = 4096 bytes per index
  |      | |     |    |     */
  S5 = 0x4010 + (o << 12); 
  int I = S5;               // initialise to start byte of selected index
  N5 = 0;                   // to re-compute number of occupied records

  for(i = 1; i <= H5; i++) {     // BUILD THE UNSORTED INDEX FILE
    if(!(T5isocc(i))) continue;  // skip if 'crop.dat' record is unoccupied
    N5++;                        // increment the number of occupied records
    char *s = "::::::::";        // high-cast string

    switch(o) {  // SWITCH ON INDEX NUMBER 0 TO 4

    case 0:  // DATE PLANTED
      fseek(FC,B+44,SEEK_SET);   /* seek Byte 44 of current crop data record

      Get the date PLANTED from the main crop record. If it's non-zero
      [i.e. it exists], put it in place of the blank high-cast string
      as the search term in the current record of the selected index. */

      if((x=getInt(FC)) > 0) s=showInt(x,8,1);
      T5cropPutIdx(I,i,s,8);
      break;

    case 1:  // HARVEST DATE
      T5getDays(B);  /* compute anticipated harvest date & time remaining

      If the elapsed time colour has been set to AMBER, it is an anticipated
      harvest date, that is, the crop has not yet been harvested store it,
      in place of the high-cast string, as the search term in current record
      of the selected index. */

      if(C5 == AMBER) s = showInt(e5,8,0);
      T5cropPutIdx(I,i,s,8);
      break;

    case 2:  // DAYS TO GO BEFORE ANTICIPATED HARVEST DATE
      T5getDays(B);  /* get elapsed time since planting

               indicates that the given harvest date it is an ANTICIPATED one
               |      time remaining to anticipated harvest date
               |      |           growing time required
               |      |           |  elapsed time since planting
               |      |           |  |  length of number including sign
               |      |           |  |  | put in leading zeros
               |      |           |  |  | |    */
      if(C5 == AMBER) s = showInt(d5-j5,4,1);  /*

                   start byte of current index record
                   | current index record number
                   | | search string: a signed 3-digit number
                   | | | string length
                   | | | |    */
      T5cropPutIdx(I,i,s,4);  // store the crop index record to disk
      break;

    case 3:  // LOCation [WHERE CROP IS LOCATED]
      fseek(FC,B+40,SEEK_SET);  // seek Byte 44 of current crop data record
      if((x=getInt(FC)) > 0)    // if LOCation is specified
        s = showInt(x,3,1);   // put it in place of the blank high-cast string
      T5cropPutIdx(I,i,s,3);  // store search term in appropriate index record
      break;

    case 4:  // CROP NAME: put field content in index record
      fseek(FC,B,SEEK_SET);    // start byte of crop name field
      int J = fgetc(FC);       // get length of crop name [unsigned char]
      if(J > 14) J = 14;       // we only search on the first 14 characters
      char S[14];              // to hold record content
      for(j = 0; j < J; j++)   // get crop name from crop data section
        S[j] = fgetc(FC);
      T5cropPutIdx(I,i,S,14);  // store search term in approp. index record
    }                          // end of switch

    I += 16;  // advance to start byte of next index record
    B += 64;  // increment to next record in 'crop.dat'
  }
  fseek(FC,32,SEEK_SET);  // N5 is stored in byte 32 of record zero
  fputc(N5,FC);           // store 'number of occupied records'
  FS = FC;                /* set file handle for the Hoare sort

        skip over the 1024 equivalent 16-bytes records of the crop data
        |      index number 0,1,2,3 or 4 of the selected index
        |      |    multiplied by 256 records per index
        |      |    |    */
  i = 0x400 + (o << 8);  /* record zero of the selected index

           first occupied record of the sort range
           |   last occupied record of the sort range
           |   |      */
  idxSort(i+1,i+N5);  // sort the newly-built index
  fflush(FC);         // make sure everything is written to disk
}




/* REGISTER THAT A CROP FIELD HAS BEEN MODIFIED [EDITED OR NEW]. The 'field
   modified' bits are stored in Byte 34 of 'crop.dat'. A field's 'modified'
   bit is set every time a field's content is new or has been edited, indic-
   ating that its corresponding index field needs to be re-sorted. A field's
   'modified' bit is unset every time the index file for that field has just
   been re-sorted. Called from T5cropListClick(), T5cropNameSave(), T5cropCR() 

   o5=5  CROP NAME                      When a bit is set, it means that the
   o5=4  |REFerence [not used]          corresponding column's index needs 
   o5=3  ||LOCATION                     to be sorted before being displayed
   o5=2  |||DAYS before harvest         If the bit is not set, the column's 
   o5=1  ||||HARVEST                    index doesn't need resorting so can
   o5=0  |||||PLANTED                   be displayed immediately.
         ||||||
       00111111  Byte 34 of record zero of 'crop.dat'  */

void T5cropSet(int s) {   // s=0 unset bit; s=1 set bit; 3=sort if bit set
  if(o5 == 4) return;     // REFerence number does not have an index file
  fseek(FC,34,SEEK_SET);  // seek byte 34 of 'crop.dat'
  int c = fgetc(FC),      // get the existing byte [bit pattern] from disk
      d = 1 << o5;        // set bit in position corresponding to field 'o5'
  switch(s) {
    case 0: c &= ~d; break;  // unset the 'field modified' bit
    case 1: c |= d; break;   // set the 'field modified' bit
    case 2:
      T5cropSort();       // sort the current index if sort bit set
      T5cropSet(0);       // Re-enter to unset the field's
      return;             // 'field modified' bit.
  }
  fseek(FC,34,SEEK_SET);  // seek byte 34 of 'crop.dat'
  fputc(c,FC);            // set bit for modified field and restore the byte
  fflush(FC);             // make sure it is actually put to disk
}




/* HANDLES MOUSE CLICKS ON THE CROP LIST COLUMN HEADINGS
   Called from only one place in T5click(). */

void T5cropListClick() {
  int i = -1;
  if(my > 54 && my < 76) {                // headings buttons height limits
    if(mx >  86 && mx < 134)      i = 0;  // PLANTED heading clicked
    else if(mx > 140 && mx < 188) i = 1;  // HARVEST heading clicked
    else if(mx > 194 && mx < 224) i = 2;  // DAYS heading clicked
    else if(mx > 230 && mx < 254) i = 3;  // LOC heading clicked
    else if(mx > 260 && mx < 284) i = 4;  // REF heading clicked
    else if(mx > 290 && mx < 483) i = 5;  // CROP NAME heading clicked

    if(i >= 0) {      // provided the click was on a heading button
      l5 = 0;         // clear index number of highlighted item
      b5 &= ~0xFC00;  // switch off all the heading column highlighting
      switch(i) {
        case 0: if(!(b5 &  0x400)) { b5 |=  0x400; } break;  // PLANTED
        case 1: if(!(b5 &  0x800)) { b5 |=  0x800; } break;  // HARVEST
        case 2: if(!(b5 & 0x1000)) { b5 |= 0x1000; } break;  // DAYS
        case 3: if(!(b5 & 0x2000)) { b5 |= 0x2000; } break;  // LOCation
        case 4: if(!(b5 & 0x4000)) { b5 |= 0x4000; } break;  // REFerence
        case 5: if(!(b5 & 0x8000)) { b5 |= 0x8000; }         // CROP NAME
      }
      o5 = i;  // only update o5 if a column heading has actually been clicked
    }
  }
  /* Else the click could have been on one of the 11 list items. So if indeed
     the click was on one of the list items, compute the record number 'lo',
     within the index file, of the highlighted item. */

  else if((i = listClick()) > 0) l5 = i + g5;

  T5cropSet(2);    // sort index if field o5's 'field modified' bit is set
  T5cropList();    // list the [sorted] crops
}




/* MOUSE CLICK HANDLER FOR TAB 5. Used to determine which button in the vert-
   ical column was clicked. Called only from T5click() below. */

int T5cropButClick() {
  if(mx < 17 || mx > 69)  // exit if outside horizontal buttons limits
    return(0);            // return that no button was clicked
  int i, y = 84, q = 0; 
  for(i = 0; i < 8; i++) {   // for each of the 8 vertical buttons
    if(my > y && my < y+21)  // if mouse in vertical limits of this button
      break;                 // break out of loop: i = button number
    y += 27;                 // drop down to the next button
  } if(i > 7) return(0);     /* click was below the bottom button

  Clear all buttons      256            trawl HELP button      128
  that are set 'on',     512           amber| |LIST button      64
  except as shown on    1024        PLANTED|| ||KILL button     32
  the right:            2048       HARVEST||| |||NEW button     16
                        4096         DAYS|||| ||||PREV button    8
                        8192         LOC||||| |||||NEXT button   4
                       16384        REF|||||| ||||||TRAWL button 2
                       32768 CROP NAME||||||| |||||||FIND button 1
                                     |||||||| ||||||||  */
  b5 &= 0xFF42; // 00000000 00000000 11111111 01000010

  if(i == 6) {            // if the LIST button was clicked:
    if(b5 & 0x40) {       // if the LIST button is on
      b5 &= ~0x1FF;       // switch all buttons + trawl mode bit off
      if(r5 < 1) r5 = 1;  // if crop number < 1 set crop number = 1
      T5crops();          // display the crop details
    } else {              // else, the LIST button is dim, so
      b5 &= ~0x1FF;       // switch all buttons + trawl mode bit off
      b5 |= 0x40;         // switch the LIST button on
      T5cropList();       // show Crops List in ref number order by default
    }
    T5cropShowButs();  // show the new state of the vertical buttons
    return(1);         // return that a button was clicked
  }
  /* If the LIST button is on but wasn't clicked, ignore a click on any other
  button. return(1) means the click was not on a line of the LIST display. */
  if(b5 & 0x40) return(1); 

  switch(i) {           // switch on button number 0 to 7
    case 0: T5cropFind(); break;  // FIND key was clicked
    case 2: T5cropNext(); break;  // NEXT key was clicked
    case 3: T5cropPrev(); break;  // PREV key was clicked
    case 4: T5cropNew();  break;  // NEW  key was clicked
    case 5: T5cropKill(); break;  // KILL key was clicked
    case 7: T5cropHelp(); break;  // HELP key was clicked

    case 1:             // TRAWL key was pressed
      if(b5 & 2)        // if the TRAWL button is bright
        b5 &= ~2;       // dim it
      else              // otherwise, TRAWL button is dim
        T5cropTrawl();  // so initiate a TRAWL search
  }
  T5cropShowButs();  // show the new state of the vertical buttons
  return(1);         // return that a button was clicked
}




/* HANDLE A CLICK MADE ON THE CROPS TAB. Highlights [selects] one
   ham displayed in the hams list. Called only from MEH() */

void T5cropFieldClick() {
  T5crops();  // show a pristine crop record display
  ea = 0;     // disable the Line Editor
  int j;      // loop variables
  T5cropBoxesClear();  // in case click is outside any field

  if(my < 85 || my > 282) return;  // if click was outside vertical limits
  if(mx > 196 && mx < 251) {       // inside short fields column
    int y = 85;                    // top of top data field
    for(f5 = 0; f5 < 10; f5++) {   // for each of the remaining 6 fields
      if(my > y && my < y + 16)    // if click was within the f5-th field
        break;                     // exit the loop
      y += 20;                     // drop down to next field
    }
    if(f5 > 9) return;      // exit is the click was not on a field
    if(f5 > 5 && f5 < 8) {  // fields 6 & 7 can't be edited so show the 
      showMsg(16); return;  // "can't edit this field" message and exit
    }
  }
  else if(mx > 252 && mx < 444)  // click was inside product name field
      f5 = 0;                    // field zero [product name] was clicked
  else return;                   /* click was outside any field

  start byte of the current CROP record [start byte of crop field zero]
  |    this crop's record number in 'crop.dat'
  |    |     64 bytes per record
  |    |     |   */
  q5 = r5 << 6;  /*

  make the start byte of the CROP field currently being edited
  |    the same as the start byte of current crop record
  |    |   */
  p5 = q5;
  if(f5 == 0) {              // if we're editing the CROP NAME field
    fseek(FC,p5,SEEK_SET);   // start byte of the current crop record
    en = fgetc(FC);          // get the first byte of current record
    if(en < 0) en = 0;       // invalid line length
    if(en > 31) en = 31;     // upper safety limit
    for(j = 0; j < en; j++)  // for each character of the product name
      le[j] = fgetc(FC);     // put it into the Line Editor's array
    ex = 256;                // line editor's field start position
  }
  else {           // else, if it is ANY OTHER FIELD
    j = f5;        // set field number same as in disk record
    if(f5 > 7)     // if screen field is 8 or 9
      j = f5 - 2;  /* subtract 2 to get field number within disk record

    start byte of crop field currently being edited
    |     start byte of current CROP record [start byte of crop field zero]
    |     |    length of first field - 4 bytes 
    |     |    |    field number within crop record
    |     |    |    |    4 bytes per field for fields 1 to 5
    |     |    |    |    |      */
    p5 = q5 + 28 + (j << 2);
    fseek(FC,p5,SEEK_SET);  // seek the start byte of this field

    /* Get the integer from the disk record, convert it to an 8-character
    array then copy its 8 characters into the line editor's work array. */

    char *s = showInt(getInt(FC),8,0);
    for(j = 0; j < 8; j++)
      le[j] = *(s + j);
    le[8] = 0;           // make sure it has a NULL terminator character
    ex = X6;             // horizontal start position of line editor's field
    en = 8;              // number of characters in line editor's field
  }
  ey = 99 + f5 * 20;     // line editor's vertical letter base coord
  ec = (mx - ex) / 6;    // cursor's character position within field
  ea = 1;                // activate line editor
  T5cropShowBox(f5,YELLOW);  // display the clicked box
  doCursor(1);           // show the cursor in green
}




/* MOUSE CLICK HANDLER FOR TAB 5. Deals with clicks within current sub-tab.
   Used to determine which button in the vertical column was clicked
   or which of the current sub-tab's data fields was clicked.
   Called from only one place in the Mouse Events Handler MEH().*/

void T5click() { 
  if(a5 & 1) {              // if we're in the CROPS sub-tab
    if(b5 & 0x40) {         // if in the Crop List display
      if(T5cropButClick())  // If click was on one of the buttons
        return;             // in the vertical column, then exit.
      T5cropListClick();    // click was on a column heading button
    } else {
      if(T5cropButClick())  // If click was on one of the buttons
        return;             // in the vertical column, then exit.
      T5cropFieldClick();   // otherwise click was in a data field
    }
  }                         // or in an invalid part of the tab area
}




/* HANDLES CLICKS ON THE HORIZONTAL BOTTOM BUTTONS OF TAB 5: THE 'PROJS' TAB
   Called from only 1 place in MEH(). */

void T5horizButClick(int i) {
  clearTab();            // clear the tab area
  b5 &= ~0x100;          // kill CROPS trawl mode
  a5 &= ~0xFF;           // kill all PROJect tab buttons
  showMsg(0);            // kill the trawl message
  switch(i) {            // IF DIM, BRIGHTEN, SHOW SUB-TAB  EXIT  
    case 0:              // CROPS
      if(!(a5 &  1)) {   // if the CROPS button is dim
        a5 |=  1;        // illuminate the CROPS button
        if(b5 & 0x40)    // if the CROPS->LIST button is bright
          T5cropList();  // re-display the Crops List
        else             // otherwise
          T5crops();     // re-display the crop details
      } break;
    case 1: if(!(a5 &    2)) { a5 |=    2; T5anims(); } break;  // ANIMALS
    case 2: if(!(a5 &    4)) { a5 |=    4; T5fabri(); } break;  // FABRIcation
    case 3: if(!(a5 &    8)) { a5 |=    8; T5RandD(); } break;  // R&D
    case 4: if(!(a5 & 0x10)) { a5 |= 0x10; T5craft(); } break;  // CRAFT
    case 5: if(!(a5 & 0x20)) { a5 |= 0x20; T5arts();  } break;  // ARTS
    case 6: if(!(a5 & 0x40)) { a5 |= 0x40; T5spare(); } break;  // SPARE
    case 7: if(!(a5 & 0x80)) { a5 |= 0x80;                      // HELP
      helpPage("software/fep.html#projs");
    }
  } T5showButs();  // re-display the buttons
}




/* [RE]DISPLAY A SINGLE FIELD OF A CROP RECORD
   Called from one place each in T5cropCR() and T5cropEsc(). */

void T5showCropField() {
  int I,i; char S[32];        // loop variables + field characters array
  fseek(FC,p5,SEEK_SET);      // go to the first byte of the edited field
  if(f5 == 0 && !(b5 & 1)) {  // if it was the crop name that was edited:
    I = fgetc(FC);            // get the first byte of current record
    if(I < 0) I = 0;          // invalid line length
    if(I > 31) I = 31;        // upper safety limit
    for(i = 0; i < I; i++)    // for each character of the product name
      S[i] = fgetc(FC);       // get it from the disk record
    XDrawString(XD,XW,XG,X5,Y6,S,I);  // display PRODUCT NAME
  } else  /* else,

    IF IT'S ONE OF THE OTHER FIELDS, RETRIEVE AND DISPLAY IT:
    |
    |   Y6 is really the baseline for the 1st field of the TRANSactions data
    |   display. But it's the same value for the CROP display, so we use it.
    |                       |
    |                       |    current field number in crop data display
    |                       |    |    times 20 pixels inter-line drop
    |                       |    |    |                               */
    XDrawString(XD,XW,XG,X6,Y6 + f5 * 20,showInt(getInt(FC),8,0),8);  /*
                                             |      |   |   | |  |
            show as a numeric character string      |   |   | |  |
    numeric quantity pulled from field of CROP record   |   | |  |
                               file handle for 'crops.dat'  | |  |
                                     set as an 8-digit string |  |
                                            with leading spaces  |
                                          display all 8 characters    */
}




/* WRITES THE CONTENT OF THE LINE-EDITOR ARRAY TO THE CROP'S DISK RECORD,
   ENSURING THAT ALL LOWER CASE LETTERS ARE MADE CAPITAL.
   Called from 2 palces in T5cropCR(). */

void T5cropNameSave() {
  int c;                  // to hold a single character
  fseek(FC,p5,SEEK_SET);  // seek start byte of record
  fputc(en,FC);           // write length of the new field content
  SUC(en,le);             // convert to upper case only
  for(int i = 0; i < en; i++)
    fputc(le[i],FC);      // write each character of crop name to disk
  T5cropSet(1);           // set this field's 'field modified' bit
}




/* CARRIAGE-RETURN [ENTER] PRESSED WHEN EDITING A CROP DATA FIELD
   Stores the edited version of the field. Removes the yellow box.
   Re-gets from disk and re-displays the new field content.
   Called from only 1 place in the Carriage-Return Dispatcher CR(). */

void T5cropCR() {
  fseek(FC,p5,SEEK_SET);  // go to the first byte of the edited field
  if(f5 == 0) {           // if either top numeric field or top text field

    // CR PRESSED AFTER ENTERING A CROP NAME WHILE IN FIND MODE

    if(b5 & 1) {          // if the FIND button was clicked

      /*  If crop record number typed in to the green FIND field is
          |              less or equal that of highest occupied record
          |              |        and is itself currently occupied
          |              |        |            display the crop record 'r5'.
          |              |        |            |             */
      if((r5 = atoi(le)) <= H5 && T5isocc(r5)) T5crops();

      else showMsg(17);  // otherwise display the 'not found' message
      return;            // then in either case, exit
    } 

    // CR PRESSED AFTER ENTERING A CROP NAME WHILE IN TRAWL MODE

    else if(b5 & 2) {    // else if the TRAWL button was clicked
      for(int i = 0; i < en; i++) {
        st[i] = le[i];   // put line editor field content into search term
      } st[en] = 0;      // array and terminate it with a NULL character
      r5 = 1;            // start at the first record
      T5trawl();         // trawl for the record containing the entered name
      return;            // and exit
    }

    // CR PRESSED AFTER ENTERING THE CROP NAME FOR A NEW CROP RECORD

    else if(b5 & 0x10) {  // else if the NEW button is bright
      T5setocc();         // register the prospective record as a new record
      T5cropNameSave();   // save the new crop name
      T5crops();          // re-display the new record for editing
      b5 &= ~0x10;        // dim the NEW button
      T5cropShowButs();   // redisplay the vertical button states
      return;             // and exit
    }

    // CR PRESSED AFTER SIMPLY EDITING AN EXISTING CROP NAME

    else                 // else it was the crop name field being edited
      T5cropNameSave();  // so save the crop name
  }

  // CR PRESSED AFTER EDITING A FIELD OTHER THAN A CROP NAME

  else                    // else, it was one of the other fields:
    putInt(atoi(le),FC);  // so store field to disk as an integer

  T5cropSet(1);                 // set the 'field modified' bit
  T5cropShowBox(f5,BLACK);      // remove the yellow box
  T5cropFieldClear();           // clear original content from the field f5
  XSetForeground(XD,XG,GREEN);  // colour for field lettering
  T5showCropField();
}




/* THE ESCAPE KEY WAS PRESSED WHILE EDITING A CROP DATA FIELD
   Called only from one place in Esc(). */

void T5cropEsc() {
  if(b5 & 0x40 || b5 & 0x100)  // if the TRAWL or LIST mode
    T5crops();                 // Redisplay the crop data
  else {                       // otherwise
    T5cropShowBox(f5,BLACK);   // remove the yellow box
    T5cropFieldClear();        // clear original content from the field f5
    XSetForeground(XD,XG,WHITE);  // colour for field lettering
    T5showCropField();  // display original content of escaped field
  }
}




/* HANDLES A 'PgUp' OR 'PgDp' KEY STROKE. Eleven lines of the CROPS list is
   displayed on the screen. A 'PgDn' keystroke causes the list to advance
   by only 10 lines, leaving what was the bottom line of the list as its new
   top line. This gives better user confidence and sense of continuity. */

void T5PgUpDn(int s) {   // 's' has a value of +10 for PgDn and -10 for PgUp
  if(tb != 5) return;    // exit if not in Tab 5
  int x = -11;           // start position of current page [screen-full]
  switch(a5) {           // switch on button logic
    case 1:              // if we're in the LIST sub-tab
      g5 += s;           // increment/decrement mouse bias by 10 lines
      if(s < 0) {        // if it was a 'Page-Up' key-stroke
        if(g5 < 0)       // if we've overshot the beginning of diary list
          g5 = 0;        // display from the beginning of the diary list 
      } else {           // if it was a 'Page-Down' key-stroke
        x += N5;         // start of the final 11 lines of diary list
        if(g5 > x)       // if we've overshot the end of the diary list
          g5 = x;        // set to display the final 11 lines
      } T5cropScroll();  // re-display the diary list
  }
}




/* [RE] DISPLAYS THE APPROPRIATE SUB-TAB CONTENT IN TAB 5 [PROJects] BOTH ON
   INITIAL EXPOSURE AND RE-EXPOSURE ON RETURN FROM ANOTHER DESKTOP OF FROM A
   MINIMISED STATE. Called from one place in showTab(). */

void T5show() {
  if(tb != 5) return;  // bail out if PROJS tab not currently visible
  if(a5 & 1) {         // if the CROPS sub tab is selected
    if(b5 & 0x40)      // if the CROPS LIST button is on
      T5cropList();    // display the Crops List
    else
      T5crops();  // display the crops data screen
  }
  T5showButs();   // display the PROJ tab's bottom buttons
}





// FUNCTIONS PERTAINING TO TAB 4: EQUPT [EQUIPMENT] --------------------------


/* DRAW HORIZONTAL ROW OF CONTROL BUTTONS ACROSS BOTTOM OF EQUPT TAB.
   Called from 1 place in T4show() & T4horizButClick(). */

void T4showHorizButs() {
  if(tb != 4) return;  // bail out if LINK tab not currently visible
  showButs();  // show 8 blank buttons across the bottom of the tab area
  const char   // annotate the horizontal row of control buttons
    *A[] = {"UTD","FARM","NAV","SPARE","SPARE","SPARE","SPARE","HELP"};
  const int 
    a[] = {18,15,18,12,12,12,12,15},  // horizontal offsets for word starts
    b[] = { 3, 4, 3, 5, 5, 5, 5, 4};  // number of letters in each word
  int h = 17;                         // x coordinate of button block
  for(int i = 0; i < 8; i++) {        // for each of the 8 buttons:
    if(a4 & 1 << i)                   // if this button should be bright
      XSetForeground(XD,XG,WHITE);    // set colour for bright lettering
    else
      XSetForeground(XD,XG,GREY);     // colour for dull lettering
    XDrawString(XD,XW,XG,h+a[i],377,A[i],b[i]);
    h += 59;  // move across to the next button
  }
}




/* DISPLAY THE VERTICAL COLUMN OF BUTTONS IN THE RAIN SUB-TAB.
   Called from T4showTab() and T4spare(). */

void T4showVertiButs(int i) {
  char  // annotate the vertical row of control buttons
    *A[7][8] = {
      {"ELECT","HOB","OVEN","FRIDGE","WASHER","SEWING","WATER","WASTE"},
      {"PLOW","DISK","HARROW","PLANT","REAP","PICK","WATER","FEED"},
      {"MAPPING","PROGRAM","AREA","PROCESS","START","PAUSE","ABORT","HELP"},
      {"","","","","","","",""},
      {"","","","","","","",""},
      {"","","","","","","",""},
      {"","","","","","","",""}
  };
  int 
    a[7][8] = {  // horiz offsets for word starts
      {12,18,15, 9, 9, 9,12,12},  // UTD tab
      {15,15, 9,12,15,15,12,15},  // FARM tab
      { 6, 6,15, 6,12,12,12,15},  // NAV tab
      {0,0,0,0,0,0,0,0}, 
      {0,0,0,0,0,0,0,0}, 
      {0,0,0,0,0,0,0,0},
      {0,0,0,0,0,0,0,0}
    },
    b[7][8] = {  // number of letters in each word
      {5,3,4,6,6,6,5,5},  // UTD tab
      {4,4,6,5,4,4,5,4},  // FARM tab
      {7,7,4,7,5,5,5,4},  // NAV tab
      {0,0,0,0,0,0,0,0},
      {0,0,0,0,0,0,0,0},
      {0,0,0,0,0,0,0,0},
      {0,0,0,0,0,0,0,0}
    },
    G[] = {b4,c4,d4,e4,f4,g4,h4},
    g = G[i],  // current sub-tab number [bottom button number 0 to 7]
    h = 99;    // y coordinate of button block

  for(int j = 0; j < 8; j++) {      // for each of the 8 buttons:
    XSetForeground(XD,XG,DARK);     // shade for buttons' background
    XFillRectangle(XD,XW,XG,17,h-15,53,21);
    if(g & 1 << j)                  // if this button is bright [on]
      XSetForeground(XD,XG,WHITE);  // set colour for bright lettering
    else                            // otherwise
      XSetForeground(XD,XG,GREY);   // set colour for dull lettering
    XDrawString(XD,XW,XG,17+a[i][j],h,A[i][j],b[i][j]);
    h += 27;                        // move across to the next button
  }
}





/* DISPLAY THE CONTENT OF SUB-TAB 'i':
   Called from 1 place in T4show() and T4horizButClick(). */

void T4showTab(int i) {
  char *S[3][8] = {
  { "Electrical power and lighting circuits.",
    "Electrical induction twin hob for cooking.",
    "Thermal, forced-air and microwave ovens.",
    "Fidge and chest freezer with forced-air cooled compressors.",
    "Horizontal drum washing machine and tumble dryer.",
    "Sewing and cutting machines for clothing.",
    "Water supply circuits for kitchen, bathroom and growing units.",
    "Waste water toilet-to-tap recycling system." },

  { "Maintenance instructions & diary + mount command for plough.",
    "Maintenance instructions & diary + mount command for disk harrow.",
    "Maintenance instructions & diary + mount command for spike harrow.",
    "Maintenance instructions & diary + mount command for seed planter.",
    "Maintenance instructions & diary + mount command for cereal reaper.",
    "Maintenance instructions & diary + mount command for fruit picker.",
    "Maintenance instructions & diary + mount command for irrigator.",
    "Maintenance instructions & diary + mount command for plant feeder." },

  { "Set up or alter the mapping of crop areas.",
    "Program the small-area hyperbolic radio navigator.",
    "Select which area is to be processed.",
    "Select the type of process to be effected upon the selected area.",
    "Start and monitor the process.",
    "Pause the process to rectify a problem before re-starting.",
    "Abort the process and have the machine return to base.",
    "Access the web-based discourse on this sub-tab." }
  };
  int s[3][8] = {
    {39,42,40,59,32,41,62,43},
    {60,65,66,66,67,66,63,66},
    {42,50,37,65,30,58,54,47}
  },  v = 98;                    // y-coord of first annotation line
  clearTab();                    // clear the tab area
  XSetForeground(XD,XG,YELLOW);  // colour for the lettering
  XDrawString(XD,XW,XG,17,70,
    "ACTIVE DOMESTIC DEVICES OF THE UNIVERSAL TERRESTRIAL DWELLING [UDT]",67);
  XSetForeground(XD,XG,BLEEN);   // colour for the lettering
  for(int j = 0; j < 8; j++) {
    XDrawString(XD,XW,XG,87,v,S[i][j],s[i][j]);
    v += 27;
  }
  XDrawString(XD,XW,XG,17,315,"Click button for usage and "     //27
    "configuration instructions, plus maintenance diary.",78);   //50
  T4showVertiButs(i);  // 0=UTD sub-tab's vertical buttons
  showMsg(0);
}




/* Reset the x and y coordinate data sets for both the painter trace and
   the wiper trace then wipe the plotting area and re-draw the axes. Called
   from 1 place in T4show() and T4horizButClick(). */

void T4spare(int i) {
  XSetForeground(XD,XG,YELLOW);  // colour for the lettering
  XDrawString(XD,XW,XG,17,70,
    "SPARE TAB AVAILABLE FOR FUTURE EQUIPMENT-RELATED FUNCTIONS",58);
  T4showVertiButs(i);
  showMsg(0);
}




/* MOUSE CLICK HANDLER FOR TAB 4. Used to determine which button in the vert-
   ical column was clicked. Called only from T4click() below. */

void T4vertiButClick() {
  if(mx < 17 || mx > 69)
    return;                  // exit if outside horizontal buttons limits
  int i, y = 84;             // height [y-coord] of top of top button
  for(i = 0; i < 8; i++) {   // for each of the 8 vertical buttons
    if(my > y && my < y+21)  // if mouse within vertical limits of this
      break;                 // button, break out of loop: i = button number
    y += 27;                 // drop down to the next button
  } if(i > 7) return;        // click was above top or below bottom button
  int j = 1 << i,            // position bit for the clicked vertical button
      m = 0;                 // message number
  showMsg(0);                // clear any lingering message
  switch(i4) {               // SWITCH ON SUB-TAB BUTTON CODE
    case 0:                  // THE UTD SUB-TAB
      if(b4 & j)             // if the first button is already bright
        b4 &= ~0xFF;         // clear all vertical control buttons
      else {                 // else the first button must be dim
        b4 &= ~0xFF;         // clear all vertical control buttons
        b4 |= j;             // illuminate the first control button
        m = 28;              // display the 'not implemented' message
      } break;
    case 1:                  // THE FARM SUB-TAB
      if(c4 & j)             // if the first button is already bright
        c4 &= ~0xFF;         // clear all vertical control buttons
      else {                 // else the first button must be dim
        c4 &= ~0xFF;         // clear all vertical control buttons
        c4 |= j;             // illuminate the first control button
        m = 28;              // display the 'not implemented' message
      } break;
    case 2:                  // THE NAV SUB-TAB
      if(d4 & j)             // if the first button is already bright
        d4 &= ~0xFF;         // clear all vertical control buttons
      else {                 // else the first button must be dim
        d4 &= ~0xFF;         // clear all vertical control buttons
        d4 |= j;             // illuminate the first control button
        m = 28;              // display the 'not implemented' message
      } break;
  } showMsg(m); T4showVertiButs(i4);
}




/* HANDLES CLICKS TO THE 8 BOTTOM BUTTONS OF THE EQUPT TAB [TAB 4]
   Called from only one place in MEH(). */

void T4horizButClick(int i) {  // called from only 1 place in MEH()
  clearTab(); 
  i4 = i;        // make 'number of selected horizontal button' global
  a4 &= ~0xFF;   // kill all buttons
  a4 |= 1 << i;  // illuminate the clicked horizontal button
  if(i < 3) T4showTab(i);
  else
  if(i < 7) T4spare(i);
  else
    helpPage("software/fep.html#equpt");
  T4showHorizButs();  // re-display the buttons
}




/* THE MOUSE WAS CLICKED WITHIN THE EQUPT TAB.
   Called from 2 places in MEH(). */

void T4click() { T4vertiButClick(); }




/* SETS UP THE TEXT AND BUTTONS FOR THE EQUPT TAB. This is a dummy function
   informing the user that this Tab's functionality is not implemented in
   this demo version of the program. Called from 1 place in showTab(). */

void T4show() {
  if(i4 < 3)          // if sub-tabs 0=UTD, 1=farm, 2=nav selected
    T4showTab(i4);    // display the appropriate sub-tab
  else                // otherwise, sub-tabs 3,4,5,6 [all spare] is seected
    T4spare(i4);      // so display the 'spare' text
  T4showHorizButs();  // display the tab's buttons
}





// FUNCTIONS PERTAINING TO TAB 3: POWER --------------------------------------


/* DRAW THE HORIZONTAL ROW OF CONTROL BUTTONS ACROSS THE BOTTOM OF THE
   POWER TAB. Called from 1 place each in T3showINPUT(), T3showOUTPUT(),
   T3showBOTH() and T3horizButClick(). */

void T3Buts() {
  showButs();  // show 8 blank buttons across the bottom of the tab area
  const char   // annotate the horizontal row of control buttons
    *A[] = {"SOURCE","SOLAR","WIND","APU.","BATT","CONSUM","REGEN","HELP"};
  const int 
    a[] = { 9,12,15,18,15,9,12,15},  // horizontal offsets for word starts
    b[] = { 6, 5, 4, 3,4, 6, 5, 4};  // number of letters in each word
  int h = 17;                        // left edge of left-most button
  for(int i = 0; i < 8; i++) {       // for each of the 8 buttons:
    if(a3 & 1 << i) {                // if the button is 'on' [bright]
      XSetForeground(XD,XG,WHITE);   // display it's lettering in white
    } else
      XSetForeground(XD,XG,GREY);    // default colour for the lettering
    XDrawString(XD,XW,XG,h + a[i],377,A[i],b[i]);
    h += 59;  // move across to the next button
  }
}




/* SHOW PERCENTAGE GRAPHIC AND DIGITS. This is the grey box that's internally
   100 pixels high to show the percentage of the power that is supplied from
   sustainable sources. The green infill of the graphic is 100 pixels [100%]
   high by 40 pixels wide.

                   percentage 0 to 100
                   |      x-coord of left side of graphic box
                   |      |      y-coord of top of graphic box
                   |      |      |  */
void T3percent(int p, int h, int v) {

        /* if the percentage submitted is more than 100
           |        or less than zero
           |        |           */
  if(p > 100 || p < 0) return;  // bail out
  XSetForeground(XD,XG,BLACK);              // set colour for lettering
  XFillRectangle(XD,XW,XG,h+2,v+2,40,100);  // clear the percentage graphic
  XFillRectangle(XD,XW,XG,h,v+9,24,9);      // and the percentage readout
  XSetForeground(XD,XG,GREY);               // set colour for lettering
  XDrawRectangle(XD,XW,XG,h+1,v+1,41,101);  // show the percentage box
  XDrawLine(XD,XW,XG,h+50,v+1,h+50,v+101);  // show vertical scale line

  for(int i = 0; i < 101; i += 10) {  // show horizontal graduation marks
    XDrawLine(XD,XW,XG,h+50,v+1+i,h+55,v+1+i);
    if(i > 0 && i < 100)              // and percentage annotations
    XDrawString(XD,XW,XG,h+58,v+6+i,showInt(100-i,2,0),2);
  }
  XSetForeground(XD,XG,GREEN);                // fill the box in bright green
  XFillRectangle(XD,XW,XG,h+2,v+102-p,40,p);  // to the current percentage

  int a = 0,  // positioning variables for the 3-character percentage readout
      b = 2;
  XSetForeground(XD,XG,WHITE);  // show readout figures in white
  if(p == 100) {                // if reached exactly 100%
    a = 3; b = 3;               // set for a 4-character readout '100%'
  }
  XDrawString(XD,XW,XG,h+13-a,v+118,showInt(p,b,1),b);  // show figures 
  XDrawString(XD,XW,XG,h+26+a,v+118,"%",1);             // show % sign
}




/* POWER CONSUMPTION SUB-TAB */

void T3cons() {
  char *B[] = {"12V DC EMERGENCY", "12V DC ESSENTIAL", "12V DC LIGHTING ",
               "12V DC COMPUTERS", "12V DC LOW POWER", "115V 400HZ POWER"};
  int
    H = 260,  // left edge of second column
    C[12],    // current percentage charge of the battery cells
    S[12],    // current specific gravity of battery cell acid in SGUs
    T[12],    // temperature of battery cell in tenths of degree centigrade
    v = 70;   // vertical coordinate of initial line of text
  lq = 0;     // charge percentage of the battery for the Left-Hand Bus
  rq = 0;     // charge percentage of the battery for the Right-Hand Bus

  XSetForeground(XD,XG,BLACK);  // clear the tab area
  XFillRectangle(XD,XW,XG,16,40,480,350);
  showMsg(0);                   // display blank message area
  XSetForeground(XD,XG,YELLOW);
  XDrawString(XD,XW,XG, 17,v,"THE CURRENT ELECTRICITY CONSUMPTION RATES:"
                             " FOR THE LEFT AND RIGHT-HAND BUSES",76);
  v += 20;
  XSetForeground(XD,XG,BLEEN);  // clear the tab area
  XDrawString(XD,XW,XG,17,v,"-------FOR THE LEFT-HAND BUSES------",36);
  XDrawString(XD,XW,XG, H,v,"-------FOR THE RIGHT-HAND BUSES-----",36);
  v+= 20;
  XSetForeground(XD,XG,YELLOW);
  XDrawString(XD,XW,XG, 17,v,"----BUS NAME---- MJd  %",23);
  XDrawString(XD,XW,XG,H,v,"----BUS NAME---- MJd  %",23);
  int p = 17; v+=20;
  XSetForeground(XD,XG,GREY);
  for(int i = 0; i < 6; i++) {
    XDrawString(XD,XW,XG, 17,v,B[i],16);
    XDrawString(XD,XW,XG,H,v,B[i],16);
    v += p;
  }
  v = 130; int h = 17, c=0, d=0;
  FILE *F = fopen("data/cons.dat","r");  // open the consumption data file
  XSetForeground(XD,XG,BLEEN);           // clear the tab area
  for(int i = 0; i < 12; i++) {          // for each of the 12 battery cells
    int a = getInt(F), b = getInt(F);
    XDrawString(XD,XW,XG,h+102,v,showInt(a,3,1),3);
    XDrawString(XD,XW,XG,h+126,v,showInt(b,2,1),2);
    v += p; if(i == 5) { v = 130; h = H; }
    if(i > 5)  d += a; else c += a;
  }
  fclose(F);  // close the battery file
  XSetForeground(XD,XG,YELLOW);
  p = 16; v -= 14;
  XDrawString(XD,XW,XG, 17,v+=p,"TOTAL...........     MJ/day x 10",32);
  XDrawString(XD,XW,XG,119,v,showInt(c,3,1),3);
  XDrawString(XD,XW,XG,262,v,"TOTAL...........     MJ/day x 10",32);
  XDrawString(XD,XW,XG,364,v,showInt(d,3,1),3);

  XSetForeground(XD,XG,BLEEN);
  XDrawString(XD,XW,XG, 17,v+=p+12,
  "MJd = Tenth-megajoules per day [average"
  " electrical consumption for each bus].",77);
  XDrawString(XD,XW,XG, 17,v+=p,
  "% = Each bus's contribution to the total"
  " electrical consumption on that side.",77);
  XDrawString(XD,XW,XG, 17,v+=p,
  "The two graphics show the average consumption"
  " on each bus as a percentage of",76);
  XDrawString(XD,XW,XG, 17,v+=p,
  "that respective bus's sustainable [green] generating capacity.",62);
  XSetForeground(XD,XG,YELLOW);
  T3percent(c*10/z3,171,97);
  T3percent(d*10/z3,H+154,97);
}




/* THE BATTERY SUB-TAB: Two batteries of 6 cells each. One battery supplies
   the left-hand bus and the other battery supplies the right-hand bus. 

   Cell acid comprises 65% water + 35% sulphuric acid.
   Its specific gravity changes linearly with charge.

   100% 1265 specific gravity of cell acid times 1000 measured at 27C
     0% 1120
   ---- ----
   100%  145 range from flat to full charge in SGUs [specific gravity x 1000]
     1%  1.45 SGUs difference

   If observed specific gravity is s, what is the % charge?

   Specific gravity at full charge varies with temperature from
   1294 SGU at 0C to 1266 SGU at 40C
   So over the 40 degree range, the specific gravity changes by 28 SGU.
   That is 28/40 = 0.7 SGU per centigrade degree.

   Each battery cell is 10cm radius by 65cm tall = 20420 cc
   It may thus contain 20000 cc [20 litres] of acid. 80–90 Wh/L
   Two batteries of 6 cells each @ 20 litres per cell = 240 litres.
   240 * 80 = 19200 watt-hours = 69120000 joules = 70MJ */

void T3batt() {
  int
    C[12],  // current percentage charge of the battery cells
    S[12],  // current specific gravity of battery cell acid in SGUs
    T[12],  // temperature of battery cell in tenths of a degree centigrade
    v = 70; // vertical coordinate of initial line of text
  lq = 0;   // charge percentage of the battery for the Left-Hand Bus
  rq = 0;   // charge percentage of the battery for the Right-Hand Bus

  XSetForeground(XD,XG,BLACK);  // clear the tab area
  XFillRectangle(XD,XW,XG,16,40,480,350);
  showMsg(0);                   // display blank message area
  XSetForeground(XD,XG,YELLOW);
  XDrawString(XD,XW,XG, 17,v,"CURRENT CONDITIONS OF THE BACK-UP BATTERIES"
                             " FOR THE LEFT AND RIGHT-HAND BUSSES",78);
  v += 20;
  XSetForeground(XD,XG,BLEEN);  // clear the tab area
  XDrawString(XD,XW,XG, 17,v,"<---BATTERY FOR LEFT-HAND BUS--->",33);
  XDrawString(XD,XW,XG,250,v,"<---BATTERY FOR RIGHT-HAND BUS--->",34);
  v+= 20;
  XSetForeground(XD,XG,YELLOW);
  XDrawString(XD,XW,XG, 17,v,"SGUs  TMP  CHG",14);
  XDrawString(XD,XW,XG,250,v,"SGUs  TMP  CHG",14);
  v = 130; int h = 17, s, t;
  FILE *F = fopen("data/batt.dat","r");  // open the battery data file
  XSetForeground(XD,XG,BLEEN);   // clear the tab area
  for(int i = 0; i < 12; i++) {  // for each of the 12 battery cells
    s = getInt(F),               // get the specific gravity
    t = getInt(F);               /* get the temperature
  
           current percentage charge of the k-th battery cell
           |   datum for computation is 100% charged
           |   |     <--percent short from 100% charge-->
           |   |      specific gravity at 100% charge at 27C acid temperature
           |   |      |      actual specific gravity of cell 'k' in SGUs
           |   |      |      |  Density compensation factor to make the dens-
           |   |      |      |  ity what it would be if the acid temperature
           |   |      |      |  were the standard 27 degrees centigrade.
           |   |      |      |           |
           |   |      |      |   <--------------->          */
    double q = 100 - (1265 - s - (t - 270) * 0.07) / 1.45;  /*
                                  |     |      |       |
      actual temperature of cell 'k'    |      |       |
             standard acid temperature 27      |       |
    change in specific gravity per unit temperature    |
    change in specific gravity for a 1% change in charge    */

    int c = round(q); if(i < 6) lq += c; else rq += c;
    C[i] = c;  // put percentage charge of cell 'k' in array
    S[i] = s;  // put the specific gravity of cell 'k' in the array
    T[i] = t;  // put the temperature of cell 'k' in the array
    XDrawString(XD,XW,XG,h   ,v,showInt(s,4,1),4);
    XDrawString(XD,XW,XG,h+36,v,showInt(t,3,1),3);
    XDrawString(XD,XW,XG,h+66,v,showInt(c,3,1),3);
    v += 17; if(i == 5) { v = 130; h = 250; }
  }
  fclose(F);  // close the battery file
  XSetForeground(XD,XG,BLEEN);
  int p = 16; v -= 7;
  XDrawString(XD,XW,XG, 17,v+=p,
  "SGU = Specific Gravity Units in which the"
  " specific gravity of water is 1000.",76);
  XDrawString(XD,XW,XG, 17,v+=p,
  "TMP = Temperature of the cell's acid measured"
  " in tenths of degrees Celcius.",75);
  XDrawString(XD,XW,XG, 17,v+=p,
  "CHG = The percentage to which each cell"
  " of each battery is currently charged.",77);
  XDrawString(XD,XW,XG, 17,v+=p,
  "The total energy stored in each battery when"
  " fully charged is 35 megajoules.",76);
  XSetForeground(XD,XG,YELLOW);  // clear the tab area
  XDrawString(XD,XW,XG, 17,v+=p+7,
  "Energy currently available    MJ: good for"
  "    hours at average consumption.",75);
  int l = lq / 6,  // percentage charge of the Left-Hand Bus's battery
      r = rq / 6;  // percentage charge of the Right-Hand Bus's battery
  T3percent(l,140,97);
  T3percent(r,375,97);
  s = 35 * (l + r);  // total energy available (both batteries) in MJ
  t = 24 * s / y3;   // hours of supply at average consumption
  XSetForeground(XD,XG,WHITE);  // clear the tab area
  XDrawString(XD,XW,XG,179,v,showInt(s,2,1),2);
  XDrawString(XD,XW,XG,275,v,showInt(t,2,1),2);
}




void T3consum() {
  // megajoules per month
  const int E[] = {1620,1440,972,810,774,749,763,770,749,803,1530,1620},
            D[] = {52,51,31,27,25,25,25,25,25,26,51,52};  // MJ per day

  /* Construct the graph 366 days by 55MJ to plot the above figures. */

}




/* DISPLAYS AUXILIARY POWER UNIT DATA */

void T3apu(int n) {             // n = landshare number
  XSetForeground(XD,XG,BLACK);  // clear the tab area
  XFillRectangle(XD,XW,XG,16,40,480,350);
  showMsg(0);                   // display blank message area
  int 
    a = 0,     // type of apu equipment
    b = 1,     // fuel type
    c = 1000,  // fuel tank capacity [litres]
    d = 770,   // litres of fuel currently in tank [1000 litres = 1 cu metre]
    e = 30,    // average consumption per day [megajoules]
    f = 0;     // average daily energy short-fall

  // DISPLAY THE INFORMATION LINE TITLES
  const char *S[12] = {
    "EQUIPMENT TYPE                                      ",  // a
    "FUEL TYPE                                           ",  // b
    "ENERGY VALUE OF THIS FUEL                   kJ/LITRE",
    "CAPACITY OF THE FUEL TANK                   LITRES  ",  // 2000 litres
    "QUANTITY OF FUEL CURRENTLY IN TANK          LITRES  ",  // d
    "TOTAL ENERGY VALUE IN TANK                  MJ      ",
    "OVERALL CONVERSION EFFICIENCY               %       ",
    "AMOUNT OF ELECTRICITY RECOVERABLE           MJ      ",
    "AVERAGE DAILY ENERGY CONSUMPTION            MJ      ",  // e 30 MJ 
    "AVERAGE DAILY ENERGY SHORTFALL              MJ      ",  // f 0 MJ
    "SURVIVAL AT FULL CONSUMPTION RATE           DAYS    ",
    "SURVIVAL AT SHORTFALL CONSUMPTION           DAYS    "
  };

  int H = 17, V = 70;
  XSetForeground(XD,XG,YELLOW);  // draw the column titles in yellow
  XDrawString(XD,XW,XG,H,V,"AUXILIARY POWER UNIT [APU]",26);
  XDrawString(XD,XW,XG,364,V,"LANDSHARE",9);
  XDrawString(XD,XW,XG,364,V+55,"FUEL TANKS",10);
  int i, v = V + 20;
  for(int i = 0; i < 12; i++) {
    XSetForeground(XD,XG,GREY);  // draw line titles in grey
    XDrawString(XD,XW,XG,H,v,S[i],52);
    v += 20;
  }

  // DEFINE ENGINE TYPE AND FUEL TYPE OPTIONS
  const char *E[] = {  // engine types
    "MINIATURE WATER-INJECTED GAS TURBINE","DIESEL ENGINE","GASOLINE ENGINE"};
  const int El[] = {36,13,15};  // number of letters in each string
  const char *F[] = {  // fuel types
    "DIESEL OIL","RAPE SEED OIL","KEROSENE","GASOLINE",
    "LIQUIFIED PETROLEUM GAS","ETHANOL"};
  const int Fl[] = {10,13,8,8,23,7},  // number of letters in each string

  /* ENERGY VALUE OF FUEL [KILOJOULES PER LITRE]
          diesel,   rape oil, kerosene, gasoline, LPG,      ethanol */
  P[] = { 37400, 34385, 34265, 33339, 26796, 23439 },

  /* ENERGY CONVERSION EFFICIENCY OF ENGINE TYPE [PERCENT]
          diesel, turbine, gasoline [turbine is water-injected] */
  Q[] = { 50,     54,      40 };

  // DISPLAY THE ENGINE TYPE AND FUEL TYPE
  XSetForeground(XD,XG,BLEEN);
  v = V;  // vertical coordinate for text item
  int
  l = El[a]; XDrawString(XD,XW,XG,H+(52-l)*6,v+=20,E[a],l);
  l = Fl[b]; XDrawString(XD,XW,XG,H+(52-l)*6,v+=20,F[b],l);

  // COMPUTE THE ENERGY VALUES
  int p = P[b];  // energy value of selected fuel [kJ/litre]
  double
    T = (double)d * (double)p / 1000,  // total energy in tank [megajoules]
    x = (double)Q[a] * 72 / 10000,     // overall  conversion efficiency
    R = T * x;                         // electrical energy realisable [MJ]

  // COMPUTE SURVIVAL TIMES
  int  g = 0;  // survival time at full daily consumption rate [days]
  l = 0;       // survival at short-fall daily consumption rate [days]
  if(y3 > 0) g = round(R / y3);
  if(f > 0) l = round(R / f);

  int N[] = {
    p,  // energy value of selected fuel [kilojoules per litre]
    c,  // fuel tank capacity [litres]
    d,  // amount of fuel currently in the tank [litres]
    (int)round(T),   // total thermal energy in tank [megajoules]
    (int)(x * 100),  // thermal to electrical energy conversion efficiency [%]
    (int)round(R),   // amount of electricity recoverable [megajoules]
    y3,  // average daily energy consumption [megajoules]
    f,  // average daily energy shortfall [megajoules]
    g,  // survival at full daily consumption rate [days]
    l   // survival at short-fall daily consumption rate [days]
  },
  h = H + 210;
  for(i = 0; i < 10; i++)
    XDrawString(XD,XW,XG,h,v+=20,showInt(N[i],8,0),8);

  // DISPLAY THE TANK PERCENTAGE GRAPHIC
  v = 90;
  XSetForeground(XD,XG,GREY);  // draw the Landshare Ref in grey
  XDrawString(XD,XW,XG,364,v,"REF     of    ",14);
  XSetForeground(XD,XG,BLEEN);  // draw the column titles in grey
  XDrawString(XD,XW,XG,388,v,showInt(n,3,1),3);
  XDrawString(XD,XW,XG,430,v,showInt(n,3,1),3);
  v = 145; g = 16;
  XDrawString(XD,XW,XG,364,v, "Computations based ",19);
  XDrawString(XD,XW,XG,364,v+=g, "on fuel tank volume",19);
  XDrawString(XD,XW,XG,364,v+=g,"of 500 litres each.",19);
  T3percent(100,330,v+=g);  // show the percentage full Tank 1
  T3percent( 54,414,v);     // show the percentage full Tank 2
}




/* DISPLAYS A TABLE SHOWING THE POWER DERIVED FROM EACH SOURCE-TYPE FOR EACH
   OF THE 3 FORMS OF ENERGY [THERMAL, MECHANICAL, ELECTRICAL] AND A GRAPHIC
   SHOWING THE PERCENTAGE OF THE TOTAL POWER THAT'S RENEWABLE [SUSTAINABLE] */

void T3source(int n) {                    // n = landshare number
  FILE *F = fopen("data/land.dat","r+");  // open the landshare data file

        /* record number of this landshare
           |     multiplied by 2048 [record size in bytes]
           |     |       plus the offset of the first power field
           |     |       |             */
  fseek(F,(n << 11) + 1450,SEEK_SET);  // seek start byte of first power field
  for(int i = 0; i < 8; i++)           // for each of the 8 power sources
    for(int j = 0; j < 3; j++)         // for each form of energy required
      b3[i][j] = getInt(F);            // get the average power
  fclose(F);                           // close the landshare file

  XSetForeground(XD,XG,BLACK);
  XFillRectangle(XD,XW,XG,16,40,480,350);
  showMsg(0);

  const char *S[8] = {"PETROLEUM","LP-GAS","BIO-ALCOHOL","SOLAR",
                      "WIND","WOOD","HYDRO","GEOTHERMAL"};
  const int L[8] = {9,6,11,5,4,4,5,10};  // word lengths for the above
  int
    i,j,c,
    h = 17,  // horizontal coordinate for start of text item
    v = 70,  // vertical coordinate for text item
    x,y,z,
    Y = 0,   // for total power produced
    Z = 0,   // for total sustainable power
    C[8] = {GRED,GRED,BROWN,GLEEN,GLEEN,GLEEN,GLEEN,GLEEN};

  for(i = 0; i < 8; i++) {  // for each of the 8 source types
    x = b3[i][0] + b3[i][1] + b3[i][2];
    b3[i][3] = x;           // total power for this source
    Y += x;                 // accumulate total power for all sources
    if(i > 2) Z += x;       // accumulate total sustainable power
  }
  XSetForeground(XD,XG,YELLOW);  // draw the column titles in yellow
  XDrawString(XD,XW,XG,h,v,"ENERGY SOURCE   THERMAL     MECHANICAL  "
                           "ELECTRICAL  TOTAL %     RENEWABLE %",75);
  v = 90;
  for(i = 0; i < 8; i++) {
    XSetForeground(XD,XG,C[i]);  // draw energy source names in grey-green
    XDrawString(XD,XW,XG,h,v,S[i],L[i]);
    v += 17;
  }
  v = 90;           // letter base height for first data row
  for(i = 0; i < 8; i++) {
    h = 42;         // horizontal coord of start of data row
    for(j = 0; j < 4; j++) {
      y = b3[i][3];  // total power from this source for all 3 types of use
      if(j == 3) y = Y;
      x = b3[i][j];  // power from the i-th source used for the j-th purpose
      if(y > 0) z = x * 100 / y; else z = 0;  // avoid division by zero
      if(x > 0) c = WHITE; else c = GREY;     // grey out inactive sources
      XSetForeground(XD,XG,c);
      XDrawString(XD,XW,XG,h+=72,v,showInt(x,4,1),4);  // display watts
      XDrawString(XD,XW,XG,h+36,v,showInt(z,2,1),2);   // display percentage
    }
    v += 17;  // drop down to next line of table
  }
  v+=3;
  XSetForeground(XD,XG,YELLOW);  // draw the column totals in grey
  XDrawString(XD,XW,XG,17,v,
  "TOTAL POWER     ................................",48);
  XDrawString(XD,XW,XG,h,v,showInt(Y,4,1),4);  // display total watts
  XDrawString(XD,XW,XG,h+36,v,"W",1);

  XSetForeground(XD,XG,GREY);  // draw the column titles in grey
  XDrawString(XD,XW,XG,390,v,"REF     of    ",14);
  XSetForeground(XD,XG,BLEEN);  // draw the column titles in grey
  XDrawString(XD,XW,XG,414,v,showInt(n,3,1),3);
  XDrawString(XD,XW,XG,456,v,showInt(n,3,1),3);
  

  T3percent(100*Z/Y,400,87);  // show the percentage renewable graphic
  v = 253; x = 16;
  XSetForeground(XD,XG,BLEEN); // grey-green
  XDrawString(XD,XW,XG,17,v,"The column headings give the type or types"
                            " of energy supplied by each source.",77);
  XDrawString(XD,XW,XG,17,v+=x,"In each column, the 4-digit block gives the"
                               " power [energy per unit time] sup-",77);
  XDrawString(XD,XW,XG,17,v+=x,"plied by each source, the last one being"
                               " the total of the previous 3 columns.",77);
  XDrawString(XD,XW,XG,17,v+=x,"The 2-digit number is the percentage of"
                               " the source's power used in that form.",77); 
  XDrawString(XD,XW,XG,17,v+=x,"Percentages in last column are of each"
                               " source's contribution to total power. ",77); 
}




/* REGENERATE THE TEST VALUES [IN WATTS] FOR AVERAGE POWER. The power derived
   from each source is divided into the power of each of the three forms:
   Thermal power, Mechanical power and Electrical power.

   Storage allocation in Landshare record: */

void T3regen(int n) {  /* n = number of the landshare being dealt with

                HEAT MECH ELEC    Power [in watts] from each source */
  int P[24] = {  00,  00,  00,    // PETROLEUM [diesel, kerosene, gasoline] 
                 20,  00,  00,    // LP-GAS
                 00,  00,  00,    // BIO-ALCOHOL
                 50,  00, 120,    // SOLAR
                 00,  00, 150,    // WIND
                 50,  00,  00,    // WOOD
                 00,  00,  00,    // HYDRO
                 00,  00,  00 };  // GEOTHERMAL

  FILE *F = fopen("data/land.dat","r+");  // open the landshare data file

        /* record number of this landshare
           |     multiplied by 2048 [record size in bytes]
           |     |       plus the offset of the first power field
           |     |       |             */
  fseek(F,(n << 11) + 1450,SEEK_SET);  // seek start byte of 1st power field
  for(int i = 0; i < 24; i++)          // for each of the 24 power values
    putInt(P[i],F);                    // store the average power
  fclose(F);                           // close the landshare file

  // BATTERY TEST DATA [SEPARATE FILE SO IT CAN BE UPDATED INDEPENDENTLY]
  int 
  Q[24] = {  // Specific gravity of each cell [SGUs, density of water = 1000]
    1200,1215,1188,1209,1219,1197,  // for the Left-Hand Bus
    1207,1216,1220,1193,1215,1212   // for the Right-Hand Bus
  },
  R[24] = {  // Cell acid temperature in tenth of degrees Celcius
    370,353,367,374,381,383,  // for the Left-Hand Bus
    365,376,353,374,377,366   // for the Right-Hand Bus
  };
  F = fopen("data/batt.dat","w");  // open the battery data file
  for(int i = 0; i < 12; i++) {    // for each of the 12 battery cells
    putInt(Q[i],F);                // store the specific gravity
    putInt(R[i],F);                // store the temperature
  }
  fclose(F);                       // close the battery file

  // CONSUMPTION TEST DATA [SEPARATE FILE SO IT CAN BE UPDATED INDEPENDENTLY]
  int
  U[24] = {0,27,36,39,45,147,      // tenths of megajoules per day
           0,26,33,41,44,144},
  V[24] = {0, 9,12,13,15,49,       // percentage power consumption
           0,11,14,14,14,47};
  F = fopen("data/cons.dat","w");  // open the consumption data file
  for(int i = 0; i < 12; i++) {    // for each of the 12 battery cells
    putInt(U[i],F);                // store the specific gravity
    putInt(V[i],F);                // store the temperature
  }
  fclose(F);                       // close the battery file
}




/* CONVERT SECONDS TO HRS:MIN:SEC STRING */

char *showTime(int t, int w) {
  static char S[9];
  if(w == 0)     // if unsigned output is requested
    S[0] = ' ';  // begin with space
  else           // otherwise, signed output is required
  if(t < 0) {    // so if the presented time is negative
    S[0] = '-';  // begin with a minus sign
    t = -t;      // and make the time figure positive
  } else         // if the presented time is positive
    S[0] = '+';  // begin with a plus sign
  int
    s = t % 60,  // remaining seconds 
    x = t / 60,  // total whole minutes
    m = x % 60,  // remaining minutes
    h = x / 60;  // total whole hours

  if(h < 10) S[1] = '0'; else S[1] = h / 10 + 48;
  S[2] = h % 10 + 48;
  S[3] = ':';
  if(m < 10) S[4] = '0'; else S[4] = m / 10 + 48;
  S[5] = m % 10 + 48;
  S[6] = ':';
  if(s < 10) S[7] = '0'; else S[7] = s / 10 + 48;
  S[8] = s % 10 + 48;
  return S;
}




/* COMPUTE "EQUATION OF TIME" FOR TODAY: an empirical solar time off-set [in
   seconds]. Corrects for perturbations in position of sun caused by eccentr-
   icity of Earth's orbit and Earth's axial tilt. Ranges from -15 to +17 min-
   utes. The constants below will serve well for decades but must eventually
   be adjusted. Called once a day just after midnight by T3sunTime().

   NOTES:          winter solstice    first midday of new year
                   |                  |
   (1) Period from 15:58 21Dec2021 to 12:00 01Jan2022 = 10.834722222 days
   (2) Period from 12:00 01Jan2022 to 06:52 04Jan2022 = 2.786111 days
                   |                  |
        first midday of new year      perihelion */

void T3EoT() {            // Earth's mean orbital angular velocity
  #define OV 0.017202791  /* 2 * pi / 365.2422 radians per day

         Orbital angle [in radians] that the Earth would have swept at its
         average angular speed from last Northern winter solstice to today
         |   Rate the Earth moves around its orbit [in radians per day]
         |   |     Current number of completed days since 01 January
         |   |     |    days from the winter solstice to 01 January (1)
         |   |     |    |               */
  double A = OV * (e3 + 10.834722222);  /* in radians
                   <-------|------->
    Elapsed time [in days] since the last Northern winter solstice.

         Angle Earth has moved around its orbit since last winter solstice
         |       First-order correction for Earth's orbital eccentricity,
         |       0.0167 multiplied by 2 to give half-turns [half-days].
         |       |      Days from 01 Jan to Earth's perihelion on 04 Jan (2)
         |       |                       |            */
  double B = A + 0.0334 * sin(OV * (e3 - 2.786111));  /* in radians
                                    <-----|----->
    Elapsed time [in days] since the most recent perihelion.

    Reciprocal of cos(0.41) [0.41 radians is the tilt of the Earth's axis]
                       |                             */
  double C = (A - atan(1.089945412 * tan(B))) / pi;  /*

  number of seconds        To determine which quadrant applies
  in half a day   |        to the result of the 'atan' function.
                  |        <-------|------->      */
  t3 = (int)round(43200 * (C - (int)(C + 0.5)));  // seconds of time.

  // B is also the Sun's ecliptic longitude [shifted by pi], so: 
  c3 = asin(sin(0.41) * cos(B));  // solar declination [in radians]
  if(b1 < 0) c3 = -c3;  // reverse sign V429810-2 for Southern Hemisphere
}




/* COMPUTE THE SINE OF THE SOLAR ELEVATION AT THE CURRENT 5-MINUTE PERIOD 'p'
   OF THE CURRENT DAY. Called only once from near end of T3showSolar(), which
   is executed once every 5 minutes. */

double T3solElev(int p) {

          /* rotation per 5-minute period 2pi / 288 [radians]
             |              current 5-minute period of the day [sun time]
             |              |   half the number of 5-minute periods per day
             |              |   |      */
  double h = 0.021816616 * (p - 144);  /* HOUR ANGLE [in radians]

  Compute SINE OF SOLAR ELEVATION at current time of day. Gives proportion
  of solar power per unit area hitting a flat horizontal photovoltaic array.
  |
  |    declination         latitude [in radians]         hour angle
  |              |         |                   |         |    */
  double a = sin(c3) * sin(l3) + cos(c3) * cos(l3) * cos(h);
  double x = asin(a);   // solar angle of elevation [in radians]
  x3 = (int)(x / RPD);  /* solar angle of elevation [in degrees]

  Taking the sine again attempts to simulate the attenuation of solar power
  according to how much atmosphere it must traverse at different times of day.
  Use 'a' not 'sin(a)' for a fully steered array tracking the sun continually.
               |
               |      maximum PV output is, say, 5000 watts
               |      |      */
  w3 = (int)(sin(a) * 5000); /* simulated photovoltaic output [in watts]

  COMPUTE THE SOLAR AZIMUTH IN DEGREES [ALL INPUT ANGLES IN RADIANS]
  |                    angle of declination     angle of elevation
  |                    |       latitude         hour angle       |
  |                    |       |                         |       |  */
  d3 = (int)(acos((sin(c3)*cos(l3) - cos(c3)*sin(l3)*cos(h))/cos(x)) / RPD);
  if(h > 0) d3 = 360 - d3;  // need complementary angle for afternoon
}




void T3showGraph(int n) {  // 'n' is the landshare record number
  XSetForeground(XD,XG,BLACK);
  XFillRectangle(XD,XW,XG,H3,V3,290,201);  // CLEAR THE GRAPH AREA

  // DRAW THE GRATICULE
  int
    h = H3,        // horizontal start-point for the vertical graticule lines
    v = V3 + 200,  // vertical end-point for vertical graticule lines
    i;
  XSetForeground(XD,XG,DARK);
  for(i = 0; i < 25; i++) {  // for each of the vertical graticule lines
    XDrawLine(XD,XW,XG,h,V3,h,v);
    h += 12;  // move across to next line position
  }
  v = V3;        // vertical start position of horizontal graticule lines
  h = H3 + 288;  // end-point of horizontal graticule lines
  for(i = 0; i < 11; i++) {  // for each of the horizontal graticule lines
    XDrawLine(XD,XW,XG,H3,v,h,v);
    v += 20;  // move down to next line position
  }
  // DRAW THE HORIZONTAL SCALE
  XSetForeground(XD,XG,GREY);
  v = V3 + 203;  // vertical position of the horizontal scale axis
  XDrawLine(XD,XW,XG,H3,v,h,v);  // horizontal scale axis
  XDrawString(XD,XW,XG,H3+67,V3+235,"Hour of the Day [Sun Time]",26);
  h = H3 - 6;      // horizontal start position of the horizontal scale figures
  int 
    q = v,        // vertical position of start-point of scale mark
    p = q + 7,    // vertical position of end-point of scale mark
    r = h + 6,    // initial horizontal position of the horizontal scale marks
    s = 0;        // scale figure [00,03,06,09,12,15,18,21,24]
  v = V3 + 220;    // vertical position of base-line of scale figures
  for(i = 0; i < 9; i++) {  // for each of the horizontal scale figures
    XDrawString(XD,XW,XG,h,v,showInt(s,2,1),2);
    XDrawLine(XD,XW,XG,r,p,r,q);  // horizontal scale marks
    h += 36;  // move across to next scale figure
    r += 36;  // move across to next scale mark
    s += 3;   // increment to next figure to be displayed
  }
  // DRAW THE VERTICAL SCALE
  h = H3 - 3;    // horizontal position of the vertical scale axis
  v = V3 + 200;  // vertical end-point of the vertical scale axis
  XDrawLine(XD,XW,XG,h,V3,h,v);  // draw the vertical scale axis
  h = H3 - 35;  // horizontal start of scale figures
  v = V3 + 5;   // vertical base of scale figures
  p = H3 - 3;   // horizontal start position of scale mark
  q = H3 - 8;   // horizontal end position of scale mark
  r = V3;       // initial vertical position of scale mark
  s = 5000;       // scale figure [00 to 12]
  XDrawString(XD,XW,XG,h,V3-15,"Watts",5);
  for(i = 0; i < 11; i++) {       // for each of the vertical scale figures
    XDrawString(XD,XW,XG,h,v,showInt(s,4,0),4);
    XDrawLine(XD,XW,XG,p,r,q,r);  // horizontal scale marks
    v += 20;  // drop down to next scale figure
    r += 20;  // drop down to next scale mark
    s-=500;      // decrement to next scale figure to be displayed
  }
}




/* DISPLAY THE WIND GAIN GRAPH. Shows the wind gain in kilowatts throughout
   the day so far. Called by every 5 minutes by T3sunTime(). */

void T3showOric(int n) {
  T3showGraph(n);   // display the blank graph
  int h = H3 + p3,   // start position of graph
      v = V3 + 200,  // vertical position of bottom of yellow bar
      i, p;
  XSetForeground(XD,XG,GREEN);
  XDrawString(XD,XW,XG,H3+50,V3-15,"ELECTRICAL POWER GAIN FROM WIND",31);
  XSetForeground(XD,XG,BLU);
  XDrawLine(XD,XW,XG,h,V3,h,v);  // time period line for this 5-min period
  h = H3;
  XSetForeground(XD,XG,GRN);    // colour for wind-power bars on graph
  int y, E = 0;                 // energy gained so far today
  double x = 0.01;
  for(i = 0; i < p3; i++) {     // for each 5-minute period so far today
    p = 3500 - x * 5000;        /* wind power [in watts]

        vertical position of the horizontal base line of the graph
        |   wind power output [in kW]
        |   |       */
    y = v - p / 30;  // height of vertical yellow bar above graph base line
    if(p > 0) {      // provided the result is above zero
      XDrawLine(XD,XW,XG,h,y,h,v);  // vertical line for this 5-min period
      E += p * 300;  // add energy [in joules] for this 5-min period
    }
    h++;  // move across to the next vertical line's position
    x = 3.68 * x * (1 - x);  // compute next random wind value
  }
  v = 178;
  XSetForeground(XD,XG,BLACK);  // black background for clearing
  XFillRectangle(XD,XW,XG,83,v+=20,20,15);  // clear period number
  XFillRectangle(XD,XW,XG,53,v+=70,38,15);  // clear the power output
  XFillRectangle(XD,XW,XG,53,v+=20,56,15);  // clear the energy
  v = 190;
  XSetForeground(XD,XG,BLEEN);  // green background for clearing
  XDrawString(XD,XW,XG,83,v+=20,showInt(p3,3,0),3);  // period number
  v += 50;
  XDrawString(XD,XW,XG,53,v+=20,showInt(p,6,0),6);   // current power output
  XDrawString(XD,XW,XG,53,v+=20,showInt(E/1000,9,0),9);  // energy kJ
}




/* DISPLAY THE SOLAR GAIN GRAPH. Shows the solar gain in kilowatts throughout
   the day so far. Called by every 5 minutes by T3sunTime(). */

void T3showSolar(int n) {
  T3showGraph(n);   // display the blank graph
  int h = H3 + p3,   // start position of graph
      v = V3 + 200,  // vertical position of bottom of yellow bar
      i;
  XSetForeground(XD,XG,YELLOW);
  XDrawString(XD,XW,XG,H3+26,V3-15,
    "ELECTRICAL POWER GAIN FROM SOLAR PANELS",39);
  XSetForeground(XD,XG,BLU);
  XDrawLine(XD,XW,XG,h,V3,h,v);  // time period line for this 5-min period
  h = H3;
  XSetForeground(XD,XG,YELL);   // colour for sun-power bars on graph
  double w, z;
  int y, E = 0;                    // energy gained so far today
  for(i = 0; i < p3; i++) {     /* for each 5-minute periods so far today
    
              number of the 5-minute period of the day counting from midnight
              |    */
    T3solElev(i);  /* compute solar geometry and power output of PV array

        vertical position of the horizontal base line of the graph
        |   PV output in watts [computed by T3solElev() above]
        |   |    25 watts per vertical pixel
        |   |    |    */
    y = v - w3 / 25;  // height of vertical yellow bar above graph base line
    if(w3 > 0) {      // provided the result is above zero
      XDrawLine(XD,XW,XG,h,y,h,v);  // vertical line for this 5-min period
      E += w3 * 300;  // add energy [in joules] for this 5-min period
    }
    h++;  // move across to the next vertical line's position
  }
  v = 78;
  XSetForeground(XD,XG,BLACK);  // black background for clearing
  XFillRectangle(XD,XW,XG,83,v+=20,60,74);  // clear solar diff etc.
  XFillRectangle(XD,XW,XG,83,v+=80,20,35);  // clear day number
  XFillRectangle(XD,XW,XG,83,v+=50,20,35);  // clear the solar elev & brng
  XFillRectangle(XD,XW,XG,53,v+=40,38,15);  // clear the power output
  XFillRectangle(XD,XW,XG,53,v+=20,56,15);  // clear the energy
  v = 90;
  XSetForeground(XD,XG,BLEEN);  // display the sun's current elevation
  XDrawString(XD,XW,XG,83,v+=20,showTime(s3-q3,1),9); // Solar Diff
  XDrawString(XD,XW,XG,83,v+=20,showTime(u3,1),9);    // GMT off-set
  XDrawString(XD,XW,XG,83,v+=20,showTime(t3,1),9);    // EoT offset
  XDrawString(XD,XW,XG,83,v+=20,showTime(t3+u3,1),9); // Total offset
  XDrawString(XD,XW,XG,83,v+=20,showInt(e3+1,3,0),3); // day number today
  XDrawString(XD,XW,XG,83,v+=20,showInt(p3,3,0),3);   // period number
  XDrawString(XD,XW,XG,83,v+=30,showInt(x3,3,1),3);   // solar elevation
  XDrawString(XD,XW,XG,83,v+=20,showInt(d3,3,1),3);   // solar bearing
  XDrawString(XD,XW,XG,53,v+=20,showInt(w3,6,0),6);   // current power output
  XDrawString(XD,XW,XG,53,v+=20,showInt(E/1000,9,0),9);  // energy kJ
}




/* COMPUTE & DISPLAY CLOCK-TIME & TRUE LOCAL SUN-TIME. A day contains 86400
   seconds of time. A circle contains 1296000 seconds of arc. So Earth rotates
   15 seconds of arc per second of time. Called from timing loop in main(). */

void T3sunTime(int w) {

  et = time(NULL);       // time in seconds since 00:00:00 UDT 01 Jan 1970
  tm = *localtime(&et);  // get the local time parameters
  e3 = tm.tm_yday;       // current day number within year [staring at day 0]

  /*       provided we're currently in the POWER tab AND either the
           |          SOLAR sub-tab is visible OR the
           |          |         WIND sub-tab is visible
           |          |         |  */
  if(tb == 3 && (a3 & 2 || a3 & 4)) {

  static int S = 0;      // second number from previous pass
  if(w == 0) {           // if a normal timed pass
    int s = tm.tm_sec;   // current second number [clock time]
    if(S == s) return;   // current second not yet expired
    S = s;               // note new second number for next pass
  }
  static int D = 0;        // day number from previous pass
  if(D != e3 || w == 1) {  // if day num changed or is an initial display pass
    D = e3;                // note the day number for the next pass
    T3EoT();               // re-do the daily computations
  }
  XSetForeground(XD,XG,BLACK);           // background colour
  XFillRectangle(XD,XW,XG,83,57,55,37);  // clear the time display area
  XSetForeground(XD,XG,BLEEN);           // colour for time displays
  // local official CLOCK TIME in seconds since midnight
  q3 = (int)(((tm.tm_hour * 60) + tm.tm_min) * 60 + tm.tm_sec);
  // display the local CLOCK TIME [according to official time zone]
  XDrawString(XD,XW,XG,83,70,showTime(q3,0),9);

     /* cast to 'int' because 'et' is a 'time_t' type
        |    seconds since 01 Jan 1970 00:00:00 GMT PGBR7A3FB79
        |    |  get remainder [seconds so far today] after dividing by
        |    |  | the number of seconds per day 016.815.226-67
        |    |  | |                  */
  s3 = (int)(et % 86400) + u3 + t3;  /* SECONDS SINCE MIDNIGHT LOCAL SUN TIME
                           |    | 
                           |    equation of time [in seconds of time]
                           landshare's longitudinal time off-set [seconds] */

  // display the fully corrected SUN TIME at the landshare location
  XDrawString(XD,XW,XG,83,90,showTime(s3,0),9);
  p3 = s3 / 300;  // number of the current 5-minute sun period of the day

  /* THE 5-MINUTE [300 SECOND] TIMER: if at the start of a new 5-minute
  solar period or its an initial display pass: update the solar graph. */
  if(s3 % 300 == 0 || w == 1) {
    if(a3 & 2) T3showSolar(1);
    else    
    if(a3 & 4) T3showOric(1);    
  }
  }  // close of entry condition
}




/* DISPLAY THE IN-FILL FOR THE 'SOLAR' DISPLAY OF THE 'POWER' TAB */

void T3showWind() {
  clearTab();  // clear the tab area
  showMsg(0);  // show the blank message area
  int V = 70, v = 50;
  XSetForeground(XD,XG,GREY);
  XDrawString(XD,XW,XG,17,v+=20,"CLOCK TIME",10);
  XDrawString(XD,XW,XG,17,v+=20,"SOLAR TIME",10);
  v+=100;
  XDrawString(XD,XW,XG,17,v+=20,"PERIOD NUM",10);
  v+=50;
  XDrawString(XD,XW,XG,17,v+=20,"POWER        WATTS",18);
  XDrawString(XD,XW,XG,17,v+=20,"ENERGY          kJ",18);
  T3sunTime(1);  // 1 = force immediate execution of the 5-minute functions
  T3Buts();      // display the states of the control buttons
}




/* DISPLAY THE IN-FILL FOR THE 'SOLAR' DISPLAY OF THE 'POWER' TAB */

void T3showSun() {
  clearTab();  // clear the tab area
  showMsg(0);  // show the blank message area
  int V = 70, v = 50;
  XSetForeground(XD,XG,GREY);
  XDrawString(XD,XW,XG,17,v+=20,"CLOCK TIME",10);
  XDrawString(XD,XW,XG,17,v+=20,"SOLAR TIME",10);
  XDrawString(XD,XW,XG,17,v+=20,"SOLAR DIFF",10);
  XDrawString(XD,XW,XG,17,v+=20,"GMT OFFSET",10);
  XDrawString(XD,XW,XG,17,v+=20,"EoT OFFSET",10);
  XDrawString(XD,XW,XG,17,v+=20,"TOT OFFSET",10);
  XDrawString(XD,XW,XG,17,v+=20,"DAY NUMBER",10);
  XDrawString(XD,XW,XG,17,v+=20,"PERIOD NUM",10);
  XDrawString(XD,XW,XG,17,v+=30,"SOLAR ELEV     DEG",18);
  XDrawString(XD,XW,XG,17,v+=20,"SOLAR BRNG     DEG",18);
  XDrawString(XD,XW,XG,17,v+=20,"POWER        WATTS",18);
  XDrawString(XD,XW,XG,17,v+=20,"ENERGY          kJ",18);
  T3sunTime(1);  // 1 = force immediate execution of the 5-minute functions
  T3Buts();      // display the states of the control buttons
}




/* ONE OF THE TAB'S CONTROL BUTTONS WAS CLICKED. Button numbers:
   0:SCAN, 1:STOP, 2:STDIN, 7:HELP. a3 = Tab 3 Button States. Buttons
   are numbered 0 to 7 from left to right. 'y' has a '1' in the position
   in the lower 8 bits of b1 corresponding to the number of the clicked
   button as follows:       

   [Called from 1 place in MEH().] */

void T3horizButClick(int x) {
  a3 &= ~0x40;         // kill the REGEN button
  switch(x) {          // switch according to button number
    case 0:            // SOURCE button
      if(!(a3 & 1)) {  // if the SOURCE button is bright
        a3 = 0;        // dim all buttons
        a3 |= 1;       // make SOURCE button bright
        T3source(1);   // show landshare record 1
      } break;
    case 1:
      if(!(a3 & 2)) {  // if the SOLAR button is bright
        a3 = 0;        // dim the other buttons
        a3 |= 2;       // make SOLAR button bright
        T3showSun();   // show SOLAR gain for record 1
      } break;
    case 2:
      if(!(a3 & 4)) {  // if the WIND button is bright
        a3 = 0;        // dim all buttons
        a3 |= 4;       // make WIND button bright
        T3showWind();  // show solar gain for record 1
      } break;
    case 3:
      if(!(a3 & 8)) {  // if the APU button is bright
        a3 = 0;        // dim buttons
        a3 |= 8;       // make APU button bright
        T3apu(1);      // show APU situation
      } break;
    case 4:               // BATT
      if(!(a3 & 0x10)) {  // if the BATT button is not bright
        a3 = 0;           // clear all buttons
        a3 |= 0x10;       // brighten it
        T3batt();         // display the battery sub-tab
      } break;
    case 5:               // CONSUM
      if(!(a3 & 0x20)) {  // if the CONSUM button is not bright
        a3 = 0;           // clear all buttons
        a3 |= 0x20;       // brighten it
        T3cons();         // display the CONSUMPTION   sub-tab
      } break;
    case 6:               // REGEN button
      if(!(a3 & 0x40)) {  // if REGEN button not bright
        a3 &= 0;          // clear all buttons
        a3 |= 0x40;       // brighten the REGEN button
        T3regen(1);       // regenerate the power test data for landshare 1
      } break;
    case 7:               // HELP button was clicked
      if(!(a3 & 0x80)) {  // if HELP button not bright
        a3 &= 0;          // clear all buttons
        a3 |= 0x80;       // brighten the HELP button
        helpPage("software/fep.html#power");
      }
  } T3Buts();  // re-display the buttons
}




void T3click() { /* not used at the moment */ }




/* SETS UP THE HEADINGS AND BUTTONS FOR THE POWER TAB.
   Re-displays the current command lines for a SCAN that
   is still active. Called only from 1 place in showTab(). */

void T3show() {
  if(a3 & 2) T3showSun();  // if SOLAR button bright, display SOLAR sub-tab
  else
  if(a3 & 4) T3showWind();  // if WIND button bright, display WIND sub-tab
  else
  if(a3 & 8) T3apu(1);    // if APU button bright, display APU sub-tab
  else
  if(a3 & 16) T3batt(1);  // if BATT button bright, display APU sub-tab
  else
  if(a3 & 32) T3cons(1);  // if CONS button bright, display APU sub-tab
  else {
    a3 |= 1;      // else press SOURCE button 
    T3source(1);  // and force display of the SOURCE sup-tab
  }
  T3Buts();       // display the control buttons
}





// FUNCTIONS PERTAINING TO TAB 2: WATER --------------------------------------


/* DRAW THE HORIZONTAL ROW OF CONTROL BUTTONS ACROSS THE BOTTOM OF THE
   WATER TAB. Called from 1 place each in T2showGraph(), T2showRainTank(),
   T2horizButClick(x) and T2show(). */

void T2showButs() {
  if(tb != 2) return;
  showButs();  // show 8 blank buttons across the bottom of the tab area
  const char   // annotate the horizontal row of control buttons
    *A[] = {"RAIN","WELL","LAKE","STEAM","COND","MAIN","RECYC","HELP"};
  const int 
    a[] = {15,15,15,12,15,15,12,15},  // horizontal offsets for word starts
    b[] = { 4, 4, 4, 5, 4, 4, 5, 4};  // number of letters in each word
  int h = 17, c;                      // left edge of left-most button

  for(int i = 0; i < 8; i++) {  // for each of the 8 buttons:
    if(a2 & 1 << i)             // if this button is 'on'
      c = WHITE;                // 'on' button is white
    else c = GREY;              // if this button is off, make it grey
    XSetForeground(XD,XG,c);    // colour for the lettering
    XDrawString(XD,XW,XG,h + a[i],377,A[i],b[i]);
    h += 59;  // move across to next button
  }
}




/* DISPLAY THE VERTICAL COLUMN OF BUTTONS IN THE RAIN SUB-TAB. Called from
   T2showGraph(), T2showRainGraph(), T2showRainTank() */

void T2showGraphVertButs() {
  char  // annotate the vertical row of control buttons
    *A[] = {"mm","m3","TANK","SPARE","SPARE","SPARE","SPARE","SPARE"};
  int 
    a[] = {21,21,15,12,12,12,12,12},  // horizontal offsets for word starts
    b[] = { 2, 2, 4, 5, 5, 5, 5, 5},  // number of letters in each word
    h = 93;                           // y coordinate of button block
  for(int i = 0; i < 8; i++) {        // for each of the 8 buttons:
    XSetForeground(XD,XG,DARK);       // shade for buttons' background
    XFillRectangle(XD,XW,XG,17,h-15,53,21);
    if(v2 & 1 << i)                   // if this button is bright [on]
      XSetForeground(XD,XG,WHITE);    // set colour for bright lettering
    else                              // otherwise
      XSetForeground(XD,XG,GREY);     // set colour for dull lettering
    XDrawString(XD,XW,XG,17+a[i],h,A[i],b[i]);
    h += 27;                          // move across to the next button
  }
}




/* YEAR-GRAPH: draw a histogram graph of the water captured per day. This
   function is called by each of the sub-tabs of the WATER tab. Called only
   from: T2showRainGraph(m). */

void T2showGraph(int scale) {
  // DRAW THE GRATICULE
  int
    h = H2 - 15,   // horizontal start-point for the vertical graticule lines
    v = V2 + 200,  // vertical end-point for vertical graticule lines
    m[] = {31,28,31,30,31,30,31,31,30,31,30,31},
    n[] = {7,6,7,7,7,6,7,7,7,8,7,7},
    i;
  clearTab();
  XSetForeground(XD,XG,DARK);
  for(i = 0; i < 13; i++) {  // for each of the vertical graticule lines
    XDrawLine(XD,XW,XG,h,V2,h,v);
    h += m[i];   // move across to next line position
  }
  v = V2;        // vertical start position of horizontal graticule lines
  h = H2 + 350;  // end-point of horizontal graticule lines
  for(i = 0; i < 11; i++) {  // for each of the horizontal graticule lines
    XDrawLine(XD,XW,XG,H2-15,v,h,v);
    v += 20;     // move down to next line position
  }

  // DRAW THE HORIZONTAL SCALE
  XSetForeground(XD,XG,GREY);
  v = V2 + 203;  // vertical position of the horizontal scale axis
  XDrawLine(XD,XW,XG,H2-15,v,h,v);  // horizontal scale axis
  h = H2 - 15;   // horizontal start position of the horizontal scale figures
  int 
    q = v,       // vertical position of start-point of scale mark
    p = q + 7,   // vertical position of end-point of scale mark
    r = h + 6;   // initial horizontal position of the horizontal scale marks
  v = V2 + 220;  // vertical position of base-line of scale figures
  char *S[] = {"JAN","FEB","MAR","APR","MAY","JUN",
               "JUL","AUG","SEP","OCT","NOV","DEC",""};
  for(i = 0; i < 13; i++) {  // for each of the horizontal scale figures
    XDrawLine(XD,XW,XG,h,p,h,q);  // horizontal scale marks
    if(i < 13)
    XDrawString(XD,XW,XG,h+n[i],v,S[i],3);
    h += m[i];  // move across to next month name
    r += m[i];  // move across to next scale mark
  }

  // DRAW THE VERTICAL SCALE
  h = H2 - 18;   // horizontal position of the vertical scale axis
  v = V2 + 200;  // vertical end-point of the vertical scale axis
  XDrawLine(XD,XW,XG,h,V2,h,v);  // draw the vertical scale axis
  h = H2 - 50;  // horizontal start of scale figures
  v = V2 + 5;   // vertical base of scale figures
  p = H2 - 18;  // horizontal start position of scale mark
  q = H2 - 23;  // horizontal end position of scale mark
  r = V2;       // initial vertical position of scale mark
  int
    s = 100,  // maximum figure on vertical scale
    ds = 5,   // increment in the actual scale figure
    dv = 20,  // pixel rise between vertical scale figures
    dr = 20,  // vertical rise to next higher scale mark
    I = 11;   // Number of iterations in the for() loop below

  XSetForeground(XD,XG,YELLOW);
  if(v2 & 1) {   // if the vertical WATER button is bright
    XDrawString(XD,XW,XG,h+36,V2-15,
      "MILLIMETRES OF RAIN FOR EACH DAY OVER THE LAST 12 MONTHS",56);

    switch(scale) {  // set the parameters for the graph scale
      case 1: ds = 10; break;
      case 2: s = 50; break;
      case 3: I = 6; s = 25; dv = 40; dr = 40;
    }
  }

  /* We need to convert millimetres of rain to litres of water collected. One
     millimetre of rain falling on 1 square meter of roof = 1 litre of water.
     The UTD's roof has a collecting area of 400 square metres. So the UTD's
     roof yields 400 litres of water for every millimetre of rain. We do this
     conversion simply by changing the vertical scale figures on the graph,
     which is given in cubic metres instead of litres. */

  else {
    XDrawString(XD,XW,XG,20,V2-15,"CUBIC METRES OF RAIN WATER "
    "PER DAY COLLECTED FROM THE UTD'S 400 SQ METRE ROOF",77);
    s = 40;     // maximum figure on vertical scale
    ds = 2;     // increment in the actual scale figure

    switch(scale) {  // set the parameters for the graph scale
      case 1: ds = 10; break;
      case 2: s = 20; break;
      case 3: I = 6; s = 10; dv = 40; dr = 40;
    }
  }
  XSetForeground(XD,XG,GREY);
  for(i = 0; i < I; i++) {       // for each of the vertical scale figures
    XDrawString(XD,XW,XG,h,v,showInt(s,4,0),4);
    XDrawLine(XD,XW,XG,p,r,q,r);  // horizontal scale marks
    v += dv;  // drop down to next scale figure
    r += dr;  // drop down to next scale mark
    s -= ds;  // decrement to next scale figure to be displayed
  }
  T2showButs();
  T2showGraphVertButs();
  showMsg(0);
}




/* The following 7 sub-tabs need a common graphical sub-routine function to
   display 366 days of water quantities. Rain water is the main supply. Well,
   lake, stream, condenser and water main make up the shortfall of rain.

   Household consumes 227 litres of water per person per day, which is 82855
   litres per person per year. The UTD supports up to 4 people, so its water
   consumption would be 331,420 litres per year. Assume a rainfall of 1 metre
   per year. Each square metre of roof horizontal area yields 1 cubic metre
   of water per year. The UTD roof has a collecting area of 400 square metres.
   It can therefore collect 400 cubic metres of water per year. That is
   400,000 litres per year. This means the rainfall would supply all the
   UTD's water with 17% to spare for evaporation and leakage. To account for
   a long dry season, storage for about 200 cubic metres of water is required.
   This would need to be aerated continually. Rain supplies 1096 litres per
   day on average. Record rainfall in a single day in Belo Horizonte = 171,8
   millimetres. Make the scale up to 200 mm. 

   Put vertical buttons to get separate mm graph, litre graph and tabulated
   figures displays. Called only from: T2showRain(). */

void T2showRainGraph() {
  int  // rain each month in millimetres
    r[] = {
      27,25,23,13,20,15,7,10,5,8,10,13,12,9,14,   // January 331
      5,7,12,11,5,10,12,10,5,3,5,5,13,10,7,6,
      6,7,9,10,8,6,4,5,5,3,4,5,7,9,7,             // February 178
      5,7,9,10,8,7,9,9,7,3,4,5,6,
      6,7,9,10,8,6,4,5,5,3,4,5,7,9,7,             // March 198
      5,7,9,10,8,7,9,9,7,3,4,5,6,7,6,5,
      3,2,3,2,3,3,2,1,3,2,2,3,3,2,3,              // April 82
      2,4,3,5,4,3,4,4,3,1,2,3,3,4,3,
      2,1,0,1,0,2,1,0,0,0,0,1,0,0,1,              // May 28
      2,0,1,0,0,2,2,0,2,1,0,3,3,0,3,1,
      0,1,0,0,2,0,0,3,0,0,0,0,0,0,1,              // June 11
      2,0,1,0,0,0,0,0,0,0,0,0,0,0,0,
      0,0,0,0,2,0,0,0,0,0,0,0,0,0,1,0,            // July 5
      2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
      0,1,0,0,2,0,0,3,0,0,0,0,0,0,1,0,            // August 11
      2,0,1,0,0,0,0,0,0,0,0,0,0,0,0,
      0,1,0,0,4,0,0,6,0,0,0,0,3,0,2,              // September 49
      2,0,4,0,0,0,0,0,0,4,5,6,7,5,0,
      0,5,6,7,5,3,7,6,0,0,9,0,3,0,5,0,            // October 110
      2,3,4,0,0,0,0,7,6,9,5,6,7,5,9,
      2,3,4,0,0,0,0,7,6,9,5,6,7,5,9,              // November 236
      10,15,16,17,15,13,17,16,12,0,9,5,3,0,5,
      12,13,14,0,0,0,0,12,11,10,12,15,14,13,11,9, // December 339
      11,12,13,11,13,13,17,16,12,0,9,15,18,20,25},

  /* In an implemented system, the above data would come from a file maint-
     ained by a daemon that is constantly updating daily rainfall levels. */

    h = H2 - 15,   // horizontal start-point for the vertical graticule lines
    v = V2 + 200,  // vertical end-point for vertical graticule lines
    i,             // general loop index variable
    x,             // current day's rainfall figure in millimetres
    y = 0,         // to capture the highest daily rainfall for the year
    m = 1;         // graph scale 1 = 0 to 100, 2 = 0 to 50, 3 = 0 to 25

  for(i = 0; i < 365; i++)     // find the highest rainfall for any day
    if(y < (x = r[i])) y = x;  // to facilitate automatic graph scaling

  /* If the highest rainfall figure is less than quarter-way up the 0 to 100
     scale, quadruple the scale to 0 to 25; else if it is less than half-way,
     double the scale to 0 to 50. */

  if(y < 25) m = 3; else if(y < 50) m = 2;

  T2showGraph(m);  // display the scale and graticule

  XSetForeground(XD,XG,YELLOW);
  XDrawLine(XD,XW,XG,h + e3,v,h + e3,v - 200);  // day of the year bar
  XDrawString(XD,XW,XG,h + 60,v + 38,
  "The yellow vertical line indicates today.",41);

  for(i = 0; i < 365; i++) {      // for each of the vertical graticule lines
    XSetForeground(XD,XG,GREEN);
    if((x = r[i]) > 100)          // if the rainfall for the day is off-scale
      XSetForeground(XD,XG,RED);  // display the line in red
    XDrawLine(XD,XW,XG,h,v,h,v - (x << m));
    h++;                          // move across to next line position
  }
  T2showGraphVertButs();
}




/* Need to store enough rain water for the dry season. Total rainwater coll-
   ected during a year is 400 cubic metres. The dry season lasts 180 days.
   Therefore we need to store half of the annual collection. Total annual
   collection is 1 metre of rain times 400 square metres of roof area = 400
   cubic metres [400000 litres] of rain water. Consumption is up to 1000
   litres per day [including local crop watering]. To span the dry season, we
   need to store, over the rainy season, 1000 x 180 days = 180000 litres of
   water. For contingency, make the tank capacity 200000 litres. That is 200
   cubic metres. The cube root of 216 is 6. The tank could have a height of 6
   metres with a base area of 36 square metres. A tank of radius 3385 mm and 6
   metres high would be needed. This would be a cylinder of almost minimum
   possible surface area for the volume contained. It could be implemented as
   an underground plastic tank set in a pit lined with concrete. The water in
   the tank must be kept aerated. 

   Called from: T2graphVertButs(x), T2showRain(). */

void T2showRainTank() {  // display the water collected graph
  int V[] = {
    1000,     // A annual rainfall in millimetres
     400,     // B catchment area in square metres
       0,     // C total rain capture in litres
     180,     // D length of dry season in days
       0,     // E storage needed in litres
   200000,    // F total capacity of the water tank in litres
   192000,    // G maximum content recorded in litres
    28000,    // H minimum content recorded in litres
   160000,    // I current content in litres
     1000,    // J consumption in litres per day
        0     // K number of days water supply stored in tank
    },
    s[] = {35,37,36,28,36,30,30,30,30,34,34},  // annotation string lengths
    X = 220,  // horizontal start position of the data block
    Y = 93,   // vertical datum of top line of data block
    v = Y,    // vertical coordinate of current data line
    l = 19,   // inter-line spacing within data block
    i = 0;    // loop variable

  char *S[] = {
    "A ANNUAL RAINFALL       MILLIMETRES",
    "B CATCHMENT AREA        SQUARE METRES",
    "C ANNUAL CAPTURE        LITRES [AxB]",
    "D DRY SEASON            DAYS",
    "E STORAGE NEEDED        LITRES [DxJ]",
    "F TANK CAPACITY         LITRES",
    "G CONTENT MAX           LITRES",
    "H CONTENT MIN           LITRES",
    "I CONTENT NOW           LITRES",
    "J CONSUMPTION           LITRES/DAY",
    "K SURVIVAL TIME         DAYS [I/J]"
  };
  V[ 2] = V[0] * V[1];  // rain captures = annual rainfall x roof area
  V[ 4] = V[3] * V[9];  // required storage = dry days x daily consumption
  V[10] = V[8] / V[9];  // survival time = current reserve / daily consumption
  clearTab();
  XSetForeground(XD,XG,YELLOW);
  XDrawString(XD,XW,XG,17,65,"RAIN WATER COLLECTION",21);
  XSetForeground(XD,XG,BLEEN);
  XDrawString(XD,XW,XG,17,314,
    "Rain collection surface, rain ducts & filter cleaning due in:     days."
    ,71);
  XSetForeground(XD,XG,WHITE);
  XDrawString(XD,XW,XG,389,314,"132",3);
  XSetForeground(XD,XG,BLEEN);
  for(i = 0; i < 11; i++) {  // for each data value
    XDrawString(XD,XW,XG,X,v,S[i],s[i]);
    v += 19;   // drop down to next data line
  }
  XSetForeground(XD,XG,WHITE);
  v = Y;       // vertical coordinate of top line of data block
  for(i = 0; i < 11; i++) {  // for each data value
    XDrawString(XD,XW,XG,321,v,showInt(V[i],6,0),6);
    v += 19;   // drop down to next data line
  }
  XSetForeground(XD,XG,YELLOW);
  XDrawString(XD,XW,XG,119,Y+42,"TANK",4);
  T3percent(V[8]*100/V[5],109,Y+51);  // show % level in rain tank
  T2showGraphVertButs();
  T2showButs();
  showMsg(0);  // clear the message line
}




/* DISPLAYS THE RAIN SUB-TAB OF THE WATER TAB. Which of the three displays is
   shown is determined by which of the sub-tab's vertical buttons is bright.
   Called from: T2graphVertButs(x), T2horizButClick(x), T2show() */

void T2showRain() {
  if(v2 & 1            // if the 'mm' [millimetres of rain] button
  || v2 & 2)           // or the 'm3' [cubic metres] button is bright
    T2showRainGraph(); // display the appropriate annual graph
  else if(v2 & 4)      // else if the 'DATA' button is bright
    T2showRainTank();  // display the Rain Data with tank percentage graphic
}



/* DISPLAYS THE 'WELL' TAB. Called from: T2show(), T2horizButClick(). */

void T2showWell() {
  clearTab();
  XSetForeground(XD,XG,YELLOW);  // colour for the title
  XDrawString(XD,XW,XG,17,70,"WATER DRAWN FROM WELL",21);
  XSetForeground(XD,XG,BLEEN);   // colour for the lettering
  char *S[] = {
    "In areas where rainfall is insufficient, water can be drawn from a   ",
    "narrow gauge bore hole. The UTD has an integral small-bore well drill",
    "as standard equipment for this purpose. Flushing and washing water is",
    "re-cycled. Only drinking water need be drawn from the bore hole then ",
    "filtered and tested for potability. Purifying can be done by passing ",
    "the water very slowly through a horizontal glass pipe half full of   ",
    "air while fully exposed to strong sunlight.                          "
  };
  int h = 50, v = 120;
  for(int i = 0; i < 7; i++) {
    XDrawString(XD,XW,XG,h,v,S[i],69);
    v += 17;
  }
  showMsg(0);  // clear the message line
  // future: graph of water drawn from well each day of the year.
}




void T2showLake() {
  clearTab();
  XSetForeground(XD,XG,YELLOW);  // colour for the lettering
  XDrawString(XD,XW,XG,17,70,"WATER PUMPED FROM LAKE",22);
  XSetForeground(XD,XG,BLEEN);   // colour for the lettering
  char *S[] = {
    "In areas where rainfall is insufficient, water can be pumped from a  ",
    "small lake within the landshare. Again, flushing and washing water   ",
    "should be recycled independently, only being topped up from the lake.",
    "The lake supplies drinking water only, which is purified by exposure ",
    "to strong sunlight. Alternatively, water can be pumped from a large  ",
    "lake within an anthropological community and distributed to indivi-  ",
    "dual landshares as a community project.                              "
  };
  int h = 50, v = 120;
  for(int i = 0; i < 7; i++) {
    XDrawString(XD,XW,XG,h,v,S[i],69);
    v += 17;
  }
  showMsg(0);  // clear the message line
  // water pumped from lake for each day of the year.
  // this excludes water used as a closed cycle coolant.
}




void T2showStream() {
  clearTab();
  XSetForeground(XD,XG,YELLOW);  // colour for the lettering
  XDrawString(XD,XW,XG,17,70,"WATER TAKEN FROM STREAM",23);
  XSetForeground(XD,XG,BLEEN);   // colour for the lettering
  char *S[] = {
    "In areas where rainfall is insufficient, water can be pumped from a  ",
    "small stream within the landshare. Again, flushing and washing water ",
    "should be recycled independently, and only topped up from the stream.",
    "The stream supplies drinking water only. This is purified by exposure",
    "to strong sunlight. Alternatively, water can be pumped from a large  ",
    "river within an anthropological community and distributed to indivi- ",
    "dual landshares as a community project.                              "
  };
  int h = 50, v = 120;
  for(int i = 0; i < 7; i++) {
    XDrawString(XD,XW,XG,h,v,S[i],69);
    v += 17;
  }
  showMsg(0);  // clear the message line
  // water drawn from stream for each day of the year.
}




/* DRINKING/COOKING WATER AIR CONDENSER SUB-TAB.
   Called from: T2horizButClick() and T2show(). */

void T2showCond() {
  int
    h = 17,  // horizontal start coordinate of print line
    v = 70,  // vertical coordinate of base of print line
    l = 15,  // inter-line spacing
    I = 15,  // number of lines of print
    s[] = {65,53,56,51,60,62,58,48,     // number of characters in eacj line
           42,42,42,48,59,56,47},
    D[] = {4,48,4,37,122,45,33,90,156,  // data values for each line
           1038,1038,2000,1100,280,0},
    d[] = {2,2,2,2,3,3,3,3,             // number of digits in each line field
           4,4,4,4,4,4,4};
  char *S[] = {
    "ACTIVITY:     % Percentage of time the water extractor is active.",
    "HUMIDITY:     % Relative humidity of the outside air.",
    "CONDENSER:    C Temperature of the water condenser coil.",
    "EXHAUST:      C Temperature of the exhaust dry air.",
    "FILTER 1:       days to go before air filter needs cleaning.",
    "FILTER 2:       days to go before water filter needs cleaning.",
    "ENERGY AV:      Mj of energy per litre of water extracted.",
    "PUMP:           days to next water pump service.",
    "FAN:             hours of service to date.",
    "COMPRESSOR       hours of service to date.",
    "OZONER:          hours of service to date.",
    "TANK SIZE:       Capacity of potable water tank.",
    "TANK NOW:        litres of potable water currently in tank.",
    "DAILY:           litres average amount consumed per day.",
    "SURVIVAL:        days reserve of potable water."
  };
  D[14] = D[12]/D[13];  // compute the survival time
  clearTab();
  XSetForeground(XD,XG,YELLOW); // colour for the title
  XDrawString(XD,XW,XG,h,v,
    "DRINKING/COOKING WATER AIR CONDENSER [COMPRESSOR TYPE]",54);
  v = 92;                       // base height of first data line
  XSetForeground(XD,XG,BLEEN);  // colour for the annotations
  for(int i = 0; i < I; i++) {  // print the data line annotations
    XDrawString(XD,XW,XG,h,v,S[i],s[i]);
    v += l;                     // drop down to next line
  }
  h += 72;  // horizontal start of data values
  v = 92;   // vertical bas line of first data value
  XSetForeground(XD,XG,WHITE);  // colour for the amounts
  for(int i = 0; i < I; i++) {  // print the data values
    int c = d[i];               // number of digits in current data value
    XDrawString(XD,XW,XG,h,v,showInt(D[i],c,0),c);
    v += l;                     // drop down to next line
  }
  h = 414;  // horizontal start of tank graphic
  v = 178;  // vertical start [top] of tank graphic
  XSetForeground(XD,XG,YELLOW);          // colour for tank graphic title
  XDrawString(XD,XW,XG,h+9,v,"TANK",4);  // display tank graphic title
  T3percent(D[12]*100/D[11],h,v+6);      // show % level in rain tank
  T2showButs();                          // display the sub-tab buttons
  showMsg(0);                            // clear the message line
}




void T2showMain() {
  clearTab();
  XSetForeground(XD,XG,YELLOW);  // colour for the lettering
  XDrawString(XD,XW,XG,17,70,"WATER FROM PUBLIC WATER MAIN",28);
  XSetForeground(XD,XG,BLEEN);   // colour for the lettering
  char *S[] = {
    "This sub-tab only exists for the transition from the present mode of ",
    "the economy to the new Landshare-based anthropological community mode",
    "of economy. In the initial stages of the transition, while the means ",
    "of acquiring water from rain, well, lake, stream or condensation has ",
    "not yet been implemented, it will be necessary to buy water from an  ",
    "old style water company.                                             "
  };
  int h = 50, v = 120;
  for(int i = 0; i < 6; i++) {
    XDrawString(XD,XW,XG,h,v,S[i],69);
    v += 17;
  }
  showMsg(0);  // clear the message line
  // water drawn from the public main for each day of the year.
}




/* DRINKING WATER PRODUCTION PLUS WATER RE-CYCLING FUNCTION
   Called from: T2horizButClick() and T2show(). */

void T2showRecycle() {
  int
    X = 160,  // horizontal start position of the data block
    Y = 70,   // vertical datum of top line of data block
    v = Y,    // vertical coordinate of current data line
    l = 18,   // inter-line spacing within data block
    i = 0,    // loop variable
    s[] = {28,28,32,30,32,32,28,28,32,32,30},  // text line lengths
    D[] = {5000,3000,10,146,400,600,3000,546,70,0,95},  // numeric data
    d[] = {0,0,0,24,0,0,0,0,0,0,24};     // horiz. off-set of data figures
  char *S[] = {
    "A Tank capacity:      litres",      // 00 text lines
    "B Tank Content:       litres",      // 01
    "C Wastage Rate:       litres/day",  // 02
    "D Maintenance due in:     days",    // 03
    "E From Rain: av.      litres/day",  // 04
    "F From Air: av.       litres/day",  // 05
    "G Tank Capacity:      litres",      // 06
    "H Tank Content:       litres",      // 07
    "I Consumption:        litres/day",  // 08
    "J Reserve supply:     days [H/I]",  // 09
    "K Maintenance due in:     days"     // 10
  };
  D[9] = D[7] / D[8];  // reserve supply of drinking water in days
  clearTab();
  XSetForeground(XD,XG,YELLOW);  // colour for the titles lettering
  XDrawString(XD,XW,XG,17,70,"WATER RE-CYCLING",16);
  XDrawString(XD,XW,XG,X,Y,"GREY WATER",10);
  XDrawString(XD,XW,XG,X,Y+108,"DRINKING WATER",14);
  X -= 12;
  XSetForeground(XD,XG,BLEEN);  // colour for the text lines lettering
  for(i = 0; i < 11; i++) {     // display the annotation texts
    v += l;                     // drop to first/next line
    XDrawString(XD,XW,XG,X,v,S[i],s[i]);
    if(i == 3) v += l + l;      // skip blank drop between data sections
  }

  X += 102;  // add offsets for the majority of data figures
  v = Y;     // reset the vertical displacement back to the top line

  XSetForeground(XD,XG,WHITE);  // colour for the figures
  for(i = 0; i < 11; i++) {     // display the data figures
    v += l;                     // drop to first/next line
    XDrawString(XD,XW,XG,X+d[i],v,showInt(D[i],4,0),4);
    if(i == 3) v += l + l;      // skip blank drop between data sections
  }

  // GRAPHIC OF WATER LEVEL IN DRINKING WATER TANK
  X = 390;  // horizontal start of tank graphic
  Y = 70;   // vertical start [top] of tank graphic

  XSetForeground(XD,XG,YELLOW);          // colour for tank graphic title
  XDrawString(XD,XW,XG,X+9,Y,"TANK",4);  // display tank graphic title
  T3percent(D[1]*100/D[0],X,Y+6);        // show % level in rain tank

  // GRAPHIC OF WATER LEVEL IN GREY WATER TANK
  X = 41;   // horizontal start of tank graphic
  Y = 176;  // vertical start [top] of tank graphic

  XSetForeground(XD,XG,YELLOW);          // colour for tank graphic title
  XDrawString(XD,XW,XG,X+9,Y,"TANK",4);  // display tank graphic title
  T3percent(D[7]*100/D[6],X,Y+6);        // show % level in rain tank

  T2showButs();  // display the sub-tab buttons
  showMsg(0);    // clear the message line
}




/* ONE OF THE VERTICAL BUTTONS ON THE RAIN SUB-TAB HAS BEEN CLICKED */

void T2graphVertButs(int x) {
  switch(x) {
    case 0:
      if(!(v2 & 1)) {      // if the RAIN button was clicked
        v2 |= 1;           // set the RAIN button bit
        v2 &= ~254;        // clear all the other bits
        T2showRain();      // display the rain graph
      } break;
    case 1:
      if(!(v2 & 2)) {      // if the WATER button was clicked
        v2 |= 2;           // set the WATER button bit
        v2 &= ~253;        // clear all the other bits
        T2showRain();      // display the water collected graph
      } break;
    case 2:
      if(!(v2 & 4)) {      // if the DATA button was clicked
        v2 |= 4;           // set the DATA button bit
        v2 &= ~251;        // clear all the other bits
        T2showRainTank();  // display the data graph
      } break;
  }
}




/* ONE OF THE XFER TAB'S CONTROL BUTTONS WAS PRESSED. Buttons are numbered
   0 to 7 from left to right. [Called from 1 place in MEH().] */

void T2horizButClick(int x) { 
  switch(x) {             // switch according to button number
    case 0:
      if(!(a2 & 1)) {     // if the RAIN button is dull
        a2 |= 1;          // illuminate it
        a2 &= ~0xFE;      // dull all the other buttons
        T2showRain();     // display the RAIN tab
      } break;            // 00+02+04+08+16+32+64+128=254
    case 1:
      if(!(a2 & 2)) {     // if the WELL button is dull
        a2 |= 2;          // illuminate it
        a2 &= ~0xFD;      // dull all the other buttons
        T2showWell();     // display the WELL tab
      } break;            // 01+00+04+08+16+32+64+128=253
    case 2:
      if(!(a2 & 4)) {     // if the LAKE button is dull
        a2 |= 4;          // illuminate it
        a2 &= ~0xFB;      // dull all the other buttons
        T2showLake();     // display the LAKE tab
      } break;            // 01+02+00+08+16+32+64+128=251
    case 3:
      if(!(a2 & 8)) {     // if the STREAM button is dull
        a2 |= 8;          // illuminate it
        a2 &= ~0xF7;      // dull all the other buttons
        T2showStream();   // display the STREAM tab
      } break;            // 01+02+04+00+16+32+64+128=247
    case 4:
      if(!(a2 & 0x10)) {  // if the COND button is dull
        a2 |= 0x10;       // illuminate it
        a2 &= ~0xEF;      // dull all the other buttons
        T2showCond();     // display the COND tab
      } break;            // 01+02+04+08+00+32+64+128=239
    case 5:
      if(!(a2 & 0x20)) {  // if the MAIN button is dull
        a2 |= 0x20;       // illuminate it
        a2 &= ~0xDF;      // dull all the other buttons
        T2showMain();     // display the MAIN tab
      } break;            // 01+02+04+08+16+00+64+128=223
    case 6:
      if(!(a2 & 0x40)) {  // if the TANK button is dull
        a2 |= 0x40;       // illuminate it
        a2 &= ~0xBF;      // dull all the other buttons
        T2showRecycle();  // display the TANK tab
      } break;            // 01+02+04+08+16+32+00+128=191
    case 7:               // HELP button was clicked
      helpPage("software/fep.html#water");
  } T2showButs();         // re-display the buttons
}




/* A CLICK HAS OCCURRED WITHIN TAB 2 OTHER THAN ON ONE OF THE BOTTOM BUTTONS
   Called from only one place in MEH(). */

void T2click() {
  int i = 0, y = 77;
  if(mx < 17 || mx > 69) return;  // outside horizontal limits of buttons
  for(i = 0; i < 8; i++) {
    if(my > y && my < y + 22) 
      break;
    y += 27;  // drop down to the next button
  }
  if(i < 3) T2graphVertButs(i);
}




/* DISPLAYS THE CONTENT AND BUTTONS FOR THE WATER TAB.
   Called only from 1 place in showTab(). */

void T2show() {
  int i;
  for(i = 0; i < 8; i++)
    if(a2 & 1 << i)
      break;
  switch(i) {
    case 0: T2showRain(); break;
    case 1: T2showWell(); break;
    case 2: T2showLake(); break;
    case 3: T2showStream(); break;
    case 4: T2showCond(); break;
    case 5: T2showMain(); break;
    case 6: T2showRecycle(); break;
  }
  T2showButs(); // display the control buttons
}





/* FUNCTIONS PERTAINING TO TAB 1: LAND----------------------------------------

   
/* LOAD THE LANDSHARE'S 2048-BYTE DATA RECORD. The data for each landshare is
   stored in a 2048-byte record. */

void T1getLand(int n) {  // 'n' is the landshare record number
  FILE *F = fopen("data/land.dat","r");
  fseek(F,n << 11,SEEK_SET);  // start byte of record [2048 * record number]

  /* GET THE COORDINATES OF THE LANDSHARE'S CENTRAL REFERENCE POINT
  A landshare's location is stated as the latitude, longitude and height of
  its central reference point. */

  b1 = getInt(F);  /* latitude [seconds of arc]        int = 4 bytes

        latitude of landshare [in seconds of arc]
        |    seconds of arc per radian
        |    |     */
  l3  = b1 / SPR;  // latitude of landshare [in radians]

  c1 = getInt(F);  /* longitude [seconds of arc]       int = 4 bytes

       LONGITUDE of landshare's central reference point [seconds of arc]
       |    15 seconds of arc per second of time
       |    |    */
  u3 = c1 / 15;  // lanshare's longitudinal time off-set from GMT [seconds]

  d1 = getShort(F);  // height [metres above sea level]  short = 2 bytes

  /* GET RADII FROM CENTRAL REFERENCE POINT TO BOUNDARY. A is mapped in terms
  of the radial distance [in cm] between its central reference point and its
  boundary for each of the 360 degrees of bearing. [720 bytes on disk] */
  int i;
  for(i = 0; i < 360; i++)  // of each of the 360 degrees of bearing
    r1[i] = getShort(F);      // get the 16-bit radius in centimetres

  /* GET HEIGHT OF POINT ON BOUNDARY ABOVE OR BELOW CENTRAL REFERENCE POINT
  The relief of the landshare is expressed as height difference in centi-
  metres between its central reference point and the point on its boundary
  for each of the 360 degrees of bearing. Each height is stored as a 16-bit
  quantity in the land share disk record. [720 bytes on disk] */

  for(i = 0; i < 360; i++) {  // of each of the 360 degrees of bearing
    int x = getShort(F);        // get the 16-bit height in centimetres
    h1[i] = 32768 - x;        // half full-house bias to make it bipolar
  }
  fclose(F);                  // close the landshare's file
}


 

/* STORE THE LANDSHARE'S DATA RECORD. The data for each landshare is stored
   in a 2048-byte record.

   lat/lng/hgt  10 bytes
   radii       720 bytes
   heights     720 bytes
   TOTAL      1450 bytes [bytes 0 to 1449 */

void T1putLand(int n) {  // 'n' is the landshare record number
  FILE *F = fopen("data/land.dat","r+");
  fseek(F,n << 11,SEEK_SET);  // start byte of record [2048 * record number]

  // PUT THE COORDINATES OF THE LANDSHARE'S CENTRAL REFERENCE POINT
  putInt(b1,F);  // latitude [seconds of arc]        4 bytes
  putInt(c1,F);  // longitude [seconds of arc]       4 bytes
  putShort(d1,F);  // height [metres above sea level]  2 bytes

  /* PUT RADII FROM CENTRAL REFERENCE POINT TO BOUNDARY.
     Total disk storage for this is 720 bytes. */
  int i;
  for(i = 0; i < 360; i++)  // of each of the 360 degrees of bearing
    putShort(r1[i],F);        // put the 16-bit radius in centimetres

  /* PUT HEIGHT OF POINT ON BOUNDARY ABOVE OR BELOW CENTRAL REFERENCE POINT
  The relief of the landshare is expressed as height difference [in cm] be-
  tween its central reference point and the point on its boundary for each of
  the 360 degrees of bearing. Each height is stored as a 16-bit quantity in
  the land share disk record. Requires 720 bytes of storage. */

  for(i = 0; i < 360; i++)    // of each of the 360 degrees of bearing
    putShort(h1[i] + 32768,F);  // half full-house bias to make it monopolar
  fclose(F);                  // close the landshares
}




/* GENERATE LANDSHARE TEST DATA */

void T1regen(int n) {
  b1 = -71707;   // latitude [example used: my apartment in Belo Horizonte]
  c1 = -158334;  // longitude
  d1 =  582;     // height
  int i;
  for(i = 0; i < 360; i++)  // create circular 1-hectare landshare
    r1[i] = 5641.8958355;   // radius of a 1-hectare circle [in centimetres]
  int x = -90;
  for(i = 0; i < 180; i++)  // Create boundary that starts 90 cm below
    h1[i] = x++;            // the height of the central reference point
  while(i < 360)            // rises to 90cm above it and then descends
    h1[i++] = x--;          // back down to 90cm below it.
  T1putLand(n);             // store this landshare's test data to disk
}




/* Formats the latitude or longitude of the current Geographic Feature,
supplied as an integral number of seconds of arc, within a 10-character
array ready for display by the T0updt() function. */

char *RadToDMS(int x, int flag) {  // convert radians to DEG:MIN:SEC format
  static char C[11] = "000:00:00N";  // template for display string
  if(flag == 0)    // If we're formatting a latitude
    if(x < 0) {
      C[9] = 'S';
      x = -x;
    } else
      C[9] = 'N';
  else             // else we're formatting a longitude
    if(x < 0) {
      C[9] = 'W';
      x = -x;
    } else
      C[9] = 'E';

  int y = x / 60,              // integral minutes
      z = x % 60;              // remaining seconds
  C[7] = (char)(z / 10 + 48);  // integral 10s of seconds
  C[8] = (char)(z % 10 + 48);  // integral remaining seconds
  x = y / 60;                  // integral degrees
  z = y % 60;                  // remaining minutes
  C[4] = (char)(z / 10 + 48);  // integral 10s of minutes
  C[5] = (char)(z % 10 + 48);  // integral remaining minutes
  y = x / 10;                  // 10s of degrees
  C[2] = (char)(x % 10 + 48);  // integral remaining degrees
  C[1] = (char)(y % 10 + 48);  // integral remaining 10s of degrees

  if(flag == 0)                  // if a latitude,
    C[0] = ' ';                  // prefix it with a space
  else                           // else it's a longitude, so show the
    C[0] = (char)(y / 10 + 48);  // integral remaining 100s of degrees
  return &C[0];                  // address of the output string
}




/* DISPLAY THE ACTUAL GRAPH ITSELF ON THE GRAPH SCREEN AREA.
   Called from only one place in t7graph(). */

void T1showHgt(int n) {
  T1getLand(n);
  XSetForeground(XD,XG,BLACK);
  XFillRectangle(XD,XW,XG,16,40,480,350);
  showMsg(0);

  // ANNOTATIONS FOR GRAPH'S VERTICAL AND HORIZONTAL SCALES
  char *S[6] = {"+10","+05"," 00","-05","-10"},
       *T[14] = {"000","030","060","090","120","150","180",
                 "210","240","270","300","330","360"};

  int H = 97,   // horizontal datum of graph
      V = 185;  // vertical datum of graph

  // DRAW THE DARK GRATICULE FOR THE GRAPH
  XSetForeground(XD,XG,DARK);
  int i, j = V-101;
  for(i = 0; i < 5; i++) {   // draw the long horizontal graticule lines
    XDrawLine(XD,XW,XG,H,j,H+360,j);
    j += 50;
  }
  j = H;
  for(i = 0; i < 13; i++) {  // draw the long vertical graticule lines
    XDrawLine(XD,XW,XG,j,V-100,j,V+99);
    j += 30;
  }

  // DISPLAY THE GRAPH'S VERTICAL SCALE
  XSetForeground(XD,XG,GREY);
  XDrawLine(XD,XW,XG,H-2,V-101,H-2,V+99);  // draw long vertical scale line
  j = V-101;
  for(i = 0; i < 21; i++) {  // draw short horizontal scale marks
    XDrawLine(XD,XW,XG,H-6,j,H-2,j);
    j += 10;
  }
  j = V-96;
  for(i = 0; i < 5; i++) {  // annotate the vertical scale
    XDrawString(XD,XW,XG,H-25,j,S[i],3);
    j += 50;
  }

  // DISPLAY THE GRAPH'S HORIZONTAL SCALE
  XDrawLine(XD,XW,XG,H,V+101,H+359,V+101);  // draw the long horizontal scale
  j = H;
  for(i = 0; i < 13; i++) {  // draw the short vertical scale marks
    XDrawLine(XD,XW,XG,j,V+101,j,V+106);
    j += 30;
  }
  j = H-8;
  for(i = 0; i < 13; i++) {  // annotate the horizontal scale
    XDrawString(XD,XW,XG,j,V+118,T[i],3);
    j += 30;
  }

  // DRAW THE AXIS LABELS
  XDrawString(XD,XW,XG,H+96,V+134,"BEARING FROM NORTH [DEGREES]",28);
  XDrawString(XD,XW,XG,H-59,V-2,"HEIGHT",6);
  XDrawString(XD,XW,XG,H-59,V+10,"metres",6);

  XSetForeground(XD,XG,BLEEN);  // set colour for the plot, then display it
  XDrawString(XD,XW,XG,H,V-120,
    "LANDSHARE'S BOUNDARY HEIGHT FOR EACH DEGREE OF BEARING",54);

  // DRAW THE GREEN BAR PLOTS INDICATING THE NUMBER OF NODES
  XSetForeground(XD,XG,GREEN);  // set colour for the plot, then display it
  for(i = 0; i < 360; i++) {    // for each of the 360 degrees of bearing
    int n = h1[i] / 10;         // get the height on this bearing
    XDrawLine(XD,XW,XG,H+i,V,H+i,V-n);
  }
}




/* DISPLAY THE PLAN VIEW OF A LANDSHARE */

void T1showPlan(int n) {
  T1getLand(n);
  XSetForeground(XD,XG,BLACK);
  XFillRectangle(XD,XW,XG,16,40,480,350);
  showMsg(0);
  int i, X=350,Y=200;
  double a=0, b, r, x, y;

  // DISPLAY THE PLAN GRATICULE
  XSetForeground(XD,XG,DARK);
  y=-100;
  for(i = 0; i < 11; i++) {
    XDrawLine(XD,XW,XG,X-100,Y+y,X+100,Y+y);
    XDrawLine(XD,XW,XG,X+y,Y+100,X+y,Y-100);
    y+=20;
  }
  // DISPLAY THE LANDSHARE BOUNDARY
  XSetForeground(XD,XG,GREEN);
  for(i = 0; i < 360; i++) {
    r = r1[i];       // radius on current bearing in metres
    a += r*r*RPD/2;  // add the area of each 1-degree sector
    r /= 50;         // rescale radius to half-metres
    b = i * RPD;     // convert the bearing from degrees to radians
    x = r * sin(b);  // compute horizontal offset of boundary point
    y = r * cos(b);  // compute vertical offset of boundary point
    XDrawPoint(XD,XW,XG,x+X,y+Y);
  }
  // DISPLAY THE SCALE AND COMPASS LINES
  XSetForeground(XD,XG,GREY);
  XDrawLine(XD,XW,XG,X-100,Y-150,X+100,Y-150);
  XDrawString(XD,XW,XG,X-108,Y-145,"W",1);
  XDrawString(XD,XW,XG,X+103,Y-145,"E",1);
  XDrawString(XD,XW,XG,X-30,Y-136,"100 metres",10);
  XDrawLine(XD,XW,XG,X-150,Y-100,X-150,Y+100);
  XDrawString(XD,XW,XG,X-152,Y-103,"N",1);
  XDrawString(XD,XW,XG,X-152,Y+113,"S",1);

  // DISPLAY THE DATA FIELD ANNOTATIONS
  Y=70; y=20;
  XDrawString(XD,XW,XG,17,Y,"REFERENCE:     of    ",21);
  XDrawString(XD,XW,XG,17,Y+=y,"LANDSHARE:",10);
  XDrawString(XD,XW,XG,17,Y+=y,"LATITUDE:",9);
  XDrawString(XD,XW,XG,17,Y+=y,"LONGITUDE:",10);
  XDrawString(XD,XW,XG,17,Y+=y,"HEIGHT:        metres",21);
  XDrawString(XD,XW,XG,17,Y+=y,"AREA:       sq metres",21);
  XDrawString(XD,XW,XG,17,Y+=y,"SOIL:",5);
  XDrawString(XD,XW,XG,17,Y+=y,"CLIMATE:",8);

  // DISPLAY THE DATA ITSELF
  Y=70;
  XSetForeground(XD,XG,BLEEN);
  XDrawString(XD,XW,XG,83,Y,showInt(n,3,1),3);
  XDrawString(XD,XW,XG,125,Y,showInt(n,3,1),3);
  XDrawString(XD,XW,XG,83,Y+=y,"D9A55BBAFC",10);
  XDrawString(XD,XW,XG,83,Y+=y,RadToDMS(b1,0),10);
  XDrawString(XD,XW,XG,83,Y+=y,RadToDMS(c1,1),10);
  XDrawString(XD,XW,XG,65,Y+=y,showInt(d1,6,0),6);
  XDrawString(XD,XW,XG,53,Y+=y,showInt((int)round(a/10000),5,0),5);
  XDrawString(XD,XW,XG,53,Y+=y,"Latosols red & yellow",21);
  XDrawString(XD,XW,XG,71,Y+=y,"Tropical wet & dry",18);
}

/* Soil Types: oxisols, aridsols, mollisols, alfisols, ultisols, spodsols,
   entisols, inceptisols, vertisols, histosols, and andisols. */
 

/* DRAW THE HORIZONTAL ROW OF CONTROL BUTTONS ACROSS BOTTOM OF LAND TAB
   Called from 1 place each in T1show(), T1click() and T1horizButClick(). */

void T1Buts() {
  if(tb != 1) return;  // bail out if SCAN tab not currently visible
  showButs();  // show 8 blank buttons across the bottom of the tab area
  const char   // annotate the horizontal row of control buttons
    *A[] = {"PLAN","VERT","SPARE","SPARE","SPARE","SPARE","REGEN","HELP"};
  const int 
    a[] = {15,15,12,12,12,12,12,15},  // horizontal offsets for word starts
    b[] = { 4, 4, 5, 5, 5, 5, 5, 4};  // number of letters in each word
  int h = 17;                       // x-coord of left side of first button
  for(int i = 0; i < 8; i++) {      // for each of the 8 buttons:
    if(a1 & 1 << i)                 // if the button is 'on'
      XSetForeground(XD,XG,WHITE);  // display it's lettering in white
    else
      XSetForeground(XD,XG,GREY);   // default colour for the lettering
    XDrawString(XD,XW,XG,h + a[i],377,A[i],b[i]);
    h += 59;  // move across to the next button
  }
}




/* MOUSE CLICKED ON A NON-BUTTON PART OF THE
   SCAN TAB. Called only from 1 place in MEH(). */

void T1click() {}




/* ONE OF THE LAND TAB'S CONTROL BUTTONS WAS PRESSED
   Called from 1 place only in MEH()] */

void T1horizButClick(int x) {
  showMsg(0);           // clear any pending message
  switch(x) {           // switch according to button number
    case 0:             // PLAN button clicked
      if(a1 & 1)        // if it's on
        a1 &= ~1;       // switch it off
      else {            // if it's off
        T1showPlan(1);  // show the landshare plan
        a1 &= ~0xFF;    // switch off all buttons
        a1 |= 1;        // and illuminate its button
      } break;
    case 1:             // VERT button clicked
      if(a1 & 2)        // if it's on
        a1 &= ~2;       // switch it off
      else {            // if it's off
        T1showHgt(1);   // show the landshare's height profile
        a1 &= ~0xFF;    // switch off all buttons  0xFF = 11111111
        a1 |= 2;        // and illuminate its button
      } break;
    case 6:             // REGEN button clicked
      if(a1 & 0x40)     // if the REGEN button is bright  0x40 = 01000000
        a1 &= ~0x40;    // switch it off [dim it]        ~0x40 = 10111111
      else {            // else, the REGEN button is dim
        T1regen(1);     // regenerate the land data
        showMsg(15);    // show the 'regenerated' message
        a1 |= 0x40;     // brighten the REGEN button
      } break;
    case 7:             // HELP button was clicked
      helpPage("software/fep.html#land");
  }                     // end of 'switch'
  T1Buts();             // re-display the buttons
}




/* DISPLAY THE STATIC CONTENT OF THE LAND TAB. 
   Called only from 1 place in showTab(). */

void T1show() {
  int i, x;
  if(a1 == 0) a1 = 1;       // if no button is bright, set button 1 bright
  for(i = 0; i < 8; i++) {  // for each of the 8 buttons
    x = 1 << i;             // make mask for the i-th bit
    if(a1 & x) {            // if the i-th button is bright
      a1 &= ~x;             // set it dim
      T1horizButClick(i);   // then virtually click on it
      break;                // and break out of the for() loop
    }
  }
}





/* FUNCTIONS PERTAINING TO TAB 0: NAME AND ADDRESS ---------------------------

   Need to devise a selector code to classify people as: members of one's
   community, members of one's coterie or simply an Ausländer. */

  
/* DISPLAY THE VERTICAL COLUMN OF BUTTONS IN THE CROPS SUB-TAB. Called from
   T5crops(), T5cropList(), T5cropButClick(). */

void T0showDiaryButs() {  // below are the names of the button annotations
  char *A[] = {"NEW","ED-PREV","ED-DONE","DONE","TOP","BOT","KILL","HELP"};
  int 
    a[] = {18, 6, 6,15,18,18,15,15},  // horizontal offsets for word starts
    b[] = { 3, 7, 7, 4, 3, 3, 4, 4},  // number of letters in each word
    h = 99;                           // y coordinate of button block
  for(int i = 0; i < 8; i++) {        // for each of the 8 buttons:
    XSetForeground(XD,XG,DARK);       // shade for buttons' background
    XFillRectangle(XD,XW,XG,17,h-15,53,21);
    if(v0 & 1 << i)                   // if this button is bright [on]
      XSetForeground(XD,XG,WHITE);    // set colour for bright lettering
    else                              // otherwise
      XSetForeground(XD,XG,GREY);     // set colour for dull lettering
    XDrawString(XD,XW,XG,17+a[i],h,A[i],b[i]);
    h += 27;                          // move across to the next button
  }
}




/* DRAW THE HORIZONTAL ROW OF CONTROL BUTTONS ACROSS THE BOTTOM OF THE TAB 0.
   Called once each from T0showNAD(), T0click(), T0horizButClick(), T0CR(). */

void T0Buts() {
  showButs();  // show 8 blank buttons across the bottom of the tab area
  const char   // annotate horizontal row of control buttons
    *A[] = {"NEW","KILL","NEXT","PREV","LIST","DIARY","NOTES","HELP"};
  const int 
    a[] = {18,15,15,15,14,12,12,15},  // horizontal offsets for word starts
    b[] = { 3, 4, 4, 4, 4, 5, 5, 4};  // number of letters in each word
  int h = 17, c;                      // left edge of left-most button, colour
  for(int i = 0; i < 8; i++) {        // for each of the 8 buttons:

       /* if one of the 4 indexes is active
          |   AND      we are dealing with the NEXT button
          |   |        | OR      we're dealing with the PREV button
          |   |        | |       |   colour the button lettering bluey green
          |   |        | |       |   |       */
    if(b0 > 0 && (i == 2 || i == 3)) c = GREEN;

    else if(a0 & 1 << i)   // if button 'i' is 'on' [bright]
      c = WHITE;           // colour its lettering white
    else if(i == 5 && V8)  // if it's the DIARY button and alert flag is set
      c = RED;             // colour its lettering red
    else                   // otherwise the button is simply off
      c = GREY;            // colour its lettering grey

    XSetForeground(XD,XG,c);  // set the colour for the button lettering
    XDrawString(XD,XW,XG,h + a[i],377,A[i],b[i]);  // draw the lettering
    h += 59;  // move across to the next button
  }
}




/* DIARY SCROLL: Displays 11 lines of the diary in order of the highlighted
   column heading. Called by: 

   FORMAT OF A DIARY RECORD:
   4-byte integer:PREV date of contact
   |   4-byte integer:NEXT date of proposed contact
   |   |   4-byte integer:DAYS to go to NEXT proposed contact
   |   |   |   4-byte integer: 'nad.dat' REF of person to whom
   |   |   |   |           the diary entry pertains
   1111000011110000 */

void T0diaryScroll() {
  V8 = 0;                       // clear the 'diary alert' flag
  D0 = 2;                       // set scrolling dispatcher flag
  XSetForeground(XD,XG,BLACK);  // clear the scrolling list area
  XFillRectangle(XD,XW,XG,87,84,397,210);
  e0 = Scroller(e0,N8);  /* update mouse bias position & display scroll bar

      The record number, within 'diary.dat' as a whole, at which the currently
      selected index 'x0' starts as follows:  x0  q
      |                                       0   000
      |   index number = column number        1   256
      |   |     times 256                     2   512
      |   |     |                             3   768  */
  int q = x0 << 8,  /*                        4  1024

          selected line within whole DIARY
          |    mouse scroll bias for DIARY
          |    |         */
      w = u0 - e0,       // screen line number of currently selected item
      v = 100, i,        // print-base of first data line, loop variable
      D = ratSysDate();  /* current system date in rationalised form

  LIST THE CURRENT SCREEN-FULL OF ITEMS */

  for(i = 1; i < 12; i++) {  /* for each of the 12 lines in the list

            record number within diary INDEX 'x0' of first displayed line
            |    number of the current data line being displayed
            |    |   */
    int c = e0 + i,  // index record number within current diary INDEX
        r = q + c;   /* index record number within 'diary.dat' as a whole
                        [for non-indexed situation r = c automatically] */

    int R = r;        // first assume listing is in order of DIARY entry
  
    if(x0 > 0) {      /* but if one of the indexes 1,2,3,4 is operative

            index file handle for 'diary.dat'
            |  index record number within 'diary.dat' as a whole
            |  |    multiplied by 16 bytes per record
            |  |    |     */
      fseek(FB,r << 4,SEEK_SET);  // seek start byte or INDEX record 'r'

      R = fgetc(FB);  // get the DIARY record number from the INDEX record
    }

    fseek(FB,R << 4, SEEK_SET);  // start byte of relevant DIARY record
    int p = getInt(FB),          // PREV date
        n = getInt(FB),          // DONE date
        d = getInt(FB),          // DAYS to go
        a = getInt(FB);          // NAD REF number [nad.dat record number]

    /* index record number within current index block
       |    current 'nad.dat' record number of highlighted line
       |    |   'nad.dat' record number from index file
       |    |   |   */
    if(c == u0) r0 = a;  // 'nad.dat' record number of highlighted line

    // set the display colour: normal or highlighted
    int C = BLEEN, Q; if(i == w) C = WHITE; XSetForeground(XD,XG,C);

    XDrawString(XD,XW,XG,90,v,showInt(R,3,1),3);  // display REF
    char *s;

    // show today's date in YELLOW instead of either BLEEN or WHITE
    Q = C; if(p == D) Q = YELLOW; XSetForeground(XD,XG,Q);
    if(p > 0) s = showInt(p,8,0); else s = "--------";
    XDrawString(XD,XW,XG,116,v,s,8);              // display PREV date

    // show today's date in YELLOW instead of either BLEEN or WHITE
    Q = C; if(n == D) Q = YELLOW; XSetForeground(XD,XG,Q);
    if(n > 0) s = showInt(n,8,0); else s = "--------";
    XDrawString(XD,XW,XG,169,v,s,8);              // display DONE date
    
    Q = BLEEN;      // DISPLAY THE DAYS FIGURE
    if(n == 0) {    // if this entry has no DONE date
      if(d < 0)     // then if we still have some days left to go
        Q = GREEN;  // show the number of days in green
      else {        // else, we're late or need to attend to it today
        Q = RED;    // so display the number of days in red
        V8 = 1;     // and set 'diary alert' flag
      }
      s = showInt(d,4,0);  // days figure to string
    } else s = "----";     // DONE date exists so DAYS figure is irrelevant
    XSetForeground(XD,XG,Q);
    XDrawString(XD,XW,XG,225,v,s,4);

    // FINALLY, GET AND DISPLAY THE PERSON'S FULL NAME

    XSetForeground(XD,XG,C);
    char S[36];                  // to contain the person's name
    fseek(FA,a << 10,SEEK_SET);  // start byte of person's name
    int l = fgetc(FA);           // get the first byte of current record
    if(l < 0) l = 0;             // invalid line length
    if(l > 36) l = 36;           // upper safety limit
    for(int i = 0; i < l; i++)   // for each character of the product name
      S[i] = fgetc(FA);          // get it from the disk record
    XDrawString(XD,XW,XG,257,v,S,l);  // display crop NAME

    v += 19;  // drop down to the next line in the list
  }
  fseek(FB,2,SEEK_SET);  // seek start byte or INDEX record 'r'
  fputc(V8,FB);  // store the diary alert flag in byte 1 of 'diary.dat'
}




/* DIARY: Enables you to note the 'previous contact date', the 'proposed next
   contact date' and the reference number of the PERSON concerned. Instead of
   entering the 'next contact date', you can enter a number of days into the
   future when you should re-contact the person and the corresponding date is
   then calculated. Called by T0diaryDoneKill(), T0diaryClick(), T0LIST(),
   T0DIAR(), T0Esc(), T0show(). */

void T0diary() {
  clearTab();  // clear the whole tab
  int v = 70;  // vertical coord of the lettering base of the title line

  // DISPLAY THE COLUMN HEADING BUTTONS
  XSetForeground(XD,XG,YELLOW);  // colour for the lettering
  XDrawString(XD,XW,XG,17,v,"DIARY:",6);
  XSetForeground(XD,XG,DARK);    // colour for the title buttons
  XFillRectangle(XD,XW,XG, 87,55, 24,21);  // REF button
  XFillRectangle(XD,XW,XG,116,55, 48,21);  // PREV button
  XFillRectangle(XD,XW,XG,169,55, 48,21);  // NEXT button
  XFillRectangle(XD,XW,XG,222,55, 30,21);  // DAYS button
  XFillRectangle(XD,XW,XG,257,55,226,21);  // NAME button

  // DISPLAY ALL COLUMN HEADINGS AND HIGHLIGHT THE SELECTED ONE
  if(m0 == 0)  // if none of the column headings is selected 
    m0 |= 8;   // select the default column heading: 'REF'
  char *G[] = {"REF","PREV","DONE","DAYS","FULL NAME"};
  int H[] = {91,128,181,225,340},  // horiz pixel start points of headings
      L[] = {3,4,4,4,9};           // number of letters in each heading
  for(int i = 0; i < 5; i++) {     // for each of the 6 heading buttons
    int c = GREY;                  // colour for dim button [default]
    if(m0 & 1 << i) c = WHITE;     // colour for highlighted button
    XSetForeground(XD,XG,c);       // colour for the lettering
    XDrawString(XD,XW,XG,H[i],v,G[i],L[i]);  // show heading lettering
  }
  XSetForeground(XD,XG,GLEEN);  // colour for the data list lettering
  XDrawString(XD,XW,XG,17,318, "Click a column heading to list the items "
                               "in the casting order of that column.",77);
  T0diaryScroll();  // display the scrollable list of diary lines
  T0showDiaryButs();
  T0Buts();
}




// DISPLAY NAME & ADDRESS FIELD NUMBER 'i'. Called by T0edit() and T0Esc().

void T0showField(int i) {
  en = fgetc(FA);          // get content length from first byte of field
  if(en > L0[i])           // cap the content length of the line editor field
    en = L0[i];
  for(i = 0; i < en; i++)  // store the content into the line editor array
    le[i] = fgetc(FA);
  while(i < 47)            // clear the remainder of the line editor array
    le[i++] = 0;
  XDrawString(XD,XW,XG,198,ey,le,en);
}




/* GET THE INNER BOX WIDTH FOR FIELD 'i'.
   Called by T0clearInsides(), T0clearInside() and T0showLE(). */

int T0BIW(int i) {
  int d = P0[i] - 1;
  if(i == 8 && a0 & 0x400) d = 20;  // for Ref number entry
  return d;
}




/* CLEARS ANY VISIBLE EDITING BOX AND CREATES A BACKGROUND FOR EACH FIELD THE
   SIZE OF THE INSIDE OF A FIELD BOX.
   Called from T0showNAD(), T0edit() and T0srch(). */

void T0clearBoxes() { 
  int d,                         // horizontal extent of the inside of the box
      v = 57;                    // y-coordinate of top of box area
  XSetForeground(XD,XG,BLACK);   // colour to clear all boxes
  for(int i = 0; i < 13; i++) {  // for each field of data
    XDrawRectangle(XD,XW,XG,194,v,P0[i],18);  // clear data box
    v += 20;                     // drop down to next field
  }
  XDrawRectangle(XD,XW,XG,375,217,24,18);  // clear Ref Number box
}




// CLEAR THE INSIDE OF ALL FIELD BOXES. Called by T0showNAD(), T0srch().

void T0clearInsides() {
  XSetForeground(XD,XG,DARK);  // colour for the display field background
  int d, v = 58;
  for(int i = 0; i < 13; i++) {
    XFillRectangle(XD,XW,XG,195,v,T0BIW(i),17);  // paint background
    v += 20;
  }
  XFillRectangle(XD,XW,XG,376,218,23,17);  // clear record number
  XFillRectangle(XD,XW,XG,418,218,23,17);  // clear occupied records
  XFillRectangle(XD,XW,XG,448,218,23,17);  // clear max record number
}




void T0showIdx() {  // Called by T0showNAD(), T0Show(), T0click().
  int h = 21, v = 210;
  char *S[] = {"POSTAL CODE","FAST SEARCH NAME","PERSONAL IDENTIFIER",
               "COUNTRY CODE [DOMAIN]"};
  int n[] = {11,16,19,21}, c;
  for(int i = 0; i < 4; i++) {
    if(b0 & 1 << i) c = GREEN; else c = GREY; 
    XSetForeground(XD,XG,c);  // shade for field name
    XDrawString(XD,XW,XG,h,v+=20,S[i],n[i]);
  }
}




void T0suppBut() {
  int c = GREY;
  if(D0 == 3) c = WHITE;
  XSetForeground(XD,XG,DARK);  // set the button background colour
  XFillRectangle(XD,XW,XG,425,238,58,77);
  XSetForeground(XD,XG,c);  // set the button background colour
  int v = 265, w = 16;
  XDrawString(XD,XW,XG,433,v,"FURTHER",7); v += w;
  XDrawString(XD,XW,XG,433,v,"INFORM-",7); v += w;
  XDrawString(XD,XW,XG,433,v," ATION ",7);
}




void T0showSupp() {}




/* DISPLAY THE FULL NAME & ADDRESS INFORMATION IN RECORD NUMBER 'r0'.
   Called from: T0Show(), T0trawl() twice, T0selKey(), T0regen(), T0KILL(),
   T0nextprev(), T0NEXT(), T0PREV(), T0search(), T0CR(). */

void T0showNAD() {
  D0 = 0;            // set scrolling dispatcher flag
  T0clearBoxes();    // clear any box to black
  T0clearInsides();  /* clear insides of boxes to grey

  global variable containing start byte of current name & address record
  |    number of the record within 'nad.dat' that must be displayed
  |    |     shift left by 10 bits to multiply by 1024 bytes per record
  |    |     |    */
  q0 = r0 << 10;  // to get the start byte within file of record number 'r'
  int j,          // offset of current field within the current record
      d;          // internal horizontal extent of current field
  ey = 70;        // vertical position of the first display line

  XSetForeground(XD,XG,BLEEN);   // colour for the display lettering
  for(int i = 0; i < 13; i++) {  // FOR EACH OF THE 13 FIELDS OF DATA
    j = O0[i];                   // offset of current field of data

      /* file handle for 'nad.dat'
          |  start byte of current name & address record
          |  |    offset of current field of data
          |  |    |              */
    fseek(FA,q0 + j,SEEK_SET);   // seek start byte of current data field

    // RETRIEVE AND DISPLAY THE FIELD CONTENT
    char S[48];
    int K = fgetc(FA), k,        // get content length from 1st byte of field
    L = L0[i]; if(K > L) K = L;  // cap content length to field length
    for(k = 0; k < K; k++)       // for each character of the field content
      S[k] = fgetc(FA);          // copy it into the display array S[]
    S[k] = 0;                    // then terminate it with a NULL character
    XDrawString(XD,XW,XG,198,ey,S,K);  // display the field
    ey += 20;                    // drop down one display line of 20 pixels
  }
  XDrawString(XD,XW,XG,379,230,showInt(r0,3,1),3);  // record number
  XDrawString(XD,XW,XG,421,230,showInt(N0,3,1),3);  // occupied records
  XDrawString(XD,XW,XG,451,230,showInt(H0,3,1),3);  // highest occupied
  if(b0) T0showIdx();

  // DISPLAY IN YELLOW ANY SEARCH TERM FOUND BY THE TRAWLER
  if(a0 & 0x100) {               // if the trawler is active
    XSetForeground(XD,XG,DARK);  /* set the field background colour

            index of the 1st char of the found search term within the field
            |    number of horizontal pixels per character
            |    |   x-coordinate of the beginning of the field
            |    |   |     */
    int x = h0 * 6 + 198,  // x-coordinate of start of found search term
        y = g0 * 20 + 70;  /* y-coordinate of its lettering base line
            |    |    |
            |    |    y-coordinate of the lettering base line of the top field
            |    inter-line drop between fields [in pixels]
            number [0 to 7] of the field containing the found search term

   height of the top of the field above the lettering base line
                                  |
       y-coord of the base line   |  length of the search term [in pixels]
       of the field lettering |   |  |    horizontal pixels per character
                              |   |  |    |       */
    XFillRectangle(XD,XW,XG,x,y - 12,en * 6,17);  // clear search term area
    XSetForeground(XD,XG,YELLOW);                 // colour for search term
    XDrawString(XD,XW,XG,x,y,le,en);              // display the search term
  }
  T0suppBut();
  T0Buts();  // display the control buttons for the name & address Tab
}




/* DISPLAY THE STATIC CONTENT OF THE NAME & ADDRESS TAB.
   See global variables section for Tab 0.
   Called only from 1 place in T0clearClick(), T0Esc(), showTab(). */

void T0Show() {
  clearTab();
  int h = 21, v = 50;
  XSetForeground(XD,XG,GREY);  // shade for field names
  XDrawString(XD,XW,XG,h,v+=20,"FULL NAME",9);
  XDrawString(XD,XW,XG,h,v+=20,"HOUSE/BUILDING",14);
  XDrawString(XD,XW,XG,h,v+=20,"STREET/ROAD",11);
  XDrawString(XD,XW,XG,h,v+=20,"DISTRICT/ZONE",13);
  XDrawString(XD,XW,XG,h,v+=20,"CITY/TOWN",9);
  XDrawString(XD,XW,XG,h,v+=20,"COUNTY/MUNICIPALITY",19);
  XDrawString(XD,XW,XG,h,v+=20,"STATE/ETHNIC COUNTRY",20);
  XDrawString(XD,XW,XG,h,v+=20,"SOVEREIGN STATE/COUNTRY",23);
  T0showIdx();
  v = 290;
  XSetForeground(XD,XG,GREY);  // shade for field names
  XDrawString(XD,XW,XG,h,v+=20,"SELECTOR KEY",12);
  XDrawString(XD,XW,XG,355,230,"REF     of     [   ]",20);  // record numbers
  showMsg(0);
  T0showNAD();  // show first record
}




/* TEST TO SEE WHETHER OR NOT RECORD 'r' IS CURRENTLY OCCUPIED
   Called from T0trawl() and T0regen(). */

int T0isoc(int r) {

     /* file handle for main name & address file: 'nad.dat'
        |  record number within 'nad.dat'
        |  |    DIVIDED by 8 ['record occupied' bits per byte]
        |  |    |             */
  fseek(FA,r >> 3,SEEK_SET);  // seek byte containing the bit for record 'r'

  /* get the byte containing record 'r's 'occupied' bit
     |           a '1' bit mask
     |           |  shifted left by an amount equal to
     |           |  |   the remainder when 'r' is divided by 8 [bits per byte]
     |           |  |   | */
  if(fgetc(FA) & 1 << r % 8)  // if the bit pertaining to record 'r' is set
    return 1;                 // return '1' [indicating that it's occupied]
  return 0;                   // else return '0' showing it is unoccupied
}




/* DISPLAY THE SCROLLABLE LIST OF PEOPLE
   Called from T0list(), MWup() & MWdn(). */

void T0listScroll() {
  D0 = 1;                       // set scrolling dispatcher flag
  XSetForeground(XD,XG,BLACK);  // clear the scrolling list area
  XFillRectangle(XD,XW,XG,17,84,397,210);

  k0 = Scroller(k0,N0);  // update mouse wheel position & display scroll bar
  int v = 100,           // lettering base of first data line
      w = l0 - k0,       // line number of currently selected person
      I[] = {3,0,1},     // index number from column number
      b = 0;             // start byte of current index

  if(d0 < 3)  /* if one of the indexed fields is selected:
          skip over the 262144 [0x40000] bytes of PEOPLE data records
          |        index number 0,1,2 of current index
          |        |  column number of current sort field
          |        |  |     times 4096 bytes per index
          |        |  |     |     */
    b = 0x40000 + (I[d0] << 12);  // start byte of current index

  for(int i = 1; i < 12; i++) {   // for each of the 11 lines in the list
    int r = i + k0;               // record number of current 1st line in list

    if(d0 < 3) {  /* if listing in the order of one of the indexes 0, 1, 2

      SEEK THE START BYTE OF RECORD 'r' WITHIN THE INDEX I[d0] IN 'nad.dat'
            index file handle for 'nad.dat'
            |  start byte of current index
            |  |    record number within current index
            |  |    |    multiplied by 16 bytes per record
            |  |    |    |              */
      fseek(FA,b + (r << 4),SEEK_SET);

      r = fgetc(FA);  /* 'nad.dat' record number from index file

      GET RECORD NUMBER IN 'nad.dat' OF THE CURRENTLY HIGHLIGHTED LINE
         number of the first currently-displayed line
         |   list line number of first display line
         |   |     line number within complete total list in file
         |   |     |   current 'nad.dat' record number of highlighted line
         |   |     |   |    'nad.dat' record number from index file
         |   |     |   |    |  */
      if(i + k0 == l0) r0 = r;
    } 
    else        // else we are listing in plain REF number order, so
      r0 = l0;  // the 'nad.dat' record number is simply the line number

    if(r > H0) break;           // break if 'r' is beyond last occupied record
    if(!(T0isoc(r))) continue;  // skip if this record is unoccupied
    int B = r << 10;            // start byte of the current record
    char s[41];
    int X[] = { 20, 40,111,233},  // x-coords for start of columns content
        Y[] = {  2, 10, 14, 41},  // maximum content length for each column
        Z[] = {485,384,409,  0};  // filed offsets in the 'nad.dat' record
    XSetForeground(XD,XG,BLEEN);  // colour for normal line
    if(i == w)                    // if doing the highlighted line
      XSetForeground(XD,XG,WHITE);  // set lettering colour to WHITE
    for(int k = 0; k < 4; k++) {  // for each of the 5 columns
      fseek(FA,B+Z[k],SEEK_SET);  // start byte of field in 'nad.dat' record
      int l = fgetc(FA), m;       // length of the field text
      if(l < 0) l = 0;            // guard against corrupted disk
      if(l > (m = Y[k])) l = m;   // limit content length to column width
      for(int i = 0; i < l; i++)  // for each byte of the column content
        s[i] = fgetc(FA);         // put it sequentially in array s[]
      XDrawString(XD,XW,XG,X[k],v,s,l);  // display the column content
    }
    XDrawString(XD,XW,XG,208,v,showInt(r,3,1),3);  // show the REF number

    v += 19;  // drop down to the next line in the list
  }
}




/* DISPLAY NAMES AND ADDRESSES AS A SCROLLABLE LIST
   Called from T0horizButClick().  */

void T0list() {
  clearTab();  // clear the whole tab
  showMsg(0);  // kill the trawl message
  int v = 70;  // vertical coordinate of the lettering base of the title line

  // DISPLAY THE COLUMN HEADING BUTTONS
  XSetForeground(XD,XG,DARK);  // colour for the title buttons
  XFillRectangle(XD,XW,XG, 17,55, 18,21);  // COUNTRY CODE button 2 chars
  XFillRectangle(XD,XW,XG, 40,55, 66,21);  // POSTAL CODE button 10 chars
  XFillRectangle(XD,XW,XG,111,55, 90,21);  // SEARCH NAME button 14 chars
  XFillRectangle(XD,XW,XG,206,55, 22,21);  // REF button          3 chars
  XFillRectangle(XD,XW,XG,233,55,250,21);  // FULL NAME button   41 chars

  // DISPLAY ALL COLUMN HEADINGS AND HIGHLIGHT THE SELECTED ONE
  char *G[] = {"CC","POST CODE","SEARCH NAME","REF","FULL NAME"};
  int H[] = {20,46,122,209,325},   // horiz pixel start points of headings
      L[] = {2,9,11,3,9};          // number of letters in each heading
  for(int i = 0; i < 5; i++) {     // for each of the 6 heading buttons
    int c = GREY;                  // colour for dim button [default]
    if(c0 & 1 << i) c = WHITE;     // colour for highlighted button
    XSetForeground(XD,XG,c);       // colour for the lettering
    XDrawString(XD,XW,XG,H[i],v,G[i],L[i]);  // show heading lettering
  }
  v = 100;  // base line of the 1st data row's lettering
  XSetForeground(XD,XG,GLEEN);  // colour for the data list lettering
  XDrawString(XD,XW,XG,30,316, "Click a column heading to list people "
                               "in the casting order of that column.",74);
  T0listScroll();  // display the scrollable list of people data lines
  T0Buts();  // display the horizontal buttons of the PEOPLE tab
}




// CLEAR THE INSIDE OF A FIELD BOX. Called from T0srch() and T0Esc().

void T0clearInside() {         
  XSetForeground(XD,XG,DARK);  // colour for the display field background
  XFillRectangle(XD,XW,XG,195,ey-12,T0BIW(f0),17);  // paint the background
}




/* BOX THE FIELD 'f0' IN THE GIVEN COLOUR.
   Called from T0edit(), T0srch(), T0Esc(). */

void T0drawBox(int c) {
  XSetForeground(XD,XG,c);  /* set the specified colour for the box

         vertical top corner of box to be displayed
                                |   outer box width of field 'f0'
                                |   |            */
  XDrawRectangle(XD,XW,XG,194,ey-13,P0[f0],18);  // draw the box
}




/* ONE OF THE NAME & ADDRESS DATA FIELDS WAS CLICKED. This function displays
   a yellow box round the clicked field and sets up the line editor for edit-
   ing the field's content. Called from only one place in T0click(). */

void T0edit(int f) {     // f=number of screen field that's just been clicked
  if(ea == 0) {          // if no field is currently being edited
    f0 = f;              // field number of the clicked line
    T0clearBoxes();      /* clear all name & address fields

    start byte of field 'f0' in record 'r0' of name & address file 'nad.dat'
    |    first byte of the record 'r0' of the name & address file 'nad.dat'
    |    |    off-set [in bytes] of the start byte of field 'f0'
    |    |    |  number of the clicked field
    |    |    |  |    */
    p0 = q0 + O0[f0];
    fseek(FA,p0,SEEK_SET);        // seek start byte within 'nad.dat'
    XSetForeground(XD,XG,WHITE);  // colour for field being edited
    T0showField(f0);     // get and display the clicked field in white
    T0drawBox(YELL);     // box field 'f0' in the given colour
    ea = 1;              // activate line editor
  } else {               // else, if this field is already being edited
    if(f != f0) return;  // exit if click was not in field being edited
    doCursor(0);         // erase the cursor from its old position
  }
  ec = (mx - ex) / 6;  // compute cursor's character position within field
  if(ec > en)          // if click was beyond the end of the line content 
    ec = en;           // place cursor at end of current line content
  doCursor(1);         // draw the cursor
}




/* A NAME & ADDRESS FIELD NAME WAS CLICKED. This function sets up the corr-
   responding data field with a green box ready for entering a search term.
   Called from only one place in T0click(). */

void T0srch(int f) {
  if(ea == 0) {   // if no field is currently being edited
    f0 = f;       // set clicked screen field as the one being edited
    T0clearBoxes();    // clear all name & address fields
    T0clearInsides();  // clear the insides of all field boxes
    T0drawBox(GREEN);  // box field 'f0' in the given colour
    for(int i = 0; i < 48; i++)
      le[i] = 0;  // and clear the line editor array
    en = 0;       // set content of line editor array to zero
    ec = 0;       // set cursor to start of field
    ea = 1;       // activate line editor
    doCursor(1);  // draw the cursor
  } else {        // else, if this field is already being edited
    if(f0 != f)   // if click was not in field being edited
      return;     // exit 
    doCursor(0);  // erase the cursor from its old position
  }
  T0clearInside();  // clear the inside of the editing box
}




/* SET UP A TRAWL SEARCH. This searches all the non-indexed fields of all the
   name & address records for the occurrence of the entered search term.
   Before comparing, it converts both the search term and the retrieved field
   content to upper case letters only.
   Called only from 1 place each in T0NEXT() and T0CR(). */

void T0trawl() {
  int B = r0 << 10;  // start byte of record '1' [record number times 1024]
  a0 |= 0x100;       // set the 'trawler active' bit
  showMsg(6);        // show message "Trawling for search term."
  SUC(en,le);        // convert search term to upper case only

  for(int i = r0; i <= H0; i++) {  // for each record in 'nad.dat'
    if(T0isoc(i) == 0) continue;   // skip this record if it is unoccupied
    int o = 0;                     // offset of first field is zero
    for(int j = 0; j < 8; j++) {   // for each of the record's first 8 fields
      int
        K,      // length of the content of the current field
        k;      // individual character retrieved from disk record
      char
        S[48],  // to hold the field content read from the disk record
        *s;     /* return pointer from the string comparator strstr();

            file handle for 'nad.dat'
            |  start byte of current record
            |  |   offset of current field within record
            |  |   |             */
      fseek(FA,B + o,SEEK_SET);  /* seek start byte of current field

          if the length of this field's content
          |   as retrieved from disk
          |   |            is more than the permitted maximum of 47 bytes
          |   |            |       truncate it to the permitted 47 bytes
          |   |            |       |    */
      if((K = fgetc(FA)) > 47) K = 47;  // get length of field content
      if(K < 0) K = 0;                  // zero it if negative

      for(k = 0; k < K; k++)  // copy the field content
        S[k] = fgetc(FA);     // into the array S[]
      S[K] = 0;               // terminate the content with a NULL character
      SUC(K,S);               /* convert it S[] content to upper case only

          points to character within field at which found search term begins
          |          content of current field
          |          | entered search term
          |          | |       if search term is not found in this field
          |          | |       |       increment offset to start of next field
          |          | |       |       |        loop back to do next field
          |          | |       |       |        |            */
      if((s = strstr(S,le)) == NULL) { o += 48; continue; }  /*

      char number within array [anything from zero to number of elements - 1]
      |    is the pointer to the character of interest within the array
      |    |   minus the name of the array [the pointer to the first element]
      |    |   |    */
      h0 = s - S;   // index of start of found search term within the field
      r0 = i;       // number of record containing found search term
      g0 = j;       // number of the field containing the found search term
      T0showNAD();  // display name & address with found search term
      showMsg(7);   // display the "search term found" message in green
      return;       // can return immediately without breaking the loops
    }
    B += 1024;   // advance to the start byte of the next record
  }  
  showMsg(8);    // show message that the search term could not be found
  a0 &= ~0x100;  // kill the trawl mode
  r0 = 1;        // set back to first record
  T0showNAD();   // and display it
}




/* THE CLICK WAS IN A BLANK PART OF THE NAME & ADDRESS TAB. This function
   clears all active modes and re-displays the name & address screen.
   Called from 3 places in T0click(). */

void T0clearClick() {
  b0 = 0;        // set to NEXT/PREV by Ref Number [not in any indexed order]
  a0 &= ~0xF00;  /* clear the 'search mode', 'trawler mode', 'Ref No. entry
                    mode' and 'selector key mode' bits */
  T0Show;        // re-display field names & button states
}




/* EDIT REF NUMBER FIELD TO SEARCH FOR A NAME & ADDRESS RECORD BY REF NUMBER
   Called only from one place in T0click(). */

void T0edRef() {
  ea = 1;       // 1: editor active; 0: editor inactive
  ey = 230;     // line editor's current vertical lettering base coordinate
  ex = 379;     // line editor's current horizontal lettering coordinate
  en = 0;       // current length of the line editor's field content
  ec = 0;       // current cursor position [line editor]
  a0 |= 0x400;  // indicates Ref Entry Mode
  f0 = 8;       // screen field number
  XSetForeground(XD,XG,GREEN);             // colour for search box
  XDrawRectangle(XD,XW,XG,375,217,24,18);  // show data box
}




/* PRINT THE NAME & ADDRESS RECORD THAT MATCHES THE ENTERED SELECTOR CODE
   Called only once from T0selKey() below. */

void T0prtRec(int r, int B, FILE *F) {
  char S[48];
  fprintf(F,"%s ",showInt(r,3,1));  // print the record number
  for(int i = 0; i < 13; i++) {     // for each of the 9 NAD fields
    fseek(FA,B + O0[i],SEEK_SET);   // get start byte of [next] field 
    int j, J = fgetc(FA),           // get the field's content length
    L = L0[i]; if(J > L) J = L;     // cap content length to field length
 
    /* load the content of the field into the S[]
    array and terminate it with a NULL character */

    for(j = 0; j < J; j++) S[j] = fgetc(FA); S[j] = 0;

    if(i == 12)             // if this is the last field
      fprintf(F,"%s\n",S);  // print it and terminate it with a CR
    else                    // for all other fields
      fprintf(F,"%s, ",S);  // print and terminate with a comma + space
  }
}




/* EXAMINE THE PRESENTED CHARACTER 'c'. IF IT IS A LOWER CASE LETTER,
   MAKE IT UPPER CASE. IF IT IS NETHER A LETTER NOR A NUMBER, RETURN
   A NULL CHARACTER. Called from T0selKey() and T0key(). */

int AZ09(int c) {
  if(c >= 'a' && c <= 'z') c -= 32;  // make 'c' an upper case only letter

  /* if the value in variable 'c' is lower in ASCII code order than the
  character '0' or it is between character '9' and character 'A' or it is
  between character 'Z' and character 'a' or it is beyond character 'z'
  then make it character '0' */

  if(c < '0' || c > '9' && c < 'A' || c > 'Z' && c < 'a' || c > 'z') c = 0;
  return c;
}




/* LIST THE NAMES & ADDRESSES THAT MATCH THE ENTERED KEY SETTINGS. The best
   policy for security & legality is to print the list to a file 'list.txt'.
   Called from only one place in T0CR(). */

void T0selKey() {
  ea = 0;                           // disable the line editor
  FILE *F = fopen("list.txt","w");  // open the list file for writing
  fprintf(F,"# SELECTOR KEY LIST MATCHING THE ENTERED PATTERN %s\n\n",le);
  int i,
      g = 0,        // 'print all records' flag
      B = 1504;     // start byte of SELECTOR KEY field of record 1
  if(le[0] == '*')  // if just an asterisk entered as search term
    g = 1;          // set to print all records
  else
    for(i = 0; i < en; i++)   // limit the characters of the search
      le[i] = AZ09(le[i]);    // pattern from 0 to 9 and from A to Z

  for(i = 1; i <= H0; i++) {  // FOR EACH NAME & ADDRESS RECORD:
    fseek(FA,B,SEEK_SET);     // get start byte of its SELECTOR CODE field
    int f = 1;            // set flag to include current record
    if(g == 0)            // if not ordered to print all records
      if(fgetc(FA) == 0)  // if nothing's ever been entered into its selector 
        f = 0;            // code field, then don't consider this record
      else

        // ONLY SELECT RECORDS THAT MATCH THE ENTERED SELECTOR KEY 
        for(int j = 0; j < en; j++)  /* for each char of entered SELECTOR KEY

             if the character of the entered pattern
             |     is different from
             |     |  the character from the disk record
             |     |  |                set the flag to 'fail'
             |     |  |                |           */
          if(le[j] != fgetc(FA)) { f = 0; break; } /*
                                          |
          then break out of the character test loop because
          there's no point in testing any further characters */

    if(f == 1)  /* if the match condition survived

               number of the current record
               | start byte of current record's SELECTOR CODE field
               | |   to get start byte of the current record
               | |   |   file handle for the output text file 'list.txt'
               | |   |   |    */
      T0prtRec(i,B - 480,F);  // print the name & address record

    B += 1024;  // advance to start byte of next record's SELECTOR KEY field
  }
  fclose(F);     // close 'list.txt'
  a0 &= ~0x800;  // kill SELECTOR KEY search mode
  T0showNAD();   // re-display the original name & address
  system("gedit list.txt &");  // display the list in text editor
}




/* SAVE TO DISK THE TOTAL NUMBER OF CURRENTLY OCCUPIED RECORDS 'N0'
   AND THE NUMBER OF THE HIGHEST OCCUPIED RECORD 'H0'.
   Called from T0regen(), T0occu(), T0newIdx(). */

void T0putN0H0() {
  fseek(FA,32,SEEK_SET);  // seek byte 32 of record zero of 'nad.dat'
  fputc(N0,FA);           // store the number of occupied records
  fputc(H0,FA);           // store number of highest occupied record
  fflush(FA);             // store it to disk immediately
}




/* GENERATE OR RE-GENERATE THE DIARY INDEXES. All indexes have 16-byte rec-
   ords: 1 byte for record number and 4 bytes each for the PREV date, NEXT
   date and DAYS to go columns. There are 4 indexes: REF order, PREV order,
   NEXT order and DAYS to go order. They are stored one after the other in
   'diary.dat' [handle=FB] and contain 4096 bytes each, total: 20480 bytes. 
   Called once each in T0diaryPostEdit(), T0diaryDoneKill(), T0diaryClick(). 

   The file 'diary.dat' contains the following:
   256 * 16-byte records for the diary itself
   256 * 16-byte records for the PREV dates index
   256 * 16-byte records for the NEXT dates index
   256 * 16-byte records for the DAYS to go index
   256 * 16-byte records for the NAD REF index
   ---
  1280 * 16 = 20480 bytes total

   Record Zero of 'dairy.dat' contains:
   Bype 0: N8 current number of items in the diary
   Byte 1: V8 current setting of 'diary alert' flag
   Byte 2: W8 the 'sort required' bits for the 4 indexed fields

   Format of a DIARY record: the DIARY's data fields:
   4-byte integer:PREV date of contact
   |   4-byte integer:NEXT date of proposed contact
   |   |   4-byte integer:DAYS to go to NEXT proposed contact
   |   |   |   4-byte integer:'nad.dat' REF of person to whom
   |   |   |   |         the diary entry pertains
   1111000011110000

   Format of a diary INDEX record for each of the 4 indexes respectively:
   1-byte diary ref number
   |1-byte string length [always 8]
   ||8-byte string version of integer for PREV,NEXT, DAYS or NAD REF field
   |||
   0111111111000000  remainder of 16-byte record is filled with nulls */

void T0diarySort(int i) {  // 'i' = number 1 to 3 of the index to be sorted
  if(i == 0) return;       /* we never need to sort the REF index so exit

         indicates that the DAYS to go index is already sorted
         |indicates that the DONE dates index is already sorted
         ||indicates that the PREV dates index is already sorted
         |||bit corresponding to the REF number index [not used]
         ||||
     00001110  Q = Byte 1 of 'diary.dat'
     |
     |    if index i's sorted bit is set [index 'i' doesn't need sorting]
     |    |                */
  if(W8 & 1 << i) return;  // then exit without doing anything

  int P[] = {0,0,4,8,12},  // 10000111100001 field offsets
      p = P[i],            // start byte of INDEX i's data field within
      t,                   // Record Zero of the DIARY, elapsed time [DAYS]
      d = ratSysDate(),    /* current system date in rationalised form

      start byte of Record Zero of the 'i-th' INDEX within 'diary.dat'
      |   index number 1,2,3,4
      |   |    multiply by 4096 bytes per index
      |   |    |         */
      q = i << 12,
      j, J = N8 + 1, k;  // loop variables

  char *s;      // to hold a record's content
  showMsg(20);  /* show the 're-sorting' message

  COPY ALL RECORDS OF THE 'REF' INDEX INTO THE 'i-th' INDEX
             current DIARY record number
             |              */ 
  for(j = 1; j < J; j++) {  // for each of the diary records from 1 to 255
    p += 16;                // advance to [first/next] DIARY record

    if(i == 3) {               // if doing the DAYS index
      fseek(FB,p-8,SEEK_SET);  // start byte of INDEX i's PREV field in DIARY
      int a = getInt(FB),      // get the PREV date as an integer
          b = getInt(FB);      // get the NEXT date as an integer
      if(b == 0)               // If the NEXT date is zero find the number
        t = getET(a,d);        // of days from the PREV date to today
      else                     // else
        t = 9999;              // set the DAYS value to maximum
      fseek(FB,p,SEEK_SET);    // start byte of INDEX i's data field in DIARY
      putInt(t,FB);            // store the days to go figure
      s = showInt(t,8,1);      // form 't' into a 8-character string
    } else {                   // else, for indexes 1 ans 2
      fseek(FB,p,SEEK_SET);    // start byte of INDEX i's data field in DIARY
      s = showInt(getInt(FB),8,1);  // set the DIARY field content as an 8
    }                               // character string with leading zeros

    q += 16;                // advance to [first/next] index record
    fseek(FB,q,SEEK_SET);   // start byte of next record of index file
    fputc(j,FB);            // store the DIARY record number
    fputc(8,FB);            // store the string length

    for(k = 0; k < 8; k++)  // put the record's field content into INDEX 'i'
      fputc(*(s+k),FB);
    while(k < 14) {         // pad out the array to 14 characters
      fputc(0,FB); k++;     // using NULL characters
    }
  }
  int w = i << 8;  // record number in 'diary.dat' for Record 0 of index 'i'
  FS = FB;         // index file handle for idxSort()

  idxSort(w+1,w+N8);         // SORT INDEX 'i'
  fseek(FB,3,SEEK_SET);      // seek Byte 2 of 'diary.dat'
  fputc(W8 & ~(1 << i),FB);  // clear the 'sort required' bit for this index
  fflush(FB);                // flush the sorted index to disk
  showMsg(14);               // show 'index regeneration completed' message
}




/* GENERATE OR RE-GENERATE THE POST CODE, SEARCH NAME, PERSONAL IDENT AND
   COMMUNICATIONS ID INDEXES. All indexes have 16-byte records: 1 byte for
   record number, 1 byte for text content length and 14 bytes for text con-
   tent. All indexes are stored one after the other after the PEOPLE data in 
   'nad.dat'. Called from 1 place in T0vButClick(). */

void T0regen() {
  for(int i = 0; i < 4; i++) {  // for each of the 4 index files
    int
     p = O0[i + 8],             /* offset of indexable field in NAD record

           skip the 262144 [0x40000] bytes of the PEOPLE data in 'nad.dat'
           |        current index number
           |        |    times 4096 [0x1000] bytes per index
           |        |    |      */
     q = 0x40000 + (i << 12),   // start byte of current index within nad.idx
     j, k;                      // inner loop variables

    N0 = 0;                     // number of occupied records
    H0 = 0;                     // highest occupied record

    for(j = 1; j < 256; j++) {  // for each of the 255 records from 1 to 255
      p += 1024;  // start byte of indexed field in [first/next] NAD record

      /* You only advance to the next index record when an OCCUPIED nad.dat
      record is encountered, otherwise you don't advance along the index. */

      if(T0isoc(j)) {           // if this [the j-th] record is occupied
        char S[16];
        q += 16;                // advance to [first/next] index record

        fseek(FA,p,SEEK_SET);   // start byte of nad record's indexable field
        int l = fgetc(FA);      // get length of this field [unsigned char]
        if(l > 14) l = 14;      // limit index field text to 14 characters
        for(k = 0; k < l; k++)  // get the first 'l' characters of the field
          S[k] = fgetc(FA);     // and put them into the character array s[]

        fseek(FA,q,SEEK_SET);   // start byte of next record of index file
        fputc(j,FA);            // put 'nad.dat' record number to index file
        fputc(l,FA);            // put length of field content to index record
        for(k = 0; k < l; k++)  // put the first 'l' characters of the field
          fputc(S[k],FA);       // into the corresponding index record

        while(k < 14) {         // Fill the remainder of the 14 character 
          fputc(0,FA); k++; }   // field with null characters.
        N0++;                   // increment the number of occupied records
        H0 = j;                 // highest occupied record = current record
      }
    }

    /* idxSort works with 16-byte records. There are 64 * 16-bytes records
       in one 'nas.dat' data record. There are 256 data records, which is
       therefore equivalent to 256*64=16384 [0x4000] 16-bit records.

        pseudo 16-byte record number of start of the index to be sorted
        |     number of equivalent 16-byte records in 'nad.dat' PEOPLE data
        |     |       index number 0 to 3 of the index being sorted
        |     |       |    times 256 16-byte records per index
        |     |       |    |    */
    int w = 0x4000 + (i << 8);  // nad.dat rec num for rec 0 of current index
    FS = FA;                    // index file handle for idxSort()
    idxSort(w+1,w+N0);          // sort this index file
  }
  fflush(FA);   // flush the sorted indexes to disk
  T0putN0H0();  // save 'N0' and 'H0 and flush to disk
  r0 = 1;       // display first record after an index regen
  showMsg(14);  // show 'index regeneration completed' message
}




/* CREATES AN ENTRY FIELD FOR A NEW ENTRY OR FOR EDITING AN EXISTING PREV
   OR DONE DATE. Called from T0diaryClick(). */

void T0diaryPreEdit() {

  /*  clear any old lingering message
      |             if we're doing an ED-PREV or ED-DONE 
      |             |         and a diary entry not selected [highlighted]
      |             |         |           show 'no entry selected' message
      |             |         |           |   */
  showMsg(0); if(v0 & 6 && u0 == 0) { showMsg(21); return; }
  T0diary();
  int x = 0;     // 'x' is the horizontal field offset
  if(v0 & 4)     // which is zero for either a NEW or ED-PREV entry
    x = 53;      // or 53 for an ED-DONE entry
  ex = 116 + x;  // line editor's horizontal lettering coordinate
  ey = 316;      // line editor's vertical lettering base coordinate

  XSetForeground(XD,XG,BLACK);  // clear the original text
  XFillRectangle(XD,XW,XG,17,ey-14,466,18);

  XSetForeground(XD,XG,BLEEN);  // colour for the data list lettering
  XDrawString(XD,XW,XG,173+x,ey,"Type-in the new date.",21);

  XSetForeground(XD,XG,GREEN);  // colour for the box
  XDrawRectangle(XD,XW,XG,ex-4,ey-14,54,19);

  en = 0;       // current length of the line editor's field content
  ec = 0;       // current cursor position [line editor]
  doCursor(1);  // display the cursor at start of field initially
  ea = 1;       // 1: editor active; 0: editor inactive
}




int T0diarySeek(int y) {
  int r = u0;        // set record number to the selected line number
  if(x0 > 0) {       // but if displayed list is in PREV, DONE or DAYS order

    /* The record number of the highlighted line within the DIARY section of
    'diary.dat' must be got from the first byte of the record within the cor- 
    responding index section [1,2 or 3] of 'diary.dat' whose number is that
    of the highlighted line within the displayed list.

          file handle for 'diary.dat'
          |   selected line in current INDEX: 1, 2 or 3
          |   |     column heading [index] number 0 to 3 [default is 0: REF]
          |   |     |     times 256 records per index
          |   |     |     |      all multiplied by 16 bytes per record
          |   |     |     |      |         */
    fseek(FB,(u0 + (x0 << 8)) << 4,SEEK_SET);
    r = fgetc(FB);  // DIARY record containing the marked line's data
  } 

  /* Now seek the start byte of either 1) the DIARY data record itself if
  deleting a diary entry [y = 0], or 2) that of the DONE field within the
  diary data record [y = 4]:

        file handle for 'diary.dat'
        |   DIARY record containing the marked line's data
        |   |    multiplied by 16 bytes per record
        |   |    |    offset to start of required field in DIARY record
        |   |    |    |       */
  fseek(FB,(r << 4) + y,SEEK_SET);
  return r;
}




/* POST-ENTRY PROCESSING OF A DIARY ENTRY AFTER CREATING A NEW ENTRY OR EDIT-
   ING AN EXISTING DATE. Executed in response to the user hitting the CR key
   after keying in the new date. Called only from one plane in T0CR(). */

void T0diaryPostEdit() {
  int y = 0, i, j;  // for start-byte of the data field, loop variables
  if(v0 & 1) {      // IF WE'RE DOING A NEW DIARY ENTRY
    int B = 16;     // set start byte of diary record '1'
    if(N8 > 254) {  // if diary is FULL [it already contains 255 entries]
      char s[16];   // holds content of a 16-byte diary record

      /* Collapse the whole diary down one entry, overwriting the
      first one in order to make room for the NEW one at the end.*/

      for(i = 1; i < 255; i++) {  // for each of the 255 diary records
        fseek(FB,B+16,SEEK_SET);  // seek start byte of the record 'i+1'
        for(j = 0; j < 16; j++)   // get the content of the record above
          s[j] = fgetc(FB);
        fseek(FB,B,SEEK_SET);    // seek start byte of record 'i'
        for(j = 0; j < 16; j++)  // overwrite record 'i' with the
          fputc(s[j],FB);        // content of record i+1' in the diary
        B += 16;                 // advance to the next record
      }                          // thus vacating record 255 for NEW entry

    } else {                 // else there's still spare space in diary
      fseek(FB,0,SEEK_SET);  // start byte of record zero
      fputc(++N8,FB);        // increment & save the total number of entries
      fputc(0,FB);           // trigger a resort of all diary indexes
      fflush(FB);            // push this data to the physical disk
    }
    B = N8 << 4;              // start byte of NEW record
    fseek(FB,B+12,SEEK_SET);  // start byte of its REF number field
    putInt(r0,FB);            // store the person's REFerence number
    e0++;                     // re-display from with new extra line
    fseek(FB,B,SEEK_SET);     // seek start byte of new record
  }
  else {               // ELSE WE'RE EDITING A 'PREV' OR 'DONE' DATE
    if(v0 & 4) y = 4;  // the DONE field is 4 bytes from start of record
    T0diarySeek(y);    // seek start byte of the record or its DONE field
  }
  le[8] = 0;            // terminate the Line Editor's string for atoi()
  putInt(atoi(le),FB);  // store the newly entered date
  v0 &= ~7;             // dim the TOP 3 buttons
  u0 = 0;               // clear any highlighted data line
  v0 &= ~255;           // kill vertical buttons
  e0 = 0;               // go to top line of diary
  T0diarySort(x0);      // sort on the selected column if necessary
  T0diary();            // re-display the diary listing
}




/* PUT TODAY'S DATE AS THE 'DONE' DATE ON THE CURRENTLY SELECTED LINE
   OR DELETE THE HIGHLIGHTED DIARY ENTRY. Called from T0diaryClick()
   when the DONE or KILL button is clicked. */

void T0diaryDoneKill() {
  showMsg(0);             // clear any remaining message
  if(u0 == 0) {           // if no diary line is selected [highlighted]
    showMsg(21); return;  // show the appropriate message and exit
  }
  int y = 0, q = 0;           // field offset for start of record
  if(v0 & 8) y = 4;           // field offset for DONE date
  int x = T0diarySeek(y);     // get record number and seek start byte
  if(v0 & 8)                  // if the DONE button was clicked
    putInt(ratSysDate(),FB);  // store today's date in the DONE field

  else {  /* else we're doing a KILL, so move all records above the deleted
          entry down one place thereby overwriting the deleted record. */

    if(x < N8) {                  // provided we are not on th last line
      int i, j, B = x << 4;       // start byte of the record to be deleted
      char s[16];
      for(i = x; i < N8; i++) {   // for each record above the deleted one
        fseek(FB,B+16,SEEK_SET);  // start byte of the record above
        for(j = 0; j < 16; j++)   // get the content of the record above
          s[j] = fgetc(FB);
        fseek(FB,B,SEEK_SET);     // start byte of the current record
        for(j = 0; j < 16; j++)   // overwrite the current record with the
          fputc(s[j],FB);         // content of the next record in the diary
        B += 16;                  // advance to the next record
      }
    }
    fseek(FB,0,SEEK_SET);  // seek the start byte of 'diary.dat'
    fputc(--N8,FB);        // decrement the total number of diary records
    fputc(0,FB);           // trigger a resort of all diary indexes
    fflush(FB);            // push this data to the physical disk
    u0 = 0;                // unmark the deleted diary entry
    e0 = 0;                // display from the top of the list
    q = 1;                 // message display flag
  }
  v0 &= ~255;         // dim the TOP 3 buttons
  u0 = 0;             // clear any highlighted data line
  T0diarySort(x0);    // sort on the selected column if necessary
  T0diary();          // display the diary tab
  if(q) showMsg(22);  // diary entry deleted message
}




/* HANDLES A 'PgUp' OR 'PgDp' KEY STROKE. Eleven lines of the DIARY or people
   LIST is displayed on the screen. A 'PgDn' keystroke causes the list to adv-
   ance by only 10 lines, leaving what was the bottom line of the list as its
   new top line. This gives better user confidence and sense of continuity. */

void T0PgUpDn(int s) {    // 's' has a value of +10 for PgDn and -10 for PgUp
  int x = -11;
  switch(a0) {            // switch on button logic
    case 0x10:            // if we're in the LIST sub-tab
      k0 += s;            // increment/decrement mouse bias by 10 lines
      if(s < 0) {         // if it was a 'Page-Up' key-stroke
        if(k0 < 0)        // if we've overshot the beginning of diary list
          k0 = 0;         // display from the beginning of the diary list 
      } else {            // if it was a 'Page-Down' key-stroke
        x += N0;          // start of the final 11 lines of diary list
        if(k0 > x)        // if we've overshot the end of the diary list
          k0 = x;         // set to display the final 11 lines
      } T0listScroll();   // re-display the diary list
      break;
    case 0x20:            // if we're in the DIARY sub-tab
      e0 += s;            // increment/decrement mouse bias by 10 lines
      if(s < 0) {         // if it was a 'Page-Up' key-stroke
        if(e0 < 0)        // if we've overshot the beginning of diary list
          e0 = 0;         // display from the beginning of the diary list 
      } else {            // if it was a 'Page-Down' key-stroke
        x += N8;          // start of the final 11 lines of diary list
        if(e0 > x)        // if we've overshot the end of the diary list
          e0 = x;         // set to display the final 11 lines
      } T0diaryScroll();  // re-display the diary list
  }
}




/* HANDLES MOUSE CLICKS ON THE DIARY SUB-TAB
   Called from only one place in T0click(). */

void T0diaryClick() {
  int i = -1;  // value denotes a click that was not on any of the buttons

  // HANDLES A CLICK ON A DIARY COLUMN HEADING

  if(my > 54 && my < 76) {                // headings buttons height limits
    if(mx >  86 && mx < 111)      i = 0;  // REF heading clicked
    else if(mx > 115 && mx < 164) i = 1;  // PREV date heading clicked
    else if(mx > 168 && mx < 217) i = 2;  // NEXT date heading clicked
    else if(mx > 221 && mx < 252) i = 3;  // DAYS heading clicked

    if(i >= 0) {    // if click was on a heading button
      v0 &= ~255;   // kill vertical buttons
      u0 = 0;       // clear any highlighted data line
      m0 = 1 << i;  // set the heading column highlighting
      x0 = i;       // update x0 only if a column heading has been clicked
      T0diarySort(i);  // sort on the selected column if necessary
    }
  } 
  // HANDLES A CLICK ON ONE OF THE DIARY'S VERTICAL BUTTONS

  else if(mx > 16 && mx < 70) {  // if in horiz limits of vertical buttons
    int y = 83, Y = 105;         // top & bottom limits of top button
    for(i = 0; i < 8; i++) {     // for each of the 8 vertical buttons
      if(my > y && my < Y)       // if mouse click was in this button's area
        break;                   // break out with i = button number
      y += 27; Y += 27;          // otherwise move down to the next button
    }
    if(i < 8) {      // if click was one of the buttons
      v0 &= ~255;    // dim all vertical buttons buttons
      v0 |= 1 << i;  // code for clicked button 1,2,4,8,16,32,64,128

      switch(i) {    // SWITCH ON BUTTON NUMBER
        case 0:                                        // NEW
        case 1:                                        // ED-PREV
        case 2: T0diaryPreEdit(); break;               // ED-DONE
        case 6:                                        // KILL
        case 3: T0diaryDoneKill(); break;              // DONE
        case 4: e0 = 0;       T0diaryScroll(); break;  // TOP
        case 5: e0 = N8 - 11; T0diaryScroll(); break;  // BOT
        case 7: helpPage("software/fep.html#diary");   // HELP
      }
      return;
    }
  }
  /* ELSE THE CLICK COULD HAVE BEEN ON ONE OF THE 11 LIST ITEMS. So if indeed
     the click was on one of the list items, compute the record number 'lo',
     within the index file, of the highlighted item. */

  else if((i = listClick()) > 0) { u0 = i + e0; v0 &= ~255; }
  T0diary();  // [re]display the DIARY sub-tab
}




/* HANDLES MOUSE CLICKS ON THE PEOPLE LIST SUB-TAB
   Called from only one place in T0click(). */

void T0listClick() {
  int i = -1;
  if(my > 54 && my < 76) {                // headings buttons height limits
    if(mx >  16 && mx <  35)      i = 0;  // CC heading clicked
    else if(mx >  39 && mx < 106) i = 1;  // POST CODE heading clicked
    else if(mx > 110 && mx < 201) i = 2;  // SEARCH NAME heading clicked
    else if(mx > 205 && mx < 228) i = 3;  // REF heading clicked

    if(i >= 0) {    // if click was on a heading button
      l0 = 0;       // clear any highlighted data line
      c0 &= ~0xFF;  // switch off all the heading column highlighting
      switch(i) {
        case 0: if(!(c0 &  1)) { c0 |=  1; } break;  // Communications Code
        case 1: if(!(c0 &  2)) { c0 |=  2; } break;  // POST CODE
        case 2: if(!(c0 &  4)) { c0 |=  4; } break;  // SEARCH NAME
        case 3: if(!(c0 &  8)) { c0 |=  8; }         // REFerence
      }
      d0 = i;  // only update d0 if a column heading has actually been clicked
    }
  } 
  /* ELSE THE CLICK COULD HAVE BEEN ON ONE OF THE 11 LIST ITEMS. So if indeed
     the click was on one of the list items, compute the record number 'lo',
     within the index file, of the highlighted item.

           column heading number
           |                     index record number of highlighted line
           |                     |             */
  else if((i = listClick()) > 0) l0 = i + k0;  /*
                                          |
  scroll bias [number of records before the 1st one currently displayed */

  T0list();  // [re]display the list sub-tab
}




/* MOUSE CLICKED ON A NON-BUTTON PART OF THE TAB. The click is therefore
   presumed to be an intentional click within a name & address data field.
   Called from 1 place each in  T0NEW(), T0edField() and the Mouse Event
   Handler MEH(). */

void T0click() {
  if(tb != 0) return;  // exit if this is not the PEOPLE tab
  showMsg(0);   // clear the message line
  if(mx > 436 && mx < 495 && my > 250 && my < 328) {
    if(D0 == 3) D0 = 0; else D0 = 3;
    T0suppBut();
    T0showSupp();
    return;
  }
  switch(D0) {  // scrolling dispatcher switch 0=NAD, 1=LIST, 2=DIARY, 3=SUPP
    case 1: T0listClick(); return;  // click inside LIST sub-tab
    case 2: T0diaryClick(); return; // click inside DIARY sub-tab
  }
  if(mx >  16 && mx < 190)   // if click was on a field name
    a0 |= 0x200;             // indicates a search
  else
  if(mx > 197 && mx < 480)   // if click was in a data field
    a0 &= ~0x200;            // indicates an edit
  else {                     // else the click was somewhere else
    T0clearClick();          // on the name & address tab, so clear the
    return;                  // currently active states and exit
  }
  int f;                     // number of the field that's just been clicked
  for(f = 0; f < 13; f++) {  // for each of the 13 lines on the screen
    int z = 57 + 20 * f;     // compute top [y-coordinate] of clickable area
    if(my > z && my < z+18)  // if click was within the vertical limits
      break;                 // break out of loop with f=clicked line number
  }
       /* if the click was at the level of field number 8
          |         but it is within the horizontal limits of the REF box
          |         |           |      set up for editing the REF number
          |         |           |      |                     */
  if(f == 8 && mx > 354 && mx < 397) { T0edRef(); return; }  /* and exit

         if a field name was clicked
         |     and  it was field 1,2,3,4,5,6 or 7, which can't be searched
         |     |    |        |  */
  if((a0 & 0x200 && f > 0 && f < 8) ||  /* OR 

        the click was on the level of one of the short 24-character fields
        |         and it was beyond the end of the field
        |         |         clear currently active states and exit
        |         |         | */
     (f > 7 && mx > 338)) { T0clearClick(); return; }

  ex = 198;  /* set x-coordinate of start of first letter of field

       specified screen field number [0 to 13]
       |   times 20 because there are 20 pixels per line [inter line spacing]
       |   |    base height of lettering for the top field
       |   |    |    */
  ey = f * 20 + 70;  /* base height of lettering for the specified field
  | 
  global variable set for use elsewhere */

  int q = b0;               // preserve the index selection setting
  b0 = 0;                   // clear all index settings
  T0showIdx();              // display all index field names in GREY
  T0Buts();                 // display NEXT/PREV button annotations in GREY
  if(f > 7 && f < 12) {     // if one of the indexable fields was clicked
    int i, j = f - 8;       // number of the index [0 to 3]
    for(i = 0; i < 4; i++)  // for each of the 4 index numbers [0 to 3]
      if(j == i) {          // if 'i' is the same as the clicked index
        int k = 1 << i;     // create a bit mask for this index's bit
        if(!(q & k))        // if the i-th index wasn't already selected
          b0 = k;           // set only the bit for the i-th index
        break;              // break out of the loop
      }
  }
  if(f > 12) return;  // exit if not a valid field
  if(a0 & 0x200) {    // if 'search mode' is active
    if(f == 12) {     // if click was on the SELECTOR KEY name
      a0 &= ~0x200;   // kill 'search mode'
      a0 |= 0x800;    // set 'selector mode' instead
    } else            // otherwise
      a0 &= ~0x800;   // kill 'selector mode' in case it was set last time
    T0srch(f);        // set up an empty green search box for clicked field
  } else T0edit(f);   // else set up yellow editing box for the clicked field
}




/* FIND A NEW [SPARE, UNOCCUPIED] NAME & ADDRESS RECORD.
   The first 256 bits in record zero of 'nad.dat' are used to indicate whether
   or not a given name & address record is occupied. For instance if the tenth
   bit is set, it means that record 10 of 'nad.txt' is occupied with a current
   valid name & address. If the tenth bit is not set, it means that record No.
   10 of 'nad.dat' is unoccupied [spare] and thus may be used for a new name
   and address to be entered. This function finds the number of the first
   available spare record in which to place a new name & address.
   Called only once from T0NEW(). */

void T0unocc() {
  fseek(FA,0,SEEK_SET);  // seek the start byte of 'nad.dat'
  int
    a,      // to hold the current new byte retrieved from disk
    i, j,   // outer and inner loop variables
    k,      // for the moving mask bit
    f = 0;  // 'spare found' flag

  for(i = 0; i < 32; i++) {   /* for each of the 32 bytes

        get byte number 'i' from record '1' of 'nad.dat'
        |                 if the byte is full of '1' bits
        |                 |    skip to the next byte
        |                 |    |  */
    if((a = fgetc(FA)) == 255) continue;

    k = 1;                    // initialise the test bit
    for(j = 0; j < 8; j++) {  // for each of the 8 bits of the current byte
      if(a & k) k <<= 1;      // if the bit is set, shift to next higher bit
      else { f = 1; break; }  // set 'spare found' flag and break j-loop
    } if(f) break;            // if spare found, break i-loop
  }
  if(f) {                  // if a spare record has been found
    a |= k;                // set the found unset bit
    fseek(FA,i,SEEK_SET);  // re-seek byte number 'i'
    fputc(a,FA);           // re-write the byte to disk
    r0 = (i << 3) + j;     // compute number of found 'nad.dat' record
    q0 = r0 << 10;         // start byte of this 'nad.dat' record
    if(H0 < r0)            // if new record beyond current highest occu-
      H0 = r0;             // pied record, make it the new highest record
  } else showMsg(5);       // else show 'no spare records available' message
}




// FIND THE NUMBER OF THE NEXT OCCUPIED RECORD. Called only from T0NEXT().

int T0next() {
  int
    B = r0 >> 3,  // byte containing the 'occupied' bit for record 'r0'
    b = r0 % 8,   // the 'occupied' bit within this byte for record 'r0'
    f = 0;        // 'found' flag return value [0=no record found]

  fseek(FA,B,SEEK_SET);  // seek the byte
  int c = fgetc(FA);     // and get its content

  for(int i = 0; i < 256; i++) {  // for a maximum of 256 records

    if(b > 6) {  /* if r0's 'occupied' bit is the highest in the current byte

           advance to the next byte
           |         if that now puts us beyond the last byte
           |         |       reset back to byte zero
           |         |       |   */
      B += 1; if(B > 31) B = 0;
      b = 0;                     // set to the first bit of the next byte
      fseek(FA,B,SEEK_SET);      // seek the next byte
      c = fgetc(FA);             // and get its content
      if(B == 0) continue;       // loop back to skip record zero

    } else b++;  // else simply advance to the next bit in the same byte

    if(c & 1 << b) {      // if the record is occupied
      r0 = (B << 3) + b;  // compute the number of this next occupied record
      f = 1;              // set the 'found' flag
      break;              // and break out of the for() loop
    }
  }
  return f;  // return the 'found' [1=found; 0=not found]
}




/* FIND THE NUMBER OF THE PREVIOUS OCCUPIED RECORD
   Called by T0PREV() and T0KILL(). */

int T0prev() {
  int
    B = r0 >> 3,  // byte containing the 'occupied' bit for record 'r0'
    b = r0 % 8,   // the 'occupied' bit within this byte for record 'r0'
    f = 0;        // 'found' flag return value [0=no record found]

  fseek(FA,B,SEEK_SET);  // seek the byte
  int c = fgetc(FA);     // and get its content

  for(int i = 0; i < 256; i++) {  // for a maximum of 256 records

    if(b < 1) {  // if r0's 'occupied' bit is the lowest in the current byte

        /* back up to the previous byte
           |         if that now puts us before the first byte
           |         |       reset back round to byte 31
           |         |       |   */
      B -= 1; if(B < 0) B = 31;
      b = 7;                     // set to the last bit of the previous byte
      fseek(FA,B,SEEK_SET);      // seek the byte
      c = fgetc(FA);             // and get its content

    } else b--;  // else simply back down to the previous bit in the same byte

    if(B == 0 && b == 0) continue;  // loop back to skip record zero

    if(c & 1 << b) {      // if the record is occupied
      r0 = (B << 3) + b;  // compute the number of this next occupied record
      f = 1;              // set the 'found' flag
      break;              // and break out of the for() loop
    }
  }
  return f;  // return the 'found' [1=found; 0=not found]
}




/* SET OR UNSET THE 'OCCUPIED' BIT, IN RECORD ZERO, PERTAINING TO THE CURRENT
   RECORD 'r0'. Each record has an 'occupied' bit. When set, the record is
   deemed to be currently occupied with a current name and address. When
   unset, the record is deemed to be free to be used for a new name & address.
   There is a maximum of 255 possible occupied records. Consequently, there
   are 255 bits, 1 for each record. These are stored in the first 32 bytes
   [bytes 0 to 31] of 'nad.dat'. Byte 32 is used to store the current number
   of occupied records. Byte 33 is used to store the highest occupied record.
   Called from only 1 place each in T0KILL(). */

void T0occu(int q) {  // q=1 set to occupied; q=0 unset to unoccupied

     /* file handle for 'nad.dat'
        |  number of record currently being [or having been] edited
        |  |     divided by 8 to get the number of the byte
        |  |     containing the occupied bit for record 'r0'
        |  |     |             */
  fseek(FA,r0 >> 3,SEEK_SET);  // byte containing the required required 8 bits
  int x = fgetc(FA);           // get the byte containing the 'occupied' bit

       /* put a bit in position zero of the byte
          |  shift it left by the number of places [bits] equal to the
          |  |    remainder when the record number 'r0' is divided by 8
          |  |    |     */
  int y = 1 << r0 % 8;  // make bit mask for the appropriate 'occupied' bit

  if(q)       // if marking the record as occupied
    x |= y;   // OR in the '1' bit corresponding to the current record number
  else        // else, if unmarking the record as unoccupied
    x &= ~y;  /* AND the existing bit pattern with the inverse of the bit mask

  To restore the byte to the same position we need to back up the
  file pointer by 1 byte because fgetc() has already advanced it.
            |                           */
  fseek(FA,-1L,SEEK_CUR); fputc(x,FA);  // restore the modified bit pattern

  if(q) {         // if we're marking the record as now occupied
    N0++;         // increment the number of occupied records
    if(r0 > H0)   // if the new record is higher than the existing highest 
      H0++;       // occupied record, increment the highest occupied record
  } else {        // else we're marking the record as now unoccupied
    N0--;         // decrement the number of occupied records
    if(r0 == H0)  // if we've just killed what was the highest occupied record
      H0--;       // decrement the highest occupied record number

    /* NOTE: it's possible that the record prior to the one just deleted may
    be unoccupied. However, it doesn't matter if unoccupied records exist
    below 'H0' because these will be skipped when a NEXT/PREV operation is
    invoked. It is best to regard 'H0' simply as the maximum extent to which
    the file has so far been occupied. It has no further significance. */
  }
  T0putN0H0();    // save 'N0' and 'H0 and flush to disk
}




// CLEAR THE ENTIRE NAME & ADDRESS RECORD 'r0'. Called only from T0NEW().

void T0clearNAD() {
  fseek(FA,q0,SEEK_SET);         // start byte of new record
  for(int i = 0; i < 1024; i++)  // set each of the 1024 bytes
    fputc(0,FA);                 // in the new record to NULL
}




/* BINARY SLICE SEARCH LOOP. This while() loop rapidly homes in to within one
   element of the one required. Called from one place only in T0findNAD(). */

int T0slice() {  /* 'I' is the number 0 to 3 of the index to be searched

  number of the index record at the middle of the index range: 1 to N0 incl.
  |    current number of occupied records in the index
  |    |     divide by 2 to get the number of the record half-way up the index
  |    |     |   */
  i0 = N0 >> 1;  // start with the search term mid-way up the index

  int j = i0,    // jump size starts at half index size
      f = 0;     // string comparison flag

  while((j >>= 1) > 0) {          // While halved jump size still > 0:
    if((f = strComp(I0+i0)) < 0)  // if element is lower than search term
      i0 += j;                    // jump half-way higher up the index
    else
    if(f > 0)    // if retrieved element is higher than search term
      i0 -= j;   // jump half-way lower down the index
    else         // otherwise they are equal so an exact match has been found
      return 0;  // return 'exact match found'
  }
  return 1;      // return 'nearest higher or lower match' found
}




/* SINGLE STEP INCHING FUNCTION: called when only a nearest match could be
   found by the binary slice search function. Return values:
   0 means that the inching function has found an exact match
   1 means that an exact match was not found by the stepping function
   2 means top entry in index is the nearest lower match
   3 means first entry in index is the nearest higher match
   Called from only one place in T0findNAD(). */

int T0step() {  // NOTE: the search term is in global array st[]
  int
    u = 0,  // 1 = going-up
    d = 0,  // 1 = going-down
    f = 0;  // string comparison flag

  while(u == 0 || d == 0) {  // while stepping hasn't reversed direction
    if((f = strComp(I0+i0)) < 0) {  /* if element lower than search term

              if moving up overshoots the top of the index
              |          last entry is nearest lower match
              |          |            return 'above top of index'
              |          |            | */
      if(++i0 > N0) { i0 = N0; return 2; }
      u = 1;  // if not, set that we have moved up a peg
    }
    else if(f > 0) {  /* if retrieved element higher than search term

              if moving down undershoots bottom of index
              |         first entry is nearest higher match
              |         |           return 'we've hit the bottom of the index'
              |         |           | */
      if(--i0 < 1) { i0 = 1; return 3; }
      d = 1;  // if not, set that we have moved down a peg
    }

    /* otherwise an exact must have been found, so
       |        return 'exact match'
       |        | */
    else return 0;
  }
  return 1;  // an exact match could not be not found
}            // the match is still the nearest higher or lower




/* INCH BACK DOWNWARDS TO THE FIRST EXACT MATCH. If the binary slice searcher
   [perhaps with the help of the inching function] found an exact match, it
   may not necessarily be for FIRST exact match in the index. The index may
   contain many identical entries and the previous functions may not have
   landed on the first one. It is therefore necessary to inch down the index
   until the 1st exact match is found. Called from 1 place in T0findNAD(). */

void T0firstExact() {
  while(--i0 > 0)           // while not yet tested first index entry
    if(strComp(I0+i0) < 0)  // if next lower element is lower than search term
      break;                // break out of the while() loop
  i0++;                     // back up to the lowest exact match
}




/* FIND THE FIRST HIGHER NEAREST MATCH. If only a higher or lower nearest
   match could finally be found, we have to find the match that is nearest
   and higher. So first go down the index until you hit an entry that is
   less than the search term, then back up to the the previous entry. Then
   go up the index until you reach an entry that's greater than the search
   term then back down to the previous entry. */

void T0FHNM() {
  while(--i0 > 0)           // while not yet reached the bottom of the index
    if(strComp(I0+i0) < 0)  // if next lower element is less than search term
      break;                // break out of the while() loop
  i0++;                     // then back up to where you were
  while(++i0 < N0)          // while not yet reached the top of the index
    if(strComp(I0+i0) > 0)  // if next higher element is greater than search
      break;                // term, break out of the while() loop
  i0--;                     // then back down to where you were
}




/* SEARCH FOR THE ENTERED SEARCH TERM IN THE APPROPRIATE INDEX IN THE FILE
   'nad.dat'. Sets index number 'i0' of the 1st exact match or 1st higher
   nearest match. Global array st[] contains the search term. Called from 1
   place each in T0newIdx(),T0colIdx(),T0search() and twice in T0idxShft(). */

int T0findNAD(int l) {  // search term [length 'l'] is already in 'st[]'
  for(int i = l; i < 14; i++)
    st[i] = 0;          // fill remainder of search-term with NULL chars
  int f = T0slice();    // binary slice search finds i0

  if(f == 0)         // If the SLICER found an exact match
    T0firstExact();  // make sure it's the first one, else
  else {             // slicer only managed to find NEAREST higher or lower
    f = T0step();    /* match, so continue search using the single STEPPER.

    f = 0 means that the inching function has found an exact match
    f = 1 means that an exact match was not found by the stepper
    f = 2 means top entry in index is the nearest lower match
    f = 3 means first entry in index is the nearest higher match */

    if(f == 0) {       // if the STEPPER managed to find an EXACT match
      T0firstExact();  // make sure it's the first one
    } else if(f == 1)  // else if still only nearest higher/lower match found
      T0FHNM();        // find the nearest HIGHER match
  }
  showMsg(f+1);          // display the result message for the search
  if(i0 < 1 || i0 > N0)  // safety net
    i0 = 0;              // set number of found record in global variable 'i0' 
  return f;              // return the match condition
}




/* SHIFT AN INTERVENING BLOCK OF INDEX RECORDS to overwrite the old index
   entry and vacate the appropriate position in the index to insert the new
   one. Called twice from T0idxShft() and once from T0newIdx(). */

void T0idxRecShft(int d, int i) {
  char S[16];      // stack array for moving the record content
  int k,           /* common loop variable

      START BYTE OF RECORD 'i' WITHIN THE WHOLE OF 'nad.dat'
      |     number of equivalent 16-byte records in 'nad.dat' PEOPLE data
      |     |     number of the 1st index record of search range
      |     |     |    record number within current index search range
      |     |     |    |    times 16 to get start byte of index record
      |     |     |    |    |   */
      B = 0x400 + I0 + i << 4;  /*

  SEEK START BYTE OF THE 'i+1'th OR 'i-1'th INDEX RECORD'S TEXT FIELD
  |     file handle for the index file currently being dealt with
  |     |  start byte of current record
  |     |  |    +1 for shifting it down [or -1 for up] the index 
  |     |  |    |    times 16 to get start byte of index record
  |     |  |    |    |             */
  fseek(FA,B + (d << 4),SEEK_SET);

  for(k = 0; k < 16; k++)  // copy whole content of record 'i +or- 1'
    S[k] = fgetc(FA);      // into S[] array
  fseek(FA,B,SEEK_SET);    // seek start byte of record 'i'
  for(k = 0; k < 16; k++)  // copy the whole content of S[]
    fputc(S[k],FA);        // into record 'i'
}




/* CREATE DUMMY INDEX ENTRIES FOR A NEW NAME & ADDRESS RECORD. For each of the
   4 indexes: 1) find next higher place in existing index where the new dummy
   index entry must be placed, 2) increment 'N0' [number of index records] to
   accommodate new dummy entry, 3) shift all existing records from [including] 
   next higher entry up one place, 4) insert new entry in vacated position, 5)
   put dummy entry also in the appropriate field of the name & address record
   in 'nad.dat'. Called only from one place in T0NEW(). */

void T0newIdx() {
  int i,j,k,K;           // loop variables
  char st[14],           // to assemble dummy entry
  *S = showInt(r0,3,1);  // current record number as 3-digit string

  st[0] = '#';     // indicates that what follows is a record number
  st[1] = *S;      // set 1st digit of record number in the search string
  st[2] = *(S+1);  // set 2nd digit of record number in the search string
  st[3] = *(S+2);  // set 3rd digit of record number in the search string
  for(i = 4; j < 14; i++)
    st[i] = 0;     // fill the remainder of st[] with null chars
  FS = FA;         // make general file handle = file handle for 'nad.dat'

  for(i = 0; i < 4; i++) {  // for each of the 4 indexes + NAD fields
    I0 = i << 4;   // index record number of record zero of Index 'i'

    T0findNAD(4);  // find 'i0' where the dummy index entry must be put

    N0++;  /* increment 'number of occupied records'. The highest occupied
           record H0 has already been incremented if necessary by T0unocc() */

    for(j = N0; j > i0; j--)  // for each record of Index 'i'
      T0idxRecShft(-1,j);     /* shift all index records up by one place

    SEEK START BYTE OF THE NEW INDEX RECORD
    |     file handle for 'nad.idx', which contains all 4 indexes
    |     |    number of equivalent 16-byte records in 'nad.dat' PEOPLE data
    |     |    |     'nad.idx' record number of start of index 'i'
    |     |    |     |    rec num of the new index record within Index 'i' 
    |     |    |     |    |     multiply by 16 bytes per record
    |     |    |     |    |     |             */
    fseek(FA,0x400 + I0 + i0 << 4,SEEK_SET);

    fputc(r0,FA);            // store the number of new 'nad.dat' record
    fputc(4,FA);             // store the length of the dummy text content
    for(k = 0; k < 14; k++)  // store the dummy text content
      fputc(st[k],FA);
    fflush(FA);              /* update the index file on disk

    SEEK START BYTE OF THE CURRENT FIELD WITHIN THE NEW 'nad.dat' DATA RECORD
    |     file handle for 'nad.dat' [main names & addresses file]
    |     |  start byte of the new NAD record [computed in T0unocc()]
    |     |  |    plus the offset of this field within the NAD record
    |     |  |    |  number of the index being processed [0 to 3]
    |     |  |    |  |   indexed field '0' is field '8' of the NAD record
    |     |  |    |  |   |  */
    fseek(FA,q0 + O0[i + 8],SEEK_SET);

    fputc(4,FA);            // store length of dummy text content
    K = L0[i+8];            // length of this indexed field in NAD record
    for(k = 0; k < K; k++)  // store the dummy text content
      fputc(st[k],FA);
  }
  T0putN0H0();  // save 'N0' and 'H0' and flush to disk
}




/* THE 'NEW' BUTTON WAS CLICKED. Create new blank record and put dummy entries
   in each of the 4 indexes ready for editing. 
   Called once in T0horizButClick(). */

void T0NEW() {
  if(a0 & 0x30) return;  // exit if the LIST or DIARY button is on
  if(a0 & 1) {           // if the NEW button be bright
    a0 &= ~1;            // dim it
  } else {
    T0unocc();     // find record number of first spare record in 'nad.dat'
    T0clearNAD();  // clear the new name & address record ready for in-fill
    T0newIdx();    // create dummy index entries for a new NAD record
    T0showNAD();   // display the new clear name and address record
    mx = 198;      // mouse x-coordinate of top field
    my = 58;       // mouse y-coordinate of top field
    ea = 0;        // set line editor to inactive
    T0click();     // effectively 'click' on the top field
    a0 &= ~0xFE;   // dim all other bottom buttons 11111110
    a0 |= 1;       // brighten the NEW button
    b0 = 0;        // set to Ref Number order [not index order]
  }
}




/* TO REMOVE A DELETED RECORD FROM THE INDEXES. COLLAPSES EACH OF THE FOUR
   INDEXES OVER THIS 'nad.dat' RECORD'S INDEX ENTRY in 'nad.idx'. The index
   file 'nad.idx' has 16 bytes per record. The record ranges for each of the 4
   indexes are:
                 i  INDEXED FIELD  RECORD RANGES   I0   i0
                 0  POSTAL CODE       0 to  255     0   1 to  255
                 1  SEARCH NAME     256 to  511   256   1 to  255
                 2  PERSONAL ID     512 to  767   512   1 to  255
                 3  COMMS IDENT     768 to 1024   768   1 to  255

   Note: the record's index entry is 'collapsed over' BEFORE the record itself
   is deleted. So, while the index entry is being removed, the record's number
   in nad.dat is still in global variable 'r0'. Called only from T0KILL(). */

void T0colIdx() {
  int i,j,J,f;              // loop variables
  FS = FA;                  // set 'nad.dat' file pointer for strComp()
  for(i = 0; i < 4; i++) {  /* FOR EACH OF THE 4 INDEXES:

    1) GET THE SEARCH TERM FOR FINDING THE INDEX RECORD TO BE COLLAPSED OVER
          file handle for 'nad.dat'
          |  current 'nad.dat' record's start byte
          |  |    offset of current indexed field in a 'nad.dat' record
          |  |    |                   */
    fseek(FA,q0 + O0[i+8],SEEK_SET);  // start byte of required field
    J = fgetc(FA);                    // field content length [unsigned char]
    if(J > 14) J = 14;                // cap to upper limit

    /* Get content [text] of the i-th indexable field of record r0 of nad.dat
    to use as a search term to locate the record number 'i0' in index 'i'. */

    for(j = 0; j < J; j++)  // for each character of the field content
      st[j] = fgetc(FA);    // put it in the corresponding element of st[]
    while(j < 14) {         // fill the remainder of the array st[]
      st[j] = 0; j++; }     /* with NULL characters

    2) FIND THE ENTRY WITHIN INDEX 'i' THAT CONTAINS THE SEARCH TERM
    number of the first record in the search range [record '0' of index 'i'] 
    |    index number 0 to 3 of the index being collapsed
    |    |    multiplied by 256 records per index
    |    |    |   */
    I0 = i << 8;  // index record number of record zero of index 'i'

    f = T0findNAD(J);  /* find record number 'i0' of index 'i' containing the
                          indexed field's content in record 'r0' of 'nad.dat'

    Note that T0findNAD() does a safety check on the range of 'i0', so: */
    if(i0 == 0) continue;  /* index error, so move on to do the next index.

    T0findNAD() finds the FIRST exact match. However, if more than one record
    in 'nad.dat' contains the same content in the first 14 characters of the
    field pertaining to Index 'i', we could be collapsing Index 'i' over the
    wrong entry. We need to check that the 1st byte of the found index record
    'i0' contains record number 'r0' of the 'nad.dat' record to be deleted.

    If it doesn't, we must step forward along Index 'i' to see if the next
    index entry to see if its first byte contains the same value as 'r0' and
    so on until we find the correct index entry. We don't need to check to
    see if subsequent index entries contain the required search term because
    if the index is good, the correct record will be found.

              16384 equivalent 16-byte records of PEOPLE data in 'nad.dat'
              |      number of the 1st index record of search range
              |      |     times 16 bytes per index record
              |      |     |   */
    int Q = 0x4000 + I0 << 4;  // start byte of the required index

    while(i0 <= 255) {  /* while we've not yet reached end of index block

      SEEK THE START BYTE OF INDEX RECORD 'i0'
      |     file handle for 'nad.dat'
      |     |  start byte of the required index  
      |     |  |    number of the latest found index record
      |     |  |    |     times 16 bytes per record
      |     |  |    |     |              */
      fseek(FA,Q + (i0 << 4),SEEK_SET);  /*

         record number in 'nad.dat' of the record to be deleted
         |     record number of 'nad.dat' contained in the index entry
         |     |           */
      if(r0 != fgetc(FA))  // if these are not the same, advance
        i0++;              // to the next record 'i0' of Index 'i'
    }                      /*

    NOW CHECK THAT THE VALUE OF 'i0' WE END UP WITH IS WITHIN VALID RANGE
            first occupied record in 'nad.dat'
            |         last occupied record in 'nad.dat'
            |         |      */
    if(i0 < 1 || i0 > N0) {  // if retrieved record number beyond valid range
      showMsg(9);            // show the appropriate error message
      printf("Error in Record %3i of Index %i. Do REGEN\n",i0,i);
      continue;              // move on to the next index
    }                        /*

    3) COLLAPSE THE INDEX OVER THE INDEX RECORD TO BE OBLITERATED */

    for(j = i0; j < N0; j++) {  // for each of this index's records above 'i0'
      int k, y = Q + (j << 4);  // start byte of record 'j' of index 'i'
      fseek(FA,y+16,SEEK_SET);  // seek start byte of index record j+1
      for(k = 0; k < 16; k++)   // for each of the 16 bytes of the record
        st[k] = fgetc(FA);      // put it into corresponding element of st[]
      fseek(FA,y,SEEK_SET);     // seek start byte of index record 'j'
      for(k = 0; k < 16; k++)   // for each of the 16 bytes of the record
        fputc(st[k],FA);        // write it to corresponding byte of record
    }
    fputc(0,FA);  // zero the record number [first byte] of record j+1
    fflush(FA);   // make sure this index is up to date on disk
  }
}                 // NOTE: T0occu(0) will decrement 'N0' upon return




/* THE 'KILL' BUTTON WAS CLICKED. This function marks the current record as
   unoccupied and removes its entries from the 4 indexes. Called from only
   one place in T0horizButClick(). */

void T0KILL() {
  if(a0 & 0x30) return;  // exit if the LIST or DIARY button is on
  if(a0 & 2)             // if the KILL button be bright
    a0 &= ~2;            // dim it
  else {
    T0colIdx();     // collapse the indexes to remove this records entries
    T0occu(0);      // set the current name & address record as unoccupied
    if(T0prev())    // if there exists a previous occupied record
      T0showNAD();  // display it
    showMsg(10);    // show message "RECORD DELETED"
    a0 |= 2;        // brighten the KILL button
    a0 &= ~0xFD;    // dim all the other bottom buttons 11111101
  }
}




/* GETS AND DISPLAYS THE NEXT OR PREVIOUS RECORD IN THE ASCII ORDER OF THE
   CURRENTLY SELECTED INDEX.
   Called only from 1 place each in T0NEXT() and T0PREV(). */

void T0nextprev(int d) {   // d = +1 for NEXT and d = -1 for PREV
  int i;                   // loop index needs to be outside loop
  for(i = 0; i < 4; i++)   // for each of the 4 index fields
    if(b0 & 1 << i) {      // if this index is the active one
      I0 = i << 8;         // rec num in 'nad.idx' of Record 0 of index 'i'
      break;               // and break out of the for() loop
    }
  if(i < 4) {              // provided index field number is 0,1,2 or 3
    i0 += d;               // increment/decrement the index record number
    if(d > 0) {            // if d is +1
      if(i0 > N0) i0 = 1;  // wrap back to bottom of index if necessary
    } else {               // otherwise 'd' must be -1, so
      if(i0 < 1) i0 = N0;  // wrap back to top of index if necessary
    }                      /* 

    SEEK THE FIRST BYTE OF THE NEXT RECORD IN INDEX 'i'
          file pointer [handle] for 'nad.idx'
          |    skip the 262144 [0x40000] bytes of PEOPLE data in 'nad.dat'
          |    |      record number of Record Zero of Index 'i'
          |    |      |    record number within Index 'i'
          |    |      |    |     multiply by 16 bytes per record
          |    |      |    |     |         */
    fseek(FA,0x4000 + I0 + i0 << 4,SEEK_SET);
    r0 = fgetc(FA);  // get the corresponding 'nad.dat' record number
    T0showNAD();     // display the NEXT/PREV name & address in the index
  }
}




/* THE 'NEXT' BUTTON HAS BEEN CLICKED. If the last 'nad,dat' record was
   retrieved via an index search, the name & address next in the order
   of that index is retrieved and displayed; otherwise the next name &
   address in record [Ref number] order is retrieved and displayed.
   Called from 1 place only in T0horizButClick(). */

void T0NEXT() {
  if(a0 & 0x30) return;  // exit if the LIST or DIARY button is on
  if(a0 & 0x100) {       // if trawling is active
    r0++;           // move on to the next record
    T0trawl();      // trawl for the next occurrence of the search term
  } else
  if(b0 == 0) {     // if none of the 4 index search fields is selected
    if(T0next())    // if there exists a next occupied record
      T0showNAD();  // display it [record number r0 has been set by T0next()]
  } else            // otherwise, if one of the 4 index fields is active
    T0nextprev(+1); // see function above
  a0 &= ~0xFB;      // dim all other buttons on the bottom row
}




/* THE 'PREV' BUTTON HAS BEEN CLICKED. If the last 'nad,dat' record was
   retrieved via an index search, the previous name & address in the order
   of that index is retrieved and displayed; otherwise the previous name &
   address in record [Ref number] order is retrieved and displayed.
   Called from 1 place in T0horizButClick(). */

void T0PREV() {
  if(a0 & 0x30) return;  // exit if the LIST or DIARY button is bright
  if(b0 == 0) {     // if none of the 4 index search fields is selected
  if(T0prev())      // if there exists a previous occupied record
    T0showNAD();    // display it
  } else            // otherwise, if one of the 4 index fields is active
    T0nextprev(-1);
  a0 &= ~0xF7;      // dim all other bottom buttons 0xF7 = 11110111
}




void T0LIST() {    // THE 'LIST' BUTTON WAS CLICKED
  if(a0 & 0x20) return;  // exit if DIARY button bright
  if(a0 & 0x10) {  // if the LIST button is bright
    a0 &= ~0x10;   // dim it
    if(a0 & 0x20)  // if the DIARY button is bright
      T0diary();   // display the DIARY
    else {         // otherwise
      if(r0 < 1)   // in case no LIST item was selected
        r0 = 1;    // default to record number 1
      T0Show();    // display the current name & address panel
    }
  } else {         // otherwise
    a0 |= 16;      // brighten the LIST button
    if(c0 == 0) { c0 = 8; d0 = 3; }
    T0list();      // display the PEOPLE list
  }
}




void T0DIAR() {    // THE 'DIARY' BUTTON WAS CLICKED
  if(a0 & 0x10) return;  // exit if LIST button bright
  if(a0 & 0x20) {  // if the DIARY button is bright
    a0 &= ~0x20;   // dim it
    if(a0 & 0x10)  // if the LIST button is bright
      T0list();    // display the PEOPLE list
    else {         // otherwise
      if(r0 < 1) r0 = 1;
      T0Show();    // display the current name & address panel
    }
  } else {         // otherwise
    a0 |= 0x20;    // brighten the DIARY button
    if(c0 == 0) { c0 = 8; d0 = 3; }
    T0diary();     // display the DIARY
  }
}



/* 'NOTES' BUTTON CLICKED. This causes a notes file to be opened correspond-
   ing to the current name and address. Called only from T0horizButClick(). */

void T0NOTE() {
  char
    S[] = "gedit notes/N000.txt &",  // template for the gedit command line
    *s = showInt(r0,3,1);  // current record number a 3-digit string
  S[13] = s[0];  // embed 1st digit of the record number in the template
  S[14] = s[1];  // embed 2nd digit of the record number in the template
  S[15] = s[2];  // embed 3rd digit of the record number in the template
  system(S);     // send the completed command to the terminal
}




/* ONE OF THE PEOPLE TAB'S CONTROL BUTTONS WAS CLICKED.
   Called from 1 place in MEH(). */

void T0horizButClick(int x) {
  showMsg(0);  // clear any remaining message
  switch(x) {  // switch according to button number
    case 0: T0NEW();  break;  // NEW   button was clicked
    case 1: T0KILL(); break;  // KILL  button was clicked
    case 2: T0NEXT(); break;  // NEXT  button was clicked
    case 3: T0PREV(); break;  // PREV  button was clicked
    case 4: T0LIST(); break;  // LIST  button was clicked
    case 5: T0DIAR(); break;  // DIARY button was pressed
    case 6: T0NOTE(); break;  // NOTES button was clicked
    case 7: helpPage("software/fep.html#people");
  } T0Buts();                 // re-display the buttons
}




/* Esc KEY [ESCAPE] PRESSED WHEN EDITING A NAME & ADDRESS FIELD
   Causes the edited version of the field to be discarded and the original
   content pulled again from disk and re-displayed. Called from only 1 place
   in T0putField() and the Keyboard Event Handler KEH(). */

void T0Esc() {
  if(a0 & 0x20) {      // if we are in the DIARY sub-tab
    ea = 0;            // switch off the line editor
    u0 = 0;            // unmark any marked line
    v0 &= ~0xFF;       // dim the vertical buttons
    T0diary();         // re-display the DIARY as was
  } else {
    b0 = 0;            // clear field name colour states
    a0 &= ~0x100;      // clear trawl mode
    T0drawBox(BLACK);  // box field 'f0' in the given colour
    T0clearInside();   // background the inside of the editing box
    doCursor(0);       // kill cursor
    XSetForeground(XD,XG,BLEEN);  /* colour for the display lettering
            0x200+  00000000000000000000001000000000  search mode bit
            0x400+  00000000000000000000010000000000  REF entry mode bit
            0x800=  00000000000000000000100000000000  selector mode bit */
    if(a0 & 0xE00) {   // if in search, select or ref modes
      a0 &= ~0xE00;    // kill search, select and ref modes
      T0Show();        // re-display the original name & address
    } else {           /* else we're escaping while in edit mode

      seek start byte of name & address field that's just been edited
      |     file handle for 'nad.dat'
      |     |  start byte of field that has just been edited
      |     |  |              */
      fseek(FA,p0,SEEK_SET);  // Get the old version of the edited field
      T0showField(f0);        // from disk and redisplay it.
    }
  }
  ea = 0;  // de-activate the line editor
}




/* PUT THE CONTENT OF THE LINE EDITOR ARRAY TO THE FIELD OF THE 'nad.dat'
   RECORD THAT HAS JUST BEEN EDITED. Called once from T0edField(). */

void T0putField() {
  if(en > L0[f0]) en = L0[f0];  /* cap content length of line editor field

  seek start byte of name & address field that's just been edited
  |     file handle for 'nad.dat'
  |     |  start byte of field that has just been edited
  |     |  |              */
  fseek(FA,p0,SEEK_SET);

     /* content length of line editor field
        |  file handle for 'nad.dat'
        |  |     */
  fputc(en,FA);  /* store the length of the field content in the first byte
                 
      number of characters in the   line editor character array
      content of the edited field   |  current character number being stored
                     |              |  |  file handle for 'nad.dat'
                     |              |  |  | */
  for(int i = 0; i < en; i++) fputc(le[i],FA);  // save content of edited line

  fflush(FA);   // make sure the updated field is on disk now
  T0Esc();      // erase box if necessary and re-display field
}




/* FIND, RETRIEVE AND DISPLAY THE NAME AND ADDRESS RECORD CORRESPONDING TO A
   SEARCH TERM ENTERED INTO ONE OF THE INDEXED SEARCH FIELDS.
   Called from only one place in T0CR(). */

void T0search() {
  int x, i, f;
  if(f0 == 9) SUC(en,le);  // allow upper case only for SEARCH NAME
  FS = FA;                 /* set index for strComp()

  number of the first record in the search range [record '0' of the index] 
  |    field number of the on-screen record
  |    |    the eighth field is the first indexable field
  |    |    |    multiplied by 256 records per index
  |    |    |    |   */
  I0 = f0 - 8 << 8;  /* start byte of the index to be searched

                 number of characters in the entered search term
                 |         */
  for(i = 0; i < en; i++)  // copy search term into st[] array
    st[i] = le[i];         // for T0findNAD()
  f = T0findNAD(en);       /* get index rec num 'i0' of entered search term

  For return value, f: see T0findNAD() but it is not used here at the moment.

     index record number of the entered search term
     |    if it is less than 1
     |    |  OR     beyond the highest occupied record
     |    |  |      |            */
  if(i0 < 1 || i0 > N0) return;  /* bail out if not within valid range

  SEEK START BYTE OF THE FOUND INDEX RECORD 'i0' WITHIN 'nad.dat'
  |     file pointer [handle] for 'nad.idx'
  |     |    skip the 262144 [0x40000] bytes of PEOPLE data in 'nad.dat'
  |     |    |      record number of Record Zero of Index 'i'
  |     |    |      |    record number within Index 'i'
  |     |    |      |    |     multiply by 16 bytes per record
  |     |    |      |    |     |             */
  fseek(FA,0x4000 + I0 + i0 << 4,SEEK_SET);  /*

      if 'nad.dat' record number >= 1
      |   returns the number of the 'nad.dat' record of the matching term
      |   |              AND     current highest occupied record
      |   |              |       |     set it as current record number
      |   |              |       |     |       display name & address
      |   |              |       |     |       |           */
  if((x = fgetc(FA)) > 0 && x <= H0) { r0 = x; T0showNAD(); }
}




/* SHIFT AN EDITED INDEX ENTRY FROM ITS ORIGINAL POSITION TO ITS NEW CORRECT
   POSITION WITHIN THE ALPHABETICAL [ASCII] ORDER OF THE INDEX.
   Called only from one place in T0edField(). */

void T0idxShft() {
  int i,           // loop variable
      L,           // valid length of field content from disk
      I = L0[f0];  /* field length of this field in the NAD record

   SEEK START BYTE OF CONTENT OF EDITED INDEXED FIELD IN 'nad.dat' RECORD
   |    file handle for 'nad.dat'
   |    |  start byte of the current name & address record
   |    |  |    off-set of this field within a name & address record
   |    |  |    |  number of indexed field [0 to 3]
   |    |  |    |  |              */
  fseek(FA,q0 + O0[f0],SEEK_SET); /*

  1) LOAD THE ORIGINAL CONTENT OF THE FIELD BEFORE IT WAS EDITED */
  L = fgetc(FA);          // length of valid field content
  if(L < 0) L = 0;        // if erroneously less than zero make it zero
  if(L > I) L = I;        // if erroneously greater than field length
  for(i = 0; i < L; i++)  // for each byte of the pre-edit version
    st[i] = fgetc(FA);    /* pull it from the 'nad.dat' record into st[]

  2) FIND THE FIELD'S POSITION 'a' WITHIN THE APPROPRIATE INDEX FILE:
       on-screen field number of the displayed 'nad.dat' record
       |    the eighth field is the first indexable field
       |    |    multiplied by 256 records per index
       |    |    |       */
  I0 = f0 - 8 << 8;      /* record zero of the index to be searched */
  FS = FA;               // set index for strComp()
  int f = T0findNAD(L);  // find its record number 'i0' within index file
  if(f != 0) { printf("Err: can't find exact match. Do a REGEN\n"); return; }
  // T0findNAD() also displays an error message on the tab's message line.
  int a = i0;            /* mark the index entry to be obliterated

  3) FIND WHERE THE EDITED VERSION SHOULD BE PUT 'b' WITHIN THE INDEX FILE */
  for(i = 0; i < en; i++)  // copy the edited content of the field
    st[i] = le[i];    // into the search-term array st[]
  f = T0findNAD(en);  // find its record number 'i0' within index file 'FS'
  int b = i0;         /* mark index position to insert the edited entry

  4) SHIFT THE INTERVENING BLOCK OF RECORDS UP OR DOWN OVER THE OLD ENTRY */
  if(a < b) {               // if old content lower in index than new content
    if(f != 3)              // if search term not beyond end of index,
      b--;                  // back down one place
    for(i = a; i < b; i++)  // Working forwards from bottom to top of block
      T0idxRecShft(+1,i);   // shift record 'i+1' down to i-th position.
  } else if(a > b)          // if old content higher up index than new content
    for(i = a; i > b; i--)  // Working backwards from top to bottom of block
      T0idxRecShft(-1,i);   /* shift Record 'i-1' up to i-th position.

  5) WRITE THE EDITED VERSION OF THE FIELD INTO THE VACATED INDEX POSITION

  SEEK START BYTE OF THE INDEX RECORD ENTRY
  |     file handle for 'nad.idx'
  |     |    skip over the 256 1024-byte PEOPLE data records
  |     |    |      start byte of selected index
  |     |    |      |    record number within index to insert edited entry
  |     |    |      |    |    times 16 bytes per record
  |     |    |      |    |    |  */
  fseek(FA,0x4000 + I0 + b << 4,SEEK_SET);
 
  fputc(r0,FA);  // 'nad.dat' record number to the new index entry position
  fputc(I,FA);   // length of field content to the new index entry position
  for(i = 0; i < I; i++)  // store the content of the Line Editor array
    fputc(le[i],FA);      // in the appropriate index record
  fflush(FA);    // make sure index file is up to date on disk
}




/* MAKE THE CONTENT OF THE SELECTOR KEY FIELD CONTAIN ONLY CHARACTERS
   FROM 0 TO 9 AND FROM A TO Z. Called only from T0edField() below. */

void T0key() {
  int i, I = L0[12];       // length of selector code field in NAD record
  for(i = 0; i < en; i++)  // for each character of the valid content
    le[i] = AZ09(le[i]);   // convert letter to upper case etc.
  while(i < I)             // fill the remainder of the field
    le[i++] = '0';         // with '0' characters
  en = I;                  // set length of field to the full capacity
}




/* CARRIAGE-RETURN [ENTER] KEY HAS BEEN PRESSED AFTER A FIELD OF A NEW RECORD
   HAS BEEN ENTERED OR THE FIELD OF AN EXISTING RECORD HAS BEEN EDITED
   Called only from one place in T0CR() below. */

void T0edField() {
  if(f0 == 9)            // if just edited the SEARCH NAME field
    SUC(en,le);          // ensure it is all in upper case letters only
  else if(f0 == 12)      // if just edited the SELECTOR KEY field
    T0key();             // ensure it contains only 0 to 9 and A to Z
  if(f0 > 7 && f0 < 12)  // if just edited an indexable field
    T0idxShft();         // shift its index entry to its new correct place

  T0putField();  /* store field in 'nad.dat'; erase box; redisplay content

          IF WE'RE [STILL] ENTERING A NEW NAME & ADDRESS
          |         and we haven't yet filled in the last field
          |         |  */
  if(a0 & 1 && f0 < 12) {

    mx = 198;  /* set mouse x-coordinate for data entry fields

         y-coord of the first [top] field
         |      plus the incremented current field number
         |      |    times 20 pixels inter-field drop
         |      |    |    */
    my = 58 + ++f0 * 20;  // mouse y-coordinate for next field

    if(f0 < 8 && f0 > 11)          // if not an indexable field
      for(int i = 0; i < 48; i++)  // clear the line editor array
        le[i] = 0;

    T0click();  // do a virtual 'click' on the field
  } else {      // otherwise, we've finished entering the new name & address
    ea = 0;     // so disable the line editor
    a0 &= ~1;   // and dim the NEW button
  }
}




/* CARRIAGE-RETURN [ENTER] PRESSED WHEN EDITING A NAME & ADDRESS FIELD
   Called from only 1 place in the Keyboard Event Handler KEH(). */

void T0CR() {
  if(a0 & 0x20) {   // if we're in the DIARY sub-tab
    if(v0 & 7)      // if the diary's NEW, ED-PREV or ED-DONE button is bright
      T0diaryPostEdit();
  } else if(a0 & 0x800)  // if SELECTOR KEY search mode is active
    T0selKey();     // list names & addresses that match entered key settings
  else
  if(a0 & 0x400) {  // if in Reference Number entry mode
    ea = 0;         // disable the line editor
    r0 = atoi(le);  // convert line editor content to integer 'r0'
    T0showNAD();    // display the name & address record 'r0'
  } else
  if(a0 & 0x200) {  // if we're in search mode
    ea = 0;         // disable the line editor
    if(f0 == 0) {   // if the search term was entered in Field Zero
      r0 = 1;       // set to start with first record
      T0trawl();    // trawl for it within all non-indexed fields
    } else          // else use the appropriate index to find the search term
      T0search();   // find, retrieve and display the name and address record
    a0 &= -0x200;   // we've finished the search, so revert to editing mode
  } else            // else just modified one of the name & address fields
    T0edField();    // so drop down to next field for editing
  T0Buts();         // re-display the button states
}




// SCROLLING OPERATION dispatcher FOR TAB 0

void T0scroll() {
  switch(D0) {
    case 1: T0listScroll(); break;
    case 2: T0diaryScroll();
  }
}



// EXPOSURE/RE-EXPOSURE dispatcher FOR TAB 0

void T0show() {
  if(a0 & 0x10) T0list();
  else
  if(a0 & 0x20) T0diary();
  else T0Show();
}





// --------------------FUNCTIONS PERTAINING TO TAB 7 MAINT -------------------


/* BACKUP OR RESTORE A SPECIFIED DATA FILE: 'nad.dat', 'diary.dat',
   'crop.dat', fin.dat'. */

void T7bkup(int n) {     // n = vertical button number
  char *S[] = {
    "backup/nad.dat",    // backup file for name & address data + indexes
    "backup/diary.dat",  // backup file for diary data + indexes
    "backup/crop.dat",   // backup file for crop data + indexes
    "backup/fin.dat"     // backup file for transaction data + indexes
  },
  *s[] = {"data/nad.dat","data/diary.dat","data/crop.dat","data/fin.dat"},
  *T;           // for "w" [write] or "r" [read] file mode

  if(a7 & 1)    // if we're in the BACKUP sub-tab
    T = "w";    // set for writing to the backup file
  else          // else, we're in the RESTORE sub-tab
    T = "r";    // so set for reading from the backup file

  int N[] = {
    274704,     // size [in bytes] of nad.dat
     20480,     // size [in bytes] of diary.dat
     33312,     // size [in bytes] of crop.dat
     36864      // size [in bytes] of fin.dat
  }, I = N[n];  // get the length [in bytes] of the selected file

  FILE *Q[4] = {FA,FB,FC,FD},  // file handles for the 4 data files
       *F = Q[n],              // data file corresponding to clicked button
       *G = fopen(S[n],T),     // open the backup file for read or write
       *H,                     // 'read-from' file handle
       *J;                     // 'write-to' file handle

  if(a7 & 1) {  // if we're in BACKUP sub-tab set for 
    fseek(F,0,SEEK_SET);  // seek start byte of data file to be backed up
    H = F;      // set to read from the data file
    J = G;      // set for writing to the backup file
  } else {      // else, we're in the RESTORE sub-tab
    fclose(F);  // close the data file in "r+" [read/modify] mode
    F = fopen(s[n],"w");  // and re-open it in "w" [create/write] mode
    H = G,      // set to read from the backup file
    J = F;      // set for writing to the data file
  }
  for(int i = 0; i < I; i++)  // for each byte of the file size
    fputc(fgetc(H),J);        // get from input file; write to output file
  fclose(G);                  // close the backup file
  if(a7 & 2) {                // if in RESTORE sub-tab
    fclose(F);                // close data file in "w" [create/write] mode
    F = fopen(s[n],"r+");     // and re-open it in "r+" [read/modify] mode
  }
}




void T7bakup(int i) {
  showMsg(0);
  if(i < 4) T7bkup(i);  // if button 0,1,2,3 was clicked do backup
  else if(i == 4)       // else if 'backup all' was clicked
    for(int j = 0; j < 4; j++)
      T7bkup(j);        // back up each of the 4 data files in turn       
  else if(i == 7) {     // the HELP key was clicked
    helpPage("software/fep.html#backup");
    return;
  } showMsg(26);
}




/* INDEX INTEGRITY CHECKER. Checks that all the elements of each of the 
   indexes are in ascending ASCII casting order. Note that the indexes
   [each comprising 256 16-byte records] follow on in the main data file
   after the end of the 256 records containing the data itself.
   Called from only one place in T0vBbutClick().

                   handle of index being checked e.g. FA,FB,FC,FD
                   |      number of output file name
                   |      |      off-set [in bytes] to start of indexes
                   |      |      |      number of indexes  
                   |      |      |      |      highest occupied record
                   |      |      |      |      |     */
void T7integ(FILE *F, int n, int o, int I, int J) {
  int i,       // indicates which of the 4 index files is being checked
      j,       // indicates which of its records is being checked
      k,       // indicates which of the 14 possible characters of the text
      K,       // number of valid characters in the current index text field
      f = 0,   // overall integrity failure flag
      R = 1,   // record number of main data record pointed to by index entry
      L = 14;  /* last time's value of K */

  char *N[] = {           // OUTPUT FILES FOR INDEX INTEGRITY TEST
    "test/nadidx.txt",    // n=0
    "test/diaryidx.txt",  // n=1
    "test/cropidx.txt",   // n=2
    "test/finidx.txt"     // n=3
  }, S[16];  // to hold the current index element for testing
  FILE *G = fopen(N[n],"w");  // Human-readable index file
  for(i = 0; i < I; i++) {    // for each of the relevant indexes

    /*  START BYTE OF SORT FIELD OF FIRST RECORD OF INDEX 'i' IN 'nad.dat'
        |   off-set [in bytes] to start of indexes
        |   |     plus hex 16 bytes to get to start byte of index Record 1
        |   |     |     plus the index number
        |   |     |     |    times index size 256 * 16 = 4096 [0x1000]
        |   |     |     |    |      */
    int B = o + 0x10 + (i << 12);
    for(k = 0; k < 14; k++)    // clear the S[] character array
      st[k] = 0;               // as first time's 'previous' element
    for(j = 1; j <= J; j++) {  // for each record in this index
      fseek(F,B,SEEK_SET);     // seek the start byte of the index record
      char *s = getIdxRec(F),  // get the index record
           *S = s + 2;         // pointer to start of search term text
      R = (unsigned)s[0];      // number of the main data record
      K = (unsigned)s[1];      // length of the search term content

      /*      file handle for human-readable output text file
              |                data record number
              |                | content length of search string
              |                | | search term string
              |                | | |  */
      fprintf(G,"%3i  %2i  %s",R,K,S);
      if(K > L) K = L;          // test as far as length of shortest field
      int g = 0;  // flag to indicate an error in an individual index record
      for(k = 0; k < K; k++) {  // for each character in the search text
        if(S[k] > st[k]) break; // new character higher than old character
        if(S[k] < st[k]) {      // at the first out-of-order pair encountered
          showMsg(11);          // display the index integrity error message
          printf("Index intergrity error: Ind %i, Ele %i Chr %i\n",i,j,k);
          f = 1; g = 1; break;  // set the error flags and break out
        }
      }  
      if(g == 1) fprintf(G,"  Index error: Ind %i, Ele %i Chr %i",i,j,k);
      for(k = 0; k < K; k++)  // make current element
        st[k] = S[k];         // next time's previous element
      L = K;                  // make last time's content length this time's 
      B += 16;                // advance to the next record
      fprintf(G,"\n");        // end of record line
    } fprintf(G,"\n");        // miss a line between indexes
  } if(f == 0) showMsg(12);   // index integrity OK message
  fclose(G);                  // close the index print file
}




/* RESET THE DIARY TO TEST DATA
   The file 'diary.dat' contains the following:
   256 * 16-byte records for the diary itself
   256 * 16-byte records for the PREV dates index
   256 * 16-byte records for the NEXT dates index
   256 * 16-byte records for the DAYS to go index
   256 * 16-byte records for the NAD REF index
   ---
  1280 * 16 = 20480 bytes total

   Format of a DIARY record:
   4-byte integer:PREV date of contact
   |   4-byte integer:NEXT date of proposed contact
   |   |   4-byte integer:DAYS to go to NEXT proposed contact
   |   |   |   4-byte integer: 'nad.dat' REF of person to whom
   |   |   |   |           the diary entry pertains
   1111000011110000 */

void T7diaryReset() {
  int N[31][4] = {  /*          PREV     NEXT  DAYS  NAD REF
           TEST DATA            |        |        |  |    */
    {20230115,20230503,30,04}, {20230116,20230506,14,11},
    {20230118,       0,27,07}, {20230119,20230519,16,13},
    {20230123,20230525,06,05}, {20230128,       0,03,07},
    {20230204,20230616,19,12}, {20230211,20230618,33,11},
    {20230225,       0,45,07}, {20230227,       0,23,06},
    {20230306,20230627,18,03}, {20230319,20230707,30,02},
    {20230320,20230717,22,07}, {20230322,20230728,05,02},
    {20230324,20230731,23,11}, {20230125,       0,13,07},
    {20230329,20230813,18,06}, {20230404,20230823,07,03},
    {20230405,       0,24,02}, {20230406,20230904,27,11},
    {20230408,20230913,13,03}, {20230409,       0,11,07},
    {20230410,20230926,04,12}, {20230412,20230930,12,04},
    {20230415,20231002,22,06}, {20230419,       0,32,10},  //vc
    {20230423,20231005,12,07}, {20230424,20231023,04,01},
    {20230425,20231024,29,04}, {20230427,       0,10,05},  //jj
    {20230430,20231101,20,10}   //vc
  }, i;

  // INSERT THE TEST DATA INTO IT
  fseek(FB,0,SEEK_SET);
  fputc(31,FB);              // current number of items in the diary
  fputc(0,FB);               // say that all indexes need sorting
  fseek(FB,16,SEEK_SET);     // seek start byte of Record 1
  for(i = 0; i < 31; i++) {  // for each of the first 31 records
    putInt(N[i][0],FB);      // PREV date
    putInt(N[i][1],FB);      // NEXT date
    putInt(N[i][2],FB);      // DAYS
    putInt(N[i][3],FB);      // NAD REF
  } fflush(FB); 
}




/* TRANS RESET
   Record Format: all 4-byte integers
   0000 transaction date
   0000 transaction type
   0000 ref number of client in 'nad.dat'
   0000 ref number of product in 'crop.dat'
   0000 price 
   0000 quantity
   0000 date paid */

void T7transReset() {
  int N[31][8] = {  // 31 records
    {20230228, 1,  2,  3, 218, 230, 20230316},  // each field is 4 bytes
    {20230119, 1,  4,  8, 200, 300, 20230530},
    {20230127, 1,  5, 12, 100, 500, 20230722},
    {20230202, 1,  4, 22, 250, 200, 20230511},
    {20230225, 2, 10, 16, 300, 100, 20230428},
    {20230304, 2,  3, 13,  50, 200, 20230530},
    {20230316, 1,  1,  5, 120, 300, 20230627},
    {20230319, 1, 11,  3, 170, 400, 20230508},
    {20230425, 0,  4,  9,  30, 300, 20230511},
    {20230428, 1,  9, 24, 100, 100, 20230508},
    {20230503, 1,  7, 27, 150, 150, 20230627},
    {20230508, 2,  2, 17, 180, 300, 20230512},
    {20230511, 1,  8,  7,  40, 500, 20230807},
    {20230512, 2,  7, 15, 440, 300, 20230630},
    {20230520, 1, 12,  4, 320, 300, 20230602},
    {20230530, 3,  9,  6, 220, 250, 20230814},
    {20230602, 3,  6, 13, 260, 240, 20230722},
    {20230627, 2,  5, 19, 280,  70, 20230806},
    {20230630, 1,  4, 20, 180, 160, 20230804},
    {20230715, 3,  4, 27, 330, 300, 20230915},
    {20230722, 1,  7, 15, 190, 200, 20230814},
    {20230727, 2,  4,  5,  80, 400, 20230827},
    {20230804, 1,  3,  7, 100, 100, 20230915},
    {20230806, 4,  3,  2, 200, 350, 20230821},
    {20230807, 1,  4,  1, 100, 380, 0},
    {20230814, 2, 13, 11, 200, 330, 0},
    {20230821, 2,  4, 16, 240, 170, 0},
    {20230827, 2,  8,  8, 260, 220, 0},
    {20230915, 1,  7,  4, 160, 180, 0},
    {20230925, 4,  3, 29,  70, 300, 0},
    {20231025, 1,  4,  1, 140, 170, 0},
  }, i, j;

  fseek(FD,0,SEEK_SET);
  fputc(31,FD);               // number of records on file in byte 0
  for(i = 1; i < 32; i++)     // zero the 31 bytes that remain in
    fputc(0,FD);              // record zero
  for(i = 1; i < 32; i++) {   // for each of records 1 to 31
    for(j = 0; j < 7; j++)    // put the appropriate line of
      putInt(N[i-1][j],FD);   // data from the above array
    putInt(0,FD);              // put zero in the last int of the record
  }
  for(i = 0; i < 7168; i++)   // for 256-32=224 records of 32 bytes
    fputc(0,FD);              // zero the 7168 bytes
  for(i = 0; i < 28672; i++)  // 7 indexes of 256 records each
    fputc(0,FD);              // at 16 bytes per record = 28672 bytes
  fflush(FD); 
}




/* A TEMPORARY DEVELOPMENT FUNCTION.
   Resets the file system back to test data conditions.
   Called by T0vButClick() and T0DELL(). */

void T7rset() {
  switch(e7) {               // SWITCH ON VERTICAL BUTTON COLUMN

    case 1:                  // reset the PEOPLE data to test status
      fseek(FA,0,SEEK_SET);  // seek the start byte of 'nad.dat'
      fputc(255,FA);         // mark records  0 to  7 as occupied
      fputc(255,FA);         // mark records  8 to 15 as occupied
      fputc(255,FA);         // mark records 16 to 23 as occupied
      for(int i = 0; i < 29; i++)
        fputc(0,FA);         // mark all other records as unoccupied
      fflush(FA);            // flush this situation to disk
      break;

    case 2: T7diaryReset(); break;  // reset the DIARY data to test status

    case 4:           // reset the CROPS data to test status
      fseek(FC,0,SEEK_SET);  // seek the start byte of 'crop.dat'
      fputc(255,FC);  // set record zero + records 00 to 07 as occupied
      fputc(255,FC);  // set record zero + records 07 to 15 as occupied
      fputc(255,FC);  // set record zero + records 15 to 23 as occupied
      fputc(255,FC);  // set record zero + records 15 to 23 as occupied
      fputc(224,FC);  // set record zero + records 15 to 23 as occupied
      for(int i = 5; i < 32; i++)
        fputc(0,FC);  // set records 6 to 255 as unoccupied
      fputc(34,FC);   // number of occupied records
      fputc(34,FC);   // highest occupied record
      fputc(31,FC);   // 're-indexing necessary' bits
      fflush(FC);
      break;

    case 8: T7transReset();  // reset the TRANS data to test status
  }
}




void T7reset(int i) {
  showMsg(0);
  if(i < 4) T7rset(i);  // if button 0,1,2,3 was clicked do backup
  else if(i == 4)       // else if 'backup all' was clicked
    for(int j = 0; j < 4; j++)
      T7rset(j);        // back up each of the 4 data files in turn       
  else if(i == 7) {     // the HELP key was clicked
    helpPage("software/fep.html#reset");
    return;
  } showMsg(13);
}




void T7rgen() {
  int i;
  switch(f7) {  // switch on REGEN sub-tab's vertical buttons state

    case 1: T0regen(); break; // REGENERATE ALL THE NAD INDEXES

    case 2:                   // REGENERATE ALL THE DIARY INDEXES
      fseek(FB,3,SEEK_SET);   // seek the diary's re-sort needed flags byte
      fputc(0,FB);            // reset all 8 bits to zero
      for(i = 0; i < 3; i++)  // for each of the 3 diary indexes
        T0diarySort(i);       // regenerate it and sort it
      break;

    case 4:                   // REGENERATE ALL THE CROP INDEXES
      fseek(FC,35,SEEK_SET);  // seek the crop's re-sort needed flags byte
      fputc(0,FC);            // reset all 8 bits to zero
      for(i = 0; i < 5; i++)  // for each of the 5 crop indexes
        T5cropSort(i);        // regenerate it and sort it
      showMsg(14);            // show 'index regeneration completed' message
      break;

    case 8:                   // REGENERATE ALL THE TRANSaction INDEXES
      fseek(FD,3,SEEK_SET);   // seek the trans re-sort needed flags byte
      fputc(0,FD);            // reset all 8 bits to zero
      for(i = 0; i < 7; i++)  // for each of the 7 transaction indexes
        T6transSort(i);       // regenerate it and sort it
      showMsg(14);            // show 'index regeneration completed' message
      break;
  }
}




void T7regen(int i) {
  showMsg(0);           // clear the message field
  if(i < 4) T7rgen(i);  // if button 0,1,2,3 was clicked do backup
  else if(i == 4)       // else if 'backup all' was clicked
    for(int j = 0; j < 4; j++)
      T7rgen(j);        // back up each of the 4 data files in turn       
  else if(i == 7) {     // the HELP key was clicked
    helpPage("software/fep.html#regen");
    return;
  } showMsg(14);
}




/* DRAW HORIZONTAL ROW OF CONTROL BUTTONS ACROSS BOTTOM OF THE MAINTENANCE
   TAB. Called from 1 place each in T7show() and T7horizButClick(). */

void T7showHorizButs() {
  showButs();  // show 8 blank buttons across the bottom of the tab area
  const char   // annotate the horizontal row of control buttons
    *A[] = {"BACKUP","RESTORE","INTEG","RESET","REGEN","SPARE","SPARE","HELP"};
  const int 
    a[] = {9,6,12,12,12,12,12,15},  // horizontal offsets for word starts
    b[] = {6,7, 5, 5, 5, 5, 5, 4};  // number of letters in each word
  int h = 17;                       // x coordinate of button block
  for(int i = 0; i < 8; i++) {      // for each of the 8 buttons:
    if(a7 & 1 << i)                 // if this button is 'on'
      XSetForeground(XD,XG,WHITE);  // set colour for bright lettering
    else
      XSetForeground(XD,XG,GREY);   // set colour for dull lettering
    XDrawString(XD,XW,XG,h + a[i],377,A[i],b[i]);
    h += 59;  // move across to next button
  }
}




/* DISPLAY THE VERTICAL COLUMN OF BUTTONS IN THE RAIN SUB-TAB. Called from
   T2showGraph(), T2showRainGraph(), T2showRainTank() */

void T7showVertiButs() {
  char  // annotate the vertical row of control buttons
    *A[] = {"PEOPLE","DIARY","CROPS","TRANS","ALL","SPARE","SPARE","HELP"};
  int 
    a[] = { 9,12,12,12,18,12,12,15},  // horizontal offsets for word starts
    b[] = { 6, 5, 5, 5, 3, 5, 5, 4},  // number of letters in each word
    h = 99,                           // y coordinate of button block
    g = 0;
  switch(a7) {
    case  1: g = b7; break; 
    case  2: g = c7; break; 
    case  4: g = d7; break;
    case  8: g = e7; break;
    case 16: g = f7;
  }
  for(int i = 0; i < 8; i++) {        // for each of the 8 buttons:
    XSetForeground(XD,XG,DARK);       // shade for buttons' background
    XFillRectangle(XD,XW,XG,17,h-15,53,21);
    if(g & 1 << i)                    // if this button is bright [on]
      XSetForeground(XD,XG,WHITE);    // set colour for bright lettering
    else                              // otherwise
      XSetForeground(XD,XG,GREY);     // set colour for dull lettering
    XDrawString(XD,XW,XG,17+a[i],h,A[i],b[i]);
    h += 27;                          // move across to the next button
  }
}




void T7showTab() {
  char
    *A[5] = {
      "DATA BACKUP FACILITY   ",
      "RESTORE FROM BACKUP    ",
      "INDEX INTEGRITY CHECKER",
      "RESET BACK TO TEST DATA",
      "REGENERATE THE INDEXES "
    },
    *B[5][5] = {
    { "Backup the PEOPLE data only.",
      "Backup the DIARY data only.",
      "Backup the CROPS data only.",
      "Backup the TRANSactions data only.",
      "Backup ALL data." },
    { "Restore the PEOPLE data only.",
      "Restore the DIARY data only.",
      "Restore the CROPS data only.",
      "Restore the TRANSactions data only.",
      "Restore ALL data." },
    { "Check integrity of the 4 name & address indexes.    nadidx.txt",
      "Check integrity of the 3 diary indexes.             diaryidx.txt",
      "Check integrity of the 5 crops indexes.             cropidx.txt",
      "Check integrity of the 7 transactions indexes.      finidx.txt",
      "Index integrity can only be checked one index at a time." },
    { "Reset the PEOPLE file back to test data.",
      "Reset the DIARY back to test data.",
      "Reset the CROPS file back to test data.",
      "Reset the TRANSactions file back to test data.",
      "Reset all files back to test data." },
    { "Regenerate the PEOPLE indexes.",
      "Regenerate the DIARY indexes.",
      "Regenerate the CROPS indexes.",
      "Regenerate the TRANSactions indexes.",
      "Regenerate all indexes." }
    },
    *C[5] = {
      "The backup data is put in the directory [folder] \"/backup\".",
      "The backup data is got from the directory [folder] \"/backup\".",
      "The results are in the files named on the right in \"/test\".",
      "BACKUP all data before you click any of these buttons.",
      ""
    };
  int 
    a[] = {20,19,23,23,22},  // line lengths for the TAB titles
    b[5][5] = {
      {28,27,27,34,16},      // line lengths for BACKUP tab annotations
      {29,28,28,35,17},      // line lengths for RESTORE tab annotations
      {62,64,63,62,56},      // line lengths for INTEG tab annotations
      {40,34,39,46,34},      // line lengths for REGEN tab annotations
      {30,29,29,36,23}       // line lengths for RESET tab annotations
    },
    c[5] = {59,61,59,54,0},  // line lengths of bottom messages
    v = 98,                  // y-coordinate of first annotation line
    i, j;                    // loop variables
  for(i = 0; i < 5; i++)     // form the button number from the button code
    if(a7 == 1 << i) break;  // break with found button number
  clearTab();
  XSetForeground(XD,XG,YELLOW);  // colour for the lettering
  XDrawString(XD,XW,XG,17,70,A[i],23);
  XSetForeground(XD,XG,BLEEN);   // colour for the lettering
  for(j = 0; j < 5; j++) {
    XDrawString(XD,XW,XG,87,v,B[i][j],b[i][j]);
    v += 27;
  }
  XDrawString(XD,XW,XG,17,315,C[i],c[i]);
  showMsg(0);
  T7showVertiButs();
}




void T7spare() {
  char *S[] = {
    "This is a SPARE TAB used for performance monitoring of ",
    "the software during initial development and during cus-",
    "tomisation for a particular application site. It's not ",
    "implemented in this demonstration version.             "
  };
  clearTab();
  int y = 120;
  XSetForeground(XD,XG,BLEEN);
  for(int i = 0; i < 4; i++)
    XDrawString(XD,XW,XG,90,y+=19,S[i],55);
  showMsg(0);
}




/* MOUSE CLICK HANDLER FOR TAB 6. Used to determine which button in the vert-
   ical column was clicked. Called only from T6click() below. */

void T7vClick() {
  if(mx < 17 || mx > 69)
    return;                  // exit if outside horizontal buttons limits
  int i, y = 84;             // height [y-coordinate] of top of top button
  for(i = 0; i < 8; i++) {   // for each of the 8 vertical buttons
    if(my > y && my < y+21)  // if mouse within vertical limits of this
      break;                 // button, break out of loop: i = button number
    y += 27;                 // drop down to the next button
  } if(i > 7) return;        // click was above top or below bottom button
  switch(a7) {               // SWITCH ON SUB-TAB BUTTON CODE
    case 1:                  // THE BACKUP SUB-TAB
      b7 &= ~0xFF;           // clear the backup control buttons
      b7 |= 1 << i;          // illuminate the clicked vertical button
      T7bakup(i);            // backup the selected file(s)
      break;
    case 2:                  // THE RESTORE SUB-TAB
      c7 &= ~0xFF;           // clear the restore control buttons
      c7 |= 1 << i;          // illuminate the clicked vertical button
      T7bakup(i);            // backup the selected file(s)
      break;
    case 4:                  // THE INTEG SUB-TAB
      d7 &= ~0xFF;           // clear the index integrity check buttons
      d7 |= 1 << i;          // illuminate the clicked vertical button
      switch(i) {            // SWITCH ON BUTTON NUMBER 0 TO 7
        case 0: T7integ(FA,0,0x40000,4,N0); break;  // PEOPLE
        case 1: T7integ(FB,1, 0x1000,3,N8); break;  // DIARY
        case 2: T7integ(FC,2, 0x4000,5,N5); break;  // CROPS
        case 3: T7integ(FD,3, 0x2000,7,N6); break;  // TRANSactions
        case 7:     // the HELP key was clicked
          helpPage("software/fep.html#integ");
      } break;
    case 8:                  // THE REGEN SUB-TAB
      e7 &= ~0xFF;           // clear the REGENerator control buttons
      e7 |= 1 << i;          // illuminate the clicked vertical button
      if(i < 5) T7reset(i);  // if one of buttons 0,1,2,3 clicked
      else if(i ==7)         // the HELP key was clicked
        helpPage("software/fep.html#regen");
      break;
    case 16:                 // THE RESET SUB-TAB
      f7 &= ~0xFF;           // clear the reset control buttons
      f7 |= 1 << i;          // illuminate the clicked vertical button
      if(i < 5) T7regen(i);  // if one of buttons 0,1,2,3 clicked
      else if(i ==7)         // the HELP key was clicked
        helpPage("software/fep.html#reset");
  } T7showVertiButs();
}




/* HANDLES CLICKS TO THE 8 BOTTOM BUTTONS OF THE TRADE TAB [TAB 6]
   Called from only one place in MEH(). */

void T7horizButClick(int i) {  // called from only 1 place in MEH()
  clearTab(); 
  a7 &= ~0xFF;   // kill all buttons
  a7 |= 1 << i;  // illuminate the clicked horizontal button
  switch(i) {
    case 0: T7showTab(); break;  // BACUP
    case 1: T7showTab(); break;  // RESTORE
    case 2: T7showTab(); break;  // INTEG
    case 3: T7showTab(); break;  // RESET
    case 4: T7showTab(); break;  // REGEN
    case 5: T7spare();   break;  // SPARE
    case 6: T7spare();   break;  // SPARE
    case 7:                        // HELP
      helpPage("software/fep.html#maint");
  } T7showHorizButs();  // re-display the buttons
}




/* THE MOUSE WAS CLICKED WITHIN THE MAINT TAB.
   Called from 2 places in MEH(). */

void T7click() { T7vClick(); }




/* SETS UP THE TEXT AND BUTTONS FOR THE ADMIN TAB. This is a dummy function
informing the user that this Tab's functionality is not implemented in this
demo version of the program. Called from 1 place in showTab(). */

void T7show() {
  switch(a7) {
    case  1: T7showTab(); break;
    case  2: T7showTab(); break;
    case  4: T7showTab(); break;
    case  8: T7showTab(); break;
    case 16: T7showTab(); break;
    case 32: T7spare();   break;
    case 64: T7spare();
  }
  T7showHorizButs();  // display the tab's buttons
}





// TAB DISPLAY DISPATCHER ----------------------------------------------------


/* DISPLAY ALL THE TITLES, BUTTONS AND DEFAULT IN-FILL FOR THE CURRENT TAB.
   The current tab is specified by the numeric value of the global variable
   'tb'. Called from only 1 place each in main() and MEH(). */

void showTab() {
  const char
  *BA[8] ={"PEOPLE","LAND","WATER","POWER","EQUPT","PROJS","TRADE","MAINT"};
  const int 
  NC[8] = {6, 4, 5, 5, 5, 5, 5, 5},  // number of chars in each annotation
  NB[8] = {9,15,12,12,12,12,12,12};  // x-bias for each annotation
  XSetForeground(XD,XG,DARK);  // shade for button background
  int h = 17, i, C;            // set y-coordinate to left margin
  for(i = 0; i < 8; i++) {     // draw each button
    XFillRectangle(XD,XW,XG,h,17,53,21);
    h += 59;  // move across to the next button
  }
  h = 17;                     // reset to the left margin
  for(i = 0; i < 8; i++) {    // annotate each button
    if(tb == i)               // If this is the currently selected tab
      C = WHITE;              // annotate it in bright white lettering

    /* else if we're in the PEOPLE tab and the PEOPLE 'alert flag' is set
       or if we're in the PROJS tab and the PROJS 'alert flag' is set:
       display the button text in RED; otherwise display it in GREY. */

    else if(i == 0 & V8 || i == 5 & V5) C = RED; else C = GREY;
    XSetForeground(XD,XG,C);  // annotate the button
    XDrawString(XD,XW,XG,h + *(NB+i),32,*(BA+i),*(NC+i));
    h += 59;  // move across to the next button
  }
  clearTab();
  XSetForeground(XD,XG,BLACK); // clear Tab area
  XFillRectangle(XD,XW,XG,17,38,484,383);
  switch(tb) {                 // switch according to tab number
    case 0: T0show(); break;   // paint the PEOPLE tab
    case 1: T1show(); break;   // paint the LAND tab
    case 2: T2show(); break;   // paint the WATER tab
    case 3: T3show(); break;   // paint the POWER tab
    case 4: T4show(); break;   // paint the EQUPT tab
    case 5: T5show(); break;   // paint the PRODS tab
    case 6: T6show(); break;   // paint the TRADE tab
    case 7: T7show();          // paint the MAINT tab
  }
}





// MOUSE EVENT HANDLERS-------------------------------------------------------


/* MOUSE EVENT HANDLER. Handles clicks of the left mouse button [Button 1].
   The x and y coordinates of the click-point within the application window
   are already in global variables mx and my. Called only from one place in
   main()'s event loop. */

void MEH() {
  int i,      // loop variable
      x = 1,  // indicates a click in the unoccupied area of the tab
      y = 0;  // left edge of each button in turn

  if(my > 16 && my < 38)  // if click was on one of the Tab buttons
    x = 0;                // indicates that a Tab button was clicked
  else 
  if(my > 361 && my < 383)  // if click was on one of the Bottom buttons
    x = 2;                  // indicates a control button was clicked

  for(i = 0; i < 8; i++) {          // for each button
    if(mx > 16 + y && mx < 70 + y)  // if click was within a button
      break;                        // break out of loop
    y += 59;                        // move across to the next button
  }
  switch(x) {   // switch according to type of item clicked
  case 0:       // A TAB BUTTON WAS CLICKED
    tb = i;     // set Tab Number
    showTab();  // display the Tab's content
    break;
  case 1:       // THE CLICK OCCURRED IN SOME OTHER PART OF THE TAB AREA
    switch(tb) {
      case 0: T0click(); break;  // clicked in another part of PEOPLE Tab
      case 1: T1click(); break;  // clicked in another part of LAND Tab
      case 2: T2click(); break;  // clicked in another part of WATER Tab
      case 3: T3click(); break;  // clicked in another part of POWER Tab
      case 4: T4click(); break;  // clicked in another part of EQUPT Tab
      case 5: T5click(); break;  // clicked in another part of PRODS Tab
      case 6: T6click(); break;  // clicked in another part of TRADE Tab
      case 7: T7click();         // clicked in another part of MAINT Tab
    } break;
  case 2:         // A CONTROL BUTTON WAS CLICKED
    switch(tb) {  // 'i' is button number left to right: 0 to 7
      case 0: T0horizButClick(i); break;  // PEOPLE Tab's button click handler
      case 1: T1horizButClick(i); break;  // LAND Tab's button click handler
      case 2: T2horizButClick(i); break;  // WATER Tab's button click handler
      case 3: T3horizButClick(i); break;  // POWER Tab's button click handler
      case 4: T4horizButClick(i); break;  // EQUPT Tab's button click handler
      case 5: T5horizButClick(i); break;  // PRODS Tab's button click handler
      case 6: T6horizButClick(i); break;  // TRADE Tab's button click handler
      case 7: T7horizButClick(i);         // MAINT Tab's button click handler
    }
  }
}


void MWup() {  // MOUSE WHEEL UP. Called only from 1 place in main()
  switch(tb) {
    case 0: mw = 2; T0scroll(); break;
    case 1: mw = 2; break;
    case 2: mw = 2; break;
    case 4: mw = 2; break;
    case 5: mw = 2; T5cropScroll(); break;
    case 6: mw = 2; T6mouseWheel();
  }
}


void MWdn() {  // MOUSE WHEEL DOWN. Called only from 1 place in main()
  switch(tb) {
    case 0: mw = 1; T0scroll(); break;
    case 1: mw = 0; break;
    case 2: mw = 0; break;
    case 4: mw = 0; break;
    case 5: mw = 1; T5cropScroll(); break;
    case 6: mw = 1; T6mouseWheel();
  }
}




// ------------------------ KEYBOARD EVENT HANDLERS --------------------------


/* [RE]DISPLAY TEXTUAL CONTENT OF LINE EDITOR FIELD
   Called from 1 place each in bksp(), Del(), lineEditor(). */

void showLE() {
  int d = 50;
  XSetForeground(XD,XG,DARK);  // clear box
  switch(tb) {
    case 0:
      if(a0 & 0x20)  // if in the DIARY sub-tab
      XFillRectangle(XD,XW,XG,ex-3,ey-12,52,17);
      else
      XFillRectangle(XD,XW,XG,ex-3,ey-12,T0BIW(f0),17);
      break;
    case 5:
      d = 50;                   // most fields are 50 pixels wide
      if(f5 == 0 && !(b5 & 1))  // but the crop name field
        d = 190;                // is 190 pixels wide
      XFillRectangle(XD,XW,XG,ex-2,ey-12,d,14);
      break;
    case 6:
      XFillRectangle(XD,XW,XG,ex-2,ey-12,52,14);  // paint background
  }
  XSetForeground(XD,XG,WHITE);  // display text in WHITE
  XDrawString(XD,XW,XG,ex,ey,le,en);
  doCursor(1);
}


void CR() {  // CARRIAGE-RETURN EVENT DISPATCHER
  switch(tb) {
    case 0: T0CR(); break;
    case 5: T5cropCR(); break;
    case 6: T6transCR();
  }
}


void Esc() {  // ESCAPE KEY EVENT DISPATCHER
  switch(tb) {
    case 0: T0Esc(); break;
    case 5: T5cropEsc(); break;
    case 6: T6transEsc(); break;
  }
}


/* WHEN THE LEFT-ARROW OR THE RIGHT-ARROW KEY IS PRESSED
   Called only from one place in KEH(). */

void leftArrow() {
  doCursor(0);  /* erase the cursor from its old position

    /* decremented cursor position [move it left]
       |            position beyond end of content
       |            |    */
  if(--ec < 0) ec = en;  /* if cursor was already at beginning of the field,
                         loop back round to the end of the line content. */

  doCursor(1);  // show the cursor in its new position
}


void rightArrow() { 
  doCursor(0);  /* erase the cursor from its old position

      /* incremented cursor position [move it right]
         |    position beyond end of content
         |    |        put cursor at beginning of line
         |    |        |   */
    if(++ec > en) ec = 0;  /* if cursor was already at the end of the line 
                           content, loop forwards round to the beginning. */

  doCursor(1);  // show the cursor in its new position
}


// WHEN THE 'Backspace' KEY IS PRESSED: Called only fom 1 place in KEH().

void bkSp() {
  doCursor(0);  /* erase the cursor

     if the cursor is not at the beginning of the line editor field
     |      and there is content at or beyond it
     |      |  current position of cursor within line editor field
     |      |  |     current number of characters in the field's content
     |      |  |     |      */
  if(ec > 0 && ec <= en) {

    ec--;    /* Move the cursor one place left then pull all characters,
                at or beyond where the cursor was, back one place left.

                current position of cursor
                |       number of characters of content in the line
                |       |           current character
                |       |           |         next character
                |       |           |         | */ 
    for(int i = ec; i < en; i++) le[i] = le[i + 1];

    en--;              // decrement the field's content length
    le[en] = (char)0;  // terminate shortened field content string
    showLE();          // display the field's textual content
  }
}


// WHEN THE 'Delete' KEY IS PRESSED: Called only from 1 place in KEH().

void Del() {
  if(ec >= en) return;  /* exit if cursor at or beyond current end of content

  PULL ALL CHARACTERS, AT OR BEYOND THE CURSOR, BACK ONE PLACE LEFT.

              current position of cursor
              |       number of characters of content in the line
              |       |           current character
              |       |           |         next character
              |       |           |         | */ 
  for(int i = ec; i < en; i++) le[i] = le[i + 1];

  le[en] = 0;  // terminate shortened field content string
  en--;        // decrement the field's content length
  showLE();    // display textual content of le[] field
}


// WHEN 'Home' or 'End' KEY IS PRESSED: Called only from 2 places in KEH().

void homeEnd(int x) {
  doCursor(0);  // erase the cursor at its old position
  ec = x;       // set new position of cursor
  doCursor(1);  // display it in its new position
}


void PgUpDn(int x) {  // PgUp or PgDn KEY PRESSED
  switch(tb) {
    case 0: T0PgUpDn(x); break;
    case 5: T5PgUpDn(x); break;
    case 6: T6PgUpDn(x);
  }
}




/* LINE EDITOR: All printable characters are received by this Line Editor
   which places each, as it is received, in its appropriate place within
   the Line Editor's character array le[]. */

void lineEditor() {
  int L;
  switch(tb) {  // max number of characters permitted
    case 0: L = L0[f0]; break;  // in NAD field being edited
    case 5: L = L5[f5]; break;  // in crops field being edited
    case 6: L = 8;              // in transaction field being edited
  }
  /* current number of characters in the line editor array
     |    maximum capacity of line editor array
     |    |     */
  if(en < L) {  // if there's still room in the field for more characters

             /* character beyond the ultimate character in the array
                |        current cursor position
                |        |   stepping backwards towards the cursor
                |        |   |     */
    for(int i = en; i >= ec; i--)  // shift all chars from cursor one place
      le[i + 1] = le[i];           // right to make room for new character
    en++;                          // then increment length of field content
  }

  if(ec < L) {    // if the cursor has not yet reached the end of the field
    le[ec] = ka;  // put the new character at cursor position

    /* current position of cursor
       |     total number of characters in the line editor field
       |     |               */
    if(ec >= en) {           // if currently at end of textual content
      le[ec + 1] = (char)0;  // make character beyond into a NULL char
      en++;                  // increment number of characters of content
    }
    /* current cursor position
       |    max number of character spaces in the line editor field
       |    |  advance the cursor one position right
       |    |  |      */
    if(ec < L) ec++;

    showLE();  // display textual content of le[] array
  }
}




/* THE KEYBOARD EVENTS HANDLER: A key press event generates two values: a
   keyboard Scan Code and an ASCII value. All key-press events generate a
   Scan Code but an ASCII value is given only for printable characters. For
   each key-press event, a Scan Code and an ASCII value [where applicable]
   are put in the global variables ks and ka, after which main() calls this
   function to act upon the values presented. Called from 1 place in main().*/

void KEH() {
  if(ea == 0 || tb == 6)  // if Line Editor not active or we're in Tab 6
    switch(ks) {  // These keys are used outside the Line Editor
      case 81:    // PgUp on number pad
      case 112: PgUpDn(-10); return;  // Page-Up [10 lines to the page]
      case 89:    // PgDn on number pad
      case 117: PgUpDn(+10); return;  // Page-Down
      case 88:    // down-arrow on number pad
      case 116: MWdn(); return;  // down-arrow
      case 80:    // up-arrow on number pad
      case 111: MWup(); return;  // up-arrow
    }
  // DEAL WITH NON-PRINTABLE CONTROL CHARACTERS IN LINE EDITOR
  switch(ks) {                       // KEYBOARD SCAN CODES SWITCH
    case  50: return;                // block the shift code
    case 113: leftArrow(); return;   // if LEFTWARDS arrow key pressed
    case 114: rightArrow(); return;  // if RIGHTWARDS-arrow key pressed
    case 22:  bkSp(); return;        // if 'Backspace' key pressed
    case 119: Del(); return;         // if 'Delete' key pressed
    case  36: CR(); return;          // carriage return
    case   9: Esc(); return;         // if 'Escape' key pressed.
    case 110: homeEnd(0); return;    // 'Home' key
    case 115: homeEnd(en); return;   // 'End' key
  } 
  if(ka < 32 || ka > 127) return;    // non-printable ASCII character
  lineEditor();
}




// OPEN ALL DATA FILES IN READ-UPDATE MODE. Called only from main().

void openFiles() {
  FA = fopen("data/nad.dat","r+");  // open the main name & address file
  fseek(FA,32,SEEK_SET);            // go to byte 32 of this file

  N0 = fgetc(FA);  // Byte 32: number of occupied records
  H0 = fgetc(FA);  // Byte 33: highest occupied record
  V0 = fgetc(FA);  // Byte 34: dummy 'people alert' flag
  W0 = fgetc(FA);  // Byte 35: LIST 'index re-sort needed' flags

  FB = fopen("data/diary.dat","r+"); // open the diary file

  N8 = fgetc(FB);  // Byte 0: current number of items in DIARY
  H8 = fgetc(FA);  // Byte 1: dummy: get highest occupied record
  V8 = fgetc(FB);  // Byte 2: 'diary alert' flags
  W8 = fgetc(FA);  // Byte 3: DIARY 'index re-sort needed' flags

  FC = fopen("data/crop.dat","r+");  // open the crops data file
  fseek(FC,32,SEEK_SET);             // go to byte 32 of this file

  N5 = fgetc(FC);  // Byte 32: number of occupied records
  H5 = fgetc(FC);  // Byte 33: highest occupied record
  V5 = fgetc(FB);  // Byte 34: setting of 'crops alert' flag
  W5 = fgetc(FB);  // Byte 35: crop 'index re-sort needed' flags

  FD = fopen("data/fin.dat","r+");  // open the financial transactions file

  N6 = fgetc(FD);  // Byte 0: number of transactions on file
  H6 = fgetc(FD);  // Byte 1: highest occupied invoice record
  V6 = fgetc(FD);  // Byte 2: state of 'payment alert' flags
  W6 = fgetc(FD);  // Byte 3: trans 'index re-sort needed' bits 

  T1getLand(1);           // to prime the local longitude
}




// MAIN FUNCTION FROM WHICH ALL THE ABOVE APPLICATION FUNCTIONS ARE CALLED

int main() {
  XEvent e;    // reference to any event from the created window
  int s,       // number of the current default screen
      mouseB;  // number of the mouse button that was pressed 0, 1, 2, 3, 4

  /* Check that you can actually open the X-display by getting a non-NULL
     reference to a display. Else send an error message to the terminal. */

  XD = XOpenDisplay(NULL);
  if(XD == NULL) { printf("Cannot open display\n"); exit(1); }

  s = DefaultScreen(XD);  // get the number of the default screen
  XG = DefaultGC(XD,s);   // set the graphics context to use

  XW = XCreateSimpleWindow(  // create an application window 
         XD,                 // within the default display 
         RootWindow(XD,s),   // within the root window [desktop] 
         10,10,              // position of top right corner 
         500,400,            // width and height of the window 
         1,                  // border width in pixels 
         BlackPixel(XD,s),   // colour of window's border
         BlackPixel(XD,s)    // colour of window's background
       );

  // set the window title
  XStoreName(XD,XW,"FRACTAL ECONOMY PROGRAM by R J Morton YE572246C");

  /* Make input possible from the keyboard, window controls and mouse
     then display the new window on the screen. Not all these bit masks
     are needed so comment out those that are not needed. */

  XSelectInput(XD,XW,
    ExposureMask | ButtonPressMask | PointerMotionMask | KeyPressMask
    //| KeyReleaseMask | ButtonReleaseMask | StructureNotifyMask
  );
  XMapWindow(XD,XW);  // map the window to the display
  XFlush(XD);         // flush the display events buffer

  /* Define a unique identifier WM_DELETE_WINDOW (referred to as an atom)
     that will invoke the X11 protocol that closes windows upon the display
     'XD' we have just created. [At least I think this is what it means] */

  Atom WM_DELETE_WINDOW = XInternAtom(XD,"WM_DELETE_WINDOW",False); 

  /* Make 'delete window' events from this program's
     window visible as a ClientMessage event. */

  XSetWMProtocols(XD,XW,&WM_DELETE_WINDOW,1);

  openFiles();  // open all application data files in read + update mode

  /* The event-handling [or 'run'] loop [a permanent loop] broken
     only by the 'break' function in response to an external event. */

  while (1) { 
    if(XPending(XD) > 0) {  // Provided there are events on the event queue,
      XNextEvent(XD,&e);    // go fetch the first event.
      if(e.type == Expose)  // if an 'expose' event has occurred
        showTab();          // draw content of the tab window
      else
      if(e.type == ButtonPress) {  // IF A MOUSE BUTTON CLICKED:
        if(e.xbutton.button == Button1) {  // if left mouse button clicked
          mx = e.xbutton.x;  // set x-coordinate of click
          my = e.xbutton.y;  // set y-coordinate of click
          MEH();             // call Mouse Event Handler
        }
        else
        if(e.xbutton.button == Button4) {  // mouse wheel up-button clicked
          mx = e.xbutton.x;  // set x-coordinate of click
          my = e.xbutton.y;  // set y-coordinate of click
          MWup();            // mouse wheel scroll up
        }
        else
        if(e.xbutton.button == Button5) {  // mouse wheel down-button clicked
          mx = e.xbutton.x;  // set x-coordinate of click
          my = e.xbutton.y;  // set y-coordinate of click
          MWdn();            // mouse wheel scroll down
        }
      }
      else if(e.type == KeyPress) {     // IF A KEY HAS BEEN PRESSED:
        ks = e.xkey.keycode;            // keyboard scan code of pressed key
        ka = (int)XkbKeycodeToKeysym (  // ASCII code of the scanned key

             XD,   // window display pointer
             ks,   // key scan [position] code
             0,    // key level
             e.xkey.state & ShiftMask ? 1 : 0  // upper or lower case

        ); KEH();  // Keyboard Event Handler [KEH]
      } 

      /* Else if a box control '- + x' has been clicked, break out of the
      permanent while() loop, close the window and exit. Should also check
      here for other client message types; however, as the only protocol
      registered above is WM_DELETE_WINDOW, it is safe for this program. */

      else if (e.type == ClientMessage) break;
    } else {
      T3sunTime(0);   // call the 1-second service function
      usleep(50000);  // sleep for 50 milliseconds
    }
  }    // end of outer permanent while() loop
  XCloseDisplay(XD);  // clear this window from the X11 display 'XD'
  fclose(FA);  // close the name and address file 'nad.dat'
  fclose(FB);  // close the 'diary.dat'
  fseek(FC,34,SEEK_SET);  // seek start of flag data in 'crop.dat'
  fputc(V5,FC);  // save the crop alert flag
  fclose(FC);    // close the crops file 'crop.dat'
  fseek(FD,2,SEEK_SET);  // seek start of flag data in 'fin.dat'
  fputc(V6,FD);  // save the invoice alert flag
  fclose(FD);    // close the financial transactions file 'fin.dat'
  return 0;  // return that everything went OK
}            // end of main() : END OF PROGRAM : R J Morton