ukopp-4.9/0000775000175000017500000000000012333076512011170 5ustar micomicoukopp-4.9/doc/0000755000175000017500000000000012333076511011732 5ustar micomicoukopp-4.9/doc/changelog0000644000175000017500000002424712333076511013615 0ustar micomicoukopp change log ================ 2014.05.09 v.4.9 + Revise for changes in GTK functionality with Ubuntu 14.04. 2013.10.12 v.4.8 + Ignore incorrect EBUSY status from rmdir() kernel bug: https://lkml.org/lkml/2013/8/28/654 2013.03.01 v.4.7 + Clarifications in GUI and user guide. 2012.11.01 v.4.6 + Replaced deprecated GTK functions with new versions. 2012.07.20 v.4.5 + Toolbar text was added back after Gnome decided to remove it. + Minor improvement in error reporting. 2012.04.23 v.4.4 + Instead of doing a system-wide sync command after all files are copied, fsync() is called at the end of each file. This is a little slower but leaves the file system in good shape if the job does not complete. + Bugfix: overflow of memory array and crash if the file limit (200K) was exceeded. The bug was fixed and the limit raised to 500K files. 2012.02.01 v.4.3 + Ukopp was converted to use GTK3 and Cairo. It will no longer build or install on older Linux distros lacking these libraries. + Target disk space statistics added to differences summary report. + RPM packages are built using Fedora and rpmbuild instead of alien. 2011.10.29 v.4.1 Many small improvements in usability and minor error corrections. + Main window states if ukopp is running as root or not. + If backup + verify, report both backup and verify status after verify. + If target directory does not exist, warn and offer to create it. + Better visual separation of fields in target chooser dialog. + Clarify backup menu as "backup (no verify)". + Widen fields in target chooser dialog. + Change the mouse pointer into a clock during a long-running function. + Stop job name from disappearing from main window title. + "include /aaa/bbb" same as "include /aaa/bbb/*" if bbb is a directory. + Expand input fields with the window in restore files dialog. + Restore original target device and directory if edit job is canceled. + File names can be quoted to handle files ending in a blank character. + Some error messages were clarified and dialogs kept open afterwards. + The file chooser dialog no longer hides the restore files dialog. + New report: summary stats for new/mod/deleted files by directory. + The tolerance to consider a disk file and its corresponding backup file to have the same modification time was increased from 1 to 2 seconds. This is because the Microsoft VFAT time resolution is 2 seconds. + All error messages are shown in the main window (not stdout). + Bugfix: purge expired backup versions also for files NOT being copied. + Prevent Choose Target and Edit Job dialogs from running in parallel. + Many small improvements were made to the user guide. 2011.09.22 v.4.0 + Code cleanup for compiler warnings with GCC 4.6 (Ubuntu 11.10). + Separate File Save (no dialog) and Save As (file chooser dialog). + Added a warning for Quit with unsaved changes to backup job. + Job name was added to window title, with (*) indicator if edited. + Excluded files/folders no longer need to start with a top directory (e.g. exclude */folder1/fol*der2/* works as expected). + Toolbar has more descriptive icons for mount and unmount. + Bugfix: one more file versions than requested was being saved. + Bugfix: quit job edit dialog left dependent file chooser dialog open. + Bugfix: report saved file versions: wrong value for expired bytes. 2011.05.01 v.3.9 + A man page was added. + Window is dumped to log file before exit. + Detect error: selected file outside restore-from directory hierarchy. 2010.08.21 v.3.8 + A button [root] was added to the toolbar. This restarts the application with root privileges if the queried password (sudo) is correct. 2010.05.19 v.3.7.1 + Implement show/hide hidden files in restore file chooser dialog. 2010.03.25 v.3.7 + Improve diagnostics when target file system does not support required functions (large file, direct I/O) or there is insufficient permission. + Bail out when there are excessive errors (e.g. permissions error). + Makefile was made more package-builder friendly. 2009.12.26 v.3.6 + Support non-GUI mode for command-line / deferred execution (cron). (e.g. $ ukopp -nogui -run /.../my-ukopp.job) 2009.11.08 v.3.5.3 + bugfix - file compare fails with NTFS (Windows) file system. Workaround implemented: avoid open(...O_DIRECT) with NTFS. 2009.10.25 v.3.5.2 + Do remount only if device was mounted by ukopp, else use sync. + Use direct I/O for verify - sync leaves files in memory cache which defeats media verification. 2009.10.21 v.3.5.1 + Do remount instead of sync to insure cache is written to disk (faster and affects only one device instead of all of them). 2009.10.03 v.3.5 + GUI revised: retention specs for old file versions on the backup media are easier to specify and more conventional. Other changes to the GUI were done to make it easier to use. Changes to job file format will require minor re-editing. + Better detection of conflicts bewteen user intent and actual status. + Do auto unmount at end of job only if mount was done at start. + Auto deletion of mount point directory only when created by ukopp. + Bugfix: include/exclude filespec containing blanks did not work. 2009.09.23 v.3.4.2 Avoid deletion of non-existant directory and confusing error message. 2009.07.13 v.3.4.1 fix compile errors introduced by gcc 4.4 2009.03.30 v.3.4 The Linux program "udevinfo" has recently become "udevadm info". Ukopp was modified to use whichever variant works. 2009.03.26 v.3.3.2 Trivial changes to error messages. 2008.11.05 v.3.3.1 The USB stick format utility (vfat/ext2) was broken and has been fixed. 2008.11.02 v.3.3 The output format of the udevinfo command was changed, breaking ukopp's ability to find all mounted and unmounted disk devices. This version of ukopp works with both the new and old udevinfo formats. 2008.09.20 v.3.2 + Implement two methods for flushing the I/O memory cache to the backup device between backup and verify operations: sync command (no change) and unmount/remount of target device (somewhat faster). + Improve convenience and robustness of backup target selection. + two minor bugs were fixed. 2008.08.03 v.3.0 and v.3.1 + Do not discard symlink files - include them like regular files (symlink targets are included only if specified in job file) + Verify symlinks using readlink() function (v.3.1) + user guide changed from PDF file to HTML 2008.06.01 v. 2.9 + change from build script to traditional makefile + change name from ukop to ukopp 2008.05.24 v.28 + fix minor bug from linux file sort utility not consistent with strcmp() 2008.04.20 v.27 + fix for change in behavior of lstat64() in newest c-lib + fix compiler warnings in newest gcc (stricter rules) 2008.03.20 v.26 + make synchronize function work for VFAT (Microsoft) backup medium (use separate file for preserving owner and permissions data) + add command to format a backup device with vfat or ext2 file system 2008.02.29 v.25 + add function to synchronize disk and backup medium in both directions (new and newer files are copied from disk to backup or the reverse) + modify differences report to show which file is newer: disk or backup + copy owner and permissions for created directories and files 2008.02.13 v.24 + bugfix: crash due to defective default job file 2008.02.09 v.23 + avoid confusing check of backup target directory + user confirmation of backup target before starting backup job 2008.02.04 v.22 + retain specification includes optional filespec with wildcards (specify retain spec at finer level than include record) + new report: expired file versions (to be purged from backup location) 2008.01.29 v.21 + separate build scripts for downloaders and package builders + code changes to get application directories from build script + cosmetic improvements in report formats and error messages + correct error in file versions report (Linux sorts '.' before ' ') 2008.01.20 v.20 + backup file versioning: retain prior file versions for designated time period and/or number of retained versions. + flush disk cache between backup and verify to insure verify data is coming from the USB drive (replaces prior method using O_DIRECT). 2008.01.01 v.12 + build script allows user to choose install location and desktop icon 2007.12.10 v.11 + code cleanup for 64-bit architecture compatibility + verify thorough changed: all USB files are verified (job file no longer relevant) 2007.11.15 v.10 + new GTK requirement: if (! g_thread_supported()) g_thread_init(0); 2007.11.12 v.09 + zdialog_add_widget, options argument delimiter change 2007.10.15 v.08 + fix significant bug in thorough verify: files not being compared 100% + don't lose comment records in job file + eliminate "end" record in job file (end is EOF) 2007.09.27 v.07 + symlinks are copied, but target files are copied only if within the backup file set (see user guide about restrictions with FAT file system) + accept job file on command line (without -job) (per Linux convention) + output a summary differences report when backup job begins + remove USB stick mount point from backup job file (does not belong there) + correct several errors in user guide + minor improvements to user interface 2007.07.06 v.06 + follow Linux convention for app files in /home/username/.ukop/ or /root/.ukop/ (move your job files here for convenience) + allow show / hide of hidden files in job open / save dialogs + report and command outputs made a bit more clear 2007.06.15 v.05 + if directory is deleted and parent is now empty, delete parent + if a file fails to copy, report error in backup, not in verify + new technical note about file name limitations for FAT media 2007.05.28 v.04 + add convenience buttons for file load and save to job edit dialog + if last file in a USB stick directory is deleted, delete the directory + increase file limit from 100K to 200K files (memory usage +15 MB) 2007.05.13 v.03 + replace menu and toolbar macros with zfuncs functions 2007.04.25 v.02 + fix icon on toolbar button for USB stick selection + add report: disk:USB stick differences by directory + allow file size exceeding 2 gigabytes 2007.04.12 v.01 initial release + backup job: open, edit, save, run + backup: incremental, accumulate + verify: full, incremental, thorough + disk / USB stick differences reports + restore function ukopp-4.9/doc/README0000644000175000017500000000244712333076511012621 0ustar micomicoInstallation of ukopp from source tarball Building ukopp requires the following packages: g++ the Gnu C++ compiler and linker libgtk3.0-dev GTK graphics library (GUI base) Build and install ukopp as follows: 1. Download the tar file (ukopp-N.N.tar.gz) to Desktop 2. Open a terminal window 3. $ cd Desktop # go to Desktop 4. $ tar -xzf ukopp-N.N.tar.gz # unpack to ./ukopp 5. $ cd ukopp # go there 6. $ make # build program 7. $ sudo make install # install program Missing dependencies will cause error messages in step 6. Install these from your repository and repeat step 6. Step 7 moves all files to the following locations: /usr/bin/ukopp binary executable /usr/share/ukopp/ icons, translations ... /usr/share/doc/ukopp/ user guide, README ... For step 7, use "sudo" or "su -c" to get root privileges. Please review the user guide (Help menu) before trying ukopp. If you need to run ukopp as root, modify the launcher to put "gksu" in front of the command: gksu /.../ukopp NOTES FOR PACKAGE BUILDERS: If $PREFIX is defined, files go there instead of /usr. If $DESTDIR is also defined, files go to $DESTDIR$PREFIX. ukopp-4.9/doc/ukopp.man0000644000175000017500000000353512333076511013573 0ustar micomico.TH UKOPP 1 2010-10-01 "Linux" "Ukopp man page" .SH NAME Ukopp - copy files to USB stick or other disk or disk-like device .SH SYNOPSIS \fBukopp\fR [ \fB-nogui\fR ] [ \fB-job\fR | \fB-run\fR ] \fIjobfile\fR .SH DESCRIPTION Ukopp is a graphical menu-driven program operating in its own window. Ukopp copies the files and directories specified in the job file to disk or disk-like media (e.g. USB stick). .SH OVERVIEW Ukopp works incrementally: only files that are not already identical on the target media are copied (new and modified files or files newly added to the backup job). Files/directories to include or exclude can be selected from the file system hierarchy, using a GUI navigator. Specifications are saved in a job file which can be re-edited and/or executed. Ukopp can be run in batch mode using the \-nogui option. Ukopp can optionally keep old file versions instead of replacing them with updated versions. The retention time and/or the number of old versions to keep can be specified for each selected file or directory. File owners and permissions are retained even if the copy media has a Microsoft file system. Summary of functionality: - Copy files incrementally with optional file version retention. - Three media verification modes: full, incremental, compare. - Report disk:backup differences in detail or summary form. - Select and restore files from backup media or use drag and drop. .SH OPTIONS Command line options: [ \fB-job\fR ] \fIjobfile\fR open job file for editing [ \fB-nogui\fR ] \fB-run\fR \fIjobfile\fR execute a job file .SH SEE ALSO The online user manual is available using the menu Help > contents. This manual explains Ukopp operation in great detail. .SH AUTHORS Written by Michael Cornelison ukopp-4.9/doc/copyright0000644000175000017500000000100112333076511013655 0ustar micomicoAuthor: Michael Cornelison Copyright: Copyright 2007-2014 Michael Cornelison The source program code can be found here: http://kornelix.com/tarballs License: You are free to distribute this software under the terms of the GNU General Public Licensel, either version 3 of the License, or (at your option) any later version. The complete text of the license can be found here: http://www.gnu.org/licenses/gpl-3.0.html See "/usr/share/common-licenses/GPL-3". ukopp-4.9/zfuncs.h0000644000175000017500000013557012333076511012661 0ustar micomico/************************************************************************** zfuncs.h include file for zfuncs functions Copyright 2006-2014 Michael Cornelison source URL: kornelix.com contact: kornelix@posteo.de This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ***************************************************************************/ // zfuncs.h version v.5.8 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define VERTICAL GTK_ORIENTATION_VERTICAL // GTK shortcuts #define HORIZONTAL GTK_ORIENTATION_HORIZONTAL #define int8 char // number types #define int16 short #define int32 long #define int64 long long #define uint8 unsigned char #define uint16 unsigned short #define uint32 unsigned long #define uint64 unsigned long long #define uchar unsigned char #define cchar const char #define wstrerror(err) strerror(WEXITSTATUS(err)) // get text status for child process #define mutex_tp pthread_mutex_t // abbreviations #define mutex_init pthread_mutex_init #define mutex_lock pthread_mutex_lock #define mutex_trylock pthread_mutex_trylock #define mutex_unlock pthread_mutex_unlock #define mutex_destroy pthread_mutex_destroy #define maxfcc 1000 // max. file pathname cc tolerated #define null NULL #define true 1 #define false 0 #define TRACE trace(__FILE__,__FUNCTION__,__LINE__); // trace execution by funcion and line no. // system functions ====================================================== void printz(cchar *format, ...); // printf() with immediate fflush() v.5.8 void apppause(); // output message and wait for user void apppause(cchar * format, ... ); // same, works like printf void zpopup_message(cchar *format, ...); // popup message, thread safe void zappcrash(cchar *format, ...); // crash with popup message in text window void catch_signals(); // catch signals and do backtrace dump void trace(cchar *file, cchar *func, int line); // implements TRACE macro void tracedump(); // dump program trace data void beroot(int argc = 0, char *argv[] = 0); // restart image as root if password OK double get_seconds(); // seconds since 2000.01.01 void start_timer(double &time0); // start a timer double get_timer(double &time0); // get elapsed time in seconds void start_CPUtimer(double &time0); // start a process CPU timer double get_CPUtimer(double &time0); // get elapsed CPU time, seconds double CPUtime(); // get elapsed process CPU time for main() double CPUtime2(); // " include all threads double jobtime(); // " include all threads + subprocesses void compact_time(const time_t fileDT, char *compactDT); // time_t date/time to yyyymmddhhmmss int parseprocfile(cchar *pfile, cchar *pname, double *value, ...); // get data from /proc file int parseprocrec(char *prec, int field, double *value, ...); // get data from /proc file record void zsleep(double dsecs); // sleep specified seconds int global_lock(cchar *lockfile); // obtain exclusive lock, multi-process int global_unlock(int fd, cchar *lockfile); // release the lock void start_detached_thread(void * tfunc(void *), void * arg); // start detached thread function void synch_threads(int NT = 0); // synchronize NT threads int zget_locked(int ¶m); // lock and get multi-thread parameter void zput_locked(int ¶m, int value); // put and unlock multi-thread parameter int zadd_locked(int ¶m, int incr); // increment multi-thread parameter int shell_quiet(cchar *command, ...); // format/run shell command, return status int shell_ack(cchar *command, ...); // "" + popup an error message if error int shell_asynch(cchar *command, ...); // start shell command, return immediately int shell_asynch_status(int handle); // get status of asynch shell command char * command_output(int &contx, cchar *command, ...); // get shell command output int command_status(int contx); // get exit status of command int command_kill(int contx); // kill command before completion int signalProc(cchar * pname, cchar * signal); // pause/resume/kill subprocess int runroot(cchar *sucomm, cchar *command); // run command as root via su or sudo int checkinstall(cchar *prog, ...); // check if programs are installed char * fgets_trim(char * buff, int maxcc, FILE *, int bf = 0); // fgets + trim trailing \n \r (blanks) int samedirk(cchar *file1, cchar *file2); // returns 1 if files in same directory int parsefile(cchar *path, char **dir, char **file, char **ext); // parse a filespec int renamez(cchar *file1, cchar *file2); // rename, also across file systems // measure CPU time spent in a function or code block within a function extern volatile double cpu_profile_timer; // internal data tables extern volatile double cpu_profile_table[100]; extern volatile double cpu_profile_elapsed; void cpu_profile_init(); // initialize at start of test void cpu_profile_report(); // report CPU time per function inline void cpu_profile_enter(int fnum) // at entry to measured code block { cpu_profile_timer = cpu_profile_elapsed; } inline void cpu_profile_exit(int fnum) // at exit from measured code block { cpu_profile_table[fnum] += cpu_profile_elapsed - cpu_profile_timer; } int pagefaultrate(); // monitor own process hard fault rate // string macros and functions =========================================== #define strEqu(str1,str2) (! strcmp((str1),(str2))) // TRUE if strings compare equal #define strcaseEqu(str1,str2) (! strcasecmp((str1),(str2))) // TRUE if strings equal, ignoring case #define strNeq(str1,str2) (strcmp((str1),(str2))) // TRUE if strings compare not-equal #define strnEqu(str1,str2,cc) (! strncmp((str1),(str2),(cc))) // TRUE if strings[cc] compare equal #define strncaseEqu(str1,str2,cc) (! strncasecmp((str1),(str2),(cc))) // TRUE if strings[cc] equal, ignoring case #define strnNeq(str1,str2,cc) (strncmp((str1),(str2),(cc))) // TRUE if strings[cc] compare not-equal cchar * strField(cchar *string, cchar *delims, int Nth); // get Nth delimited field in string cchar * strField(cchar *string, cchar delim, int Nth); // get Nth delimited field in string int strParms(int &bf, cchar *inp, char *pname, int maxcc, double &pval); // parse string: name1=val1 | name2 ... int strHash(cchar *string, int max); // string --> random int 0 to max-1 cchar * strHash2(cchar *string, int outcc); // string --> random printable string int strncpy0(char *dest, cchar *source, uint cc); // strncpy, insure null, return 0 if fit void strnPad(char *dest, cchar *source, int cc); // strncpy with blank padding to cc int strTrim(char *dest, cchar *source); // remove trailing blanks int strTrim(char *string); // remove trailing blanks int strTrim2(char *dest, cchar *source); // remove leading and trailing blanks int strTrim2(char *string); // remove leading and trailing blanks int strCompress(char *dest, cchar *source); // remove all blanks incl. imbedded int strCompress(char *string); // remove all blanks int strncatv(char *dest, int maxcc, cchar *source, ...); // catenate strings (last = null) int strcmpv(cchar *string, ...); // compare to N strings, return 1-N or 0 void strToUpper(char *dest, cchar *source); // move and conv. string to upper case void strToUpper(char *string); // conv. string to upper case void strToLower(char *dest, cchar *source); // move and conv. string to lower case void strToLower(char *string); // conv. string to lower case int repl_1str(cchar *strin, char *strout, cchar *ssin, cchar *ssout); // copy string and replace 1 substring int repl_Nstrs(cchar *strin, char *strout, ...); // copy string and replace N substrings void strncpyx(char *out, cchar *in, int ccin); // conv. string to hex format void StripZeros(char *pNum); // 1.230000E+12 --> 1.23E+12 int blank_null(cchar *string); // test for blank/null string char * strdupz(cchar *string, int more = 0); // duplicate string and add space at end int clean_escapes(char *string); // replace \x escapes with real characters int utf8len(cchar *utf8string); // get graphic cc for UTF8 string int utf8substring(char *utf8out, cchar *utf8in, int pos, int cc); // get graphic substring from UTF8 string int utf8_check(cchar *string); // check utf8 string for encoding errors int utf8_position(cchar *utf8in, int Nth); // get byte position of Nth graphic char. // number conversion ===================================================== int convSI (cchar *inp, int &inum, cchar **delm = 0); // string to int int convSI (cchar *inp, int &inum, int low, int hi, cchar **delm = 0); // (with low/high limit checking) int convSD (cchar *inp, double &dnum, cchar **delm = 0); // string to double int convSD (cchar *inp, double &dnum, double low, double hi, cchar **delm = 0); // (with low/high limit checking) int convSF (cchar *inp, float &fnum, cchar **delm = 0); // string to double int convSF (cchar *inp, float &fnum, float low, float hi, cchar **delm = 0); // (with low/high limit checking) int convIS (int iin, char *outp, int *cc = 0); // int to string, returned cc int convDS (double din, int prec, char *outp, int *cc = 0); // double to string, precision, ret. cc char * formatKBMB(double fnum, int prec); // format nnn B, nn.n KB, n.nn MB, etc. // wildcard functions ==================================================== int MatchWild(cchar * wildstr, cchar * str); // wildcard string match (match = 0) int MatchWildIgnoreCase(cchar * wildstr, cchar * str); // wildcard string match, ignoring case cchar * SearchWild(cchar *wpath, int &flag); // wildcard file search cchar * SearchWildIgnoreCase(cchar *wpath, int &flag); // wildcard file search, ignoring case // search and sort functions ============================================= int bsearch(int seekint, int nn, int list[]); // binary search sorted list[nn] int bsearch(char *seekrec, char *allrecs, int recl, int nrecs); // binary search sorted records int bsearch(char *seekrec, char **allrecs, int N, int nrecs); // binary search of sorted pointers to recs typedef int HeapSortUcomp(cchar *rec1, cchar *rec2); // return -1/0/+1 if rec1 rec2 void HeapSort(int vv[], int nn); // Heap Sort - integer void HeapSort(float vv[], int nn); // Heap Sort - float void HeapSort(double vv[], int nn); // Heap Sort - double void HeapSort(char *vv[], int nn); // Heap Sort - char *, ascending order void HeapSort(char *vv[], int nn, HeapSortUcomp); // Heap Sort - char *, user-defined order void HeapSort(char *recs, int RL, int NR, HeapSortUcomp); // Heap Sort - records, user-defined order int MemSort(char * RECS, int RL, int NR, int KEYS[][3], int NK); // memory sort, records with multiple keys // bitmap functions ====================================================== struct bitmap { int nbits; // declare bitmap: bitmap *bmap uchar *bits; }; bitmap * bitmap_new(int nbits); // create and initialize all bits to zero void bitmap_set(bitmap *bm, int bit, bool value); // set a bit true or false bool bitmap_get(bitmap *bm, int bit); // get a bit value, true or false void bitmap_delete(bitmap *bm); // delete bitmap, release memory // variable string list functions ======================================== struct pvlist { int max; // max. entries int act; // actual entries char **list; // entries }; pvlist * pvlist_create(int max); // create pvlist void pvlist_free(pvlist *pv); // free pvlist int pvlist_append(pvlist *pv, cchar *entry, int unique = 0); // append new entry (opt. if unique) int pvlist_prepend(pvlist *pv, cchar *entry, int unique = 0); // prepend new entry (opt. if unique) int pvlist_find(pvlist *pv, cchar *entry); // find entry by name int pvlist_remove(pvlist *pv, cchar *entry); // remove entry by name int pvlist_remove(pvlist *pv, int Nth); // remove entry by number (0...) int pvlist_count(pvlist *pv); // return entry count int pvlist_replace(pvlist *pv, int Nth, cchar *entry); // replace Nth entry (0...) char * pvlist_get(pvlist *pv, int Nth); // return Nth entry (0...) int pvlist_sort(pvlist *pv); // sort list, ascending // random number functions =============================================== int lrandz(int64 * seed); // returns 0 to 0x7fffffff int lrandz(); // built-in seed double drandz(int64 * seed); // returns 0.0 to 0.99999... double drandz(); // built-in seed // spline curve-fitting functions ======================================== void spline1(int nn, float *dx, float *dy); // define a curve using nn data points float spline2(float x); // return y-value for given x-value // FIFO queue for text strings, single or dual-thread access ============= v.5.7 typedef struct { int qcap; // queue capacity int qnewest; // newest entry position, circular int qoldest; // oldest entry position, circular int qdone; // flag, last entry added to queue char **qtext; // up to qcap text strings } Qtext; void Qtext_open(Qtext *qtext, int cap); // initialize Qtext queue, empty void Qtext_put(Qtext *qtext, cchar *format, ...); // add text string to Qtext queue char * Qtext_get(Qtext *qtext); // remove text string from Qtext queue void Qtext_close(Qtext *qtext); // free() leftover strings if any // application initialization and administration ========================= int zinitapp(cchar *appname, ...); // initz. app directories and files v.4.1 cchar * get_zprefix(); // get /usr or /usr/local ... v.4.1 cchar * get_zuserdir(); // get /home/user/.appname/ cchar * get_zdatadir(); // get data directory cchar * get_zdocdir(); // get document directory cchar * get_zicondir(); // get icon directory v.5.7 int locale_filespec(cchar *ftype, cchar *fname, char *filespec); // get a locale dependent file v.5.5 void showz_userguide(cchar *context = 0); // show user guide in new process void showz_logfile(); // show log file in popup window v.5.2 void showz_textfile(cchar *type, cchar *file); // show text file [.gz] in popup window void showz_html(cchar *url); // show html via preferred browser void zmake_menu_launcher(cchar *command, cchar *cats, cchar *generic); // add desktop menu and launcher v.4.1 /**************************************************************************/ // translation functions #define ZTXmaxent 2000 // max. translation strings #define ZTXmaxcc 4000 // max. cc per string void ZTXinit(cchar *lang); // setup for message translation cchar * ZTX(cchar *english); // get translation for English message cchar * ZTX_missing(int &ftf); // get missing translations, one per call /************************************************************************** GTK utility functions ***************************************************************************/ void zmainloop(int skip = 0); // do main loop, process menu events void zthreadcrash(); // crash if thread is not main() thread // text window print and read utilities void wprintx(GtkWidget *Win, int line, cchar *mess, cchar *font = 0); // write text to line, optional font void wprintf(GtkWidget *Win, int line, cchar *format, ...); // "printf" version void wprintf(GtkWidget *Win, cchar *format, ... ); // "printf" to next line, scroll up void wscroll(GtkWidget *mLog, int line); // scroll window to put line on screen void wclear(GtkWidget *Win); // clear window void wclear(GtkWidget *Win, int line); // clear from line to end char * wscanf(GtkWidget *Win, int &ftf); // get text lines from edit widget int wfiledump(GtkWidget *Win, char *filespec); // dump text window to file void wfilesave(GtkWidget *Win); // wfiledump() via file-chooser dialog void wprintp(GtkWidget *Win); // print text window to default printer // intercept text window mouse click functions typedef void clickfunc_t(GtkWidget *widget, int line, int pos); // function to get clicked text position void textwidget_set_clickfunc(GtkWidget *widget, clickfunc_t clickfunc); // (wrapped lines are one logical line) char * textwidget_get_line(GtkWidget *widget, int line, int hilite); // get entire line at clicked position char * textwidget_get_word(char *line, int pos, cchar *dlims, char &end); // get delimited word at clicked position // get the screen and mouse for a given widget int get_mouse_device(GtkWidget *, GdkScreen **screen, GdkDevice **mouse); // return pointers to screen and mouse // functions to simplify building menus, tool bars, status bars #define G_SIGNAL(window,event,func,arg) \ g_signal_connect(G_OBJECT(window),event,G_CALLBACK(func),(void *) arg) #define zdcbmax 100 // max. combo box drop-down list typedef void cbFunc(GtkWidget *, cchar *mname); // menu or button response function GtkWidget * create_menubar(GtkWidget *vbox); // create menubar in packing box GtkWidget * add_menubar_item(GtkWidget *mbar, cchar *mname, cbFunc func = 0); // add menu item to menubar GtkWidget * add_submenu_item(GtkWidget *mitem, cchar *subname, // add submenu item to menu item cbFunc func = 0, cchar *mtip = 0); // with opt. function and popup tip GtkWidget * create_toolbar(GtkWidget *vbox, int iconsize = 24); // toolbar in packing box (no vert gtk3) GtkWidget * add_toolbar_button(GtkWidget *tbar, cchar *lab, cchar *tip, // add button with label, tool-tip, icon cchar *icon, cbFunc func); GtkWidget * create_stbar(GtkWidget *vbox); // create status bar in packing box int stbar_message(GtkWidget *stbar, cchar *message); // display message in status bar /**************************************************************************/ GtkWidget * create_popmenu(); // create an empty popup menu GtkWidget * add_popmenu_item(GtkWidget *popmenu, cchar *mname, // add menu item to popup menu cbFunc func, cchar *arg, cchar *mtip = 0); void popup_menu(GtkWidget *, GtkWidget *popmenu); // pop-up menu at current mouse posn. /**************************************************************************/ // user editable graphic menu in popup window // menus can be added and arranged using the mouse typedef void gmenuz_cbfunc(cchar *menu); // caller-supplied callback function void gmenuz(GtkWidget *parent, cchar *title, cchar *ufile, gmenuz_cbfunc); // show window, handle mouse drag/click /**************************************************************************/ // create vertical menu/toolbar in vertical packing box // v.5.5 struct vmenuent { // menu data from caller cchar *name; // menu name, text cchar *icon; // opt. icon file name cchar *desc; // description (mouse hover popup) cbFunc *func; // callback func (GtkWidget *, cchar *arg) cchar *arg; // callback arg GdkPixbuf *pixbuf; // icon pixbuf or null PangoLayout *playout1, *playout2; // normal and bold menu text int namex, namey; // menu name position in layout int iconx, icony; // menu icon position int ylo, yhi; // menu height limits int iconww, iconhh; // icon width and height }; struct Vmenu { GtkWidget *vbox; // parent window GtkWidget *layout; // drawing window int xmax, ymax; // layout size v.5.8 int mcount; // menu entry count vmenuent menu[100]; }; Vmenu *Vmenu_new(GtkWidget *vbox); // create new menu in parent vbox void Vmenu_add(Vmenu *vbm, cchar *name, cchar *icon, // add menu item with response function int iconww, int iconhh, cchar *desc, cbFunc func, cchar *arg); // function may be popup_menu() /**************************************************************************/ // functions to implement GTK dialogs with less complexity // widget types: dialog, hbox, vbox, hsep, vsep, frame, scrwin, label, entry, edit, radio, // check, button, togbutt, spin, combo, comboE, hscale, vscale, colorbutt #define zdmaxwidgets 300 // v.5.8 #define zdmaxbutts 10 #define zdsentinel 2138687954 struct zwidget { cchar *type; // dialog, hbox, vbox, label, entry ... cchar *name; // widget name cchar *pname; // parent (container) name char *data; // widget data, initial / returned pvlist *cblist; // combo box drop-down list int scc; // entry widget: text cc width int homog; // hbox/vbox: equal spacing flag int expand; // expandable flag int space; // extra padding space (pixels) int wrap; // wrap mode for edit widget int stopKB; // stop propagation of KB events to parent GtkWidget *widget; // GTK widget pointer }; struct zdialog { int sentinel; // validity sentinel void *eventCB; // widget event user callback function int zstat; // dialog status (from completion button) int disabled; // widget signals/events are disabled int saveposn; // save and recall window position each use int saveinputs; // save and recall user inputs each use int stopKB; // flag, next KB event will be ignored GtkWidget *parent; // parent window or null GtkWidget *compbutt[zdmaxbutts]; // dialog completion buttons zwidget widget[zdmaxwidgets]; // dialog widgets (EOF = type = 0) }; zdialog *zdialog_new(cchar *title, GtkWidget *parent, ...); // create a zdialog with opt. buttons int zdialog_add_widget(zdialog *zd, // add widget to zdialog cchar *type, cchar *name, cchar *pname, // required args cchar *data = 0, int scc = 0, int homog = 0, // optional args int expand = 0, int space = 0, int wrap = 0); int zdialog_add_widget(zdialog *zd, // add widget to zdialog cchar *type, cchar *name, cchar *pname, // (alternative form) cchar *data, cchar *options); // "scc=nn|homog|expand|space=nn|wrap" GtkWidget * zdialog_widget(zdialog *zd, cchar *name); // GTK widget from zdialog widget name int zdialog_resize(zdialog *zd, int width, int height); // set size > widget sizes int zdialog_put_data(zdialog *zd, cchar *name, cchar *data); // put data in widget (entry, spin ...) cchar * zdialog_get_data(zdialog *zd, cchar *name); // get widget data int zdialog_set_limits(zdialog *zd, cchar *name, double min, double max); // set new widget limits (spin, scale) typedef int zdialog_event(zdialog *zd, cchar *name); // widget event callback function int zdialog_run(zdialog *zd, zdialog_event = 0, cchar *posn = 0); // run dialog, handle events void KBstate(GdkEventKey *event, int state); // extern: pass KB events to main app int zdialog_send_event(zdialog *zd, cchar *event); // send an event to an active dialog int zdialog_send_response(zdialog *zd, int zstat); // complete a dialog, set status int zdialog_show(zdialog *zd, int flag); // show or hide a dialog v.5.2 int zdialog_destroy(zdialog *zd); // destroy dialog (caller resp.) int zdialog_free(zdialog *&zd); // free zdialog memory int zdialog_wait(zdialog *zd); // wait for dialog status or be destroyed int zdialog_goto(zdialog *zd, cchar *name); // put cursor at named widget void zdialog_set_cursor(zdialog *zd, GdkCursor *cursor); // set cursor for dialog window int zdialog_stuff(zdialog *zd, cchar *name, cchar *data); // stuff string data into widget int zdialog_stuff(zdialog *zd, cchar *name, int data); // stuff int data int zdialog_stuff(zdialog *zd, cchar *name, double data); // stuff double data int zdialog_fetch(zdialog *zd, cchar *name, char *data, int maxcc); // get string data from widget int zdialog_fetch(zdialog *zd, cchar *name, int &data); // get int data int zdialog_fetch(zdialog *zd, cchar *name, double &data); // get double data int zdialog_fetch(zdialog *zd, cchar *name, float &data); // get float data int zdialog_cb_app(zdialog *zd, cchar *name, cchar *data); // append entry to combo drop down list int zdialog_cb_prep(zdialog *zd, cchar *name, cchar *data); // prepend entry to combo drop down list char * zdialog_cb_get(zdialog *zd, cchar *name, int Nth); // get combo drop down list Nth entry int zdialog_cb_delete(zdialog *zd, cchar *name, cchar *data); // delete combo drop down list entry int zdialog_cb_clear(zdialog *zd, cchar *name); // clear all combo box entries int zdialog_cb_popup(zdialog *zd, cchar *name); // show all combo box list entries int zdialog_positions(cchar *action); // load or save zdialog window positions void zdialog_set_position(zdialog *zd, cchar *posn); // set initial/new zdialog window position void zdialog_save_position(zdialog *zd); // save zdialog window position int zdialog_inputs(cchar *action); // load or save zdialog input fields int zdialog_save_inputs(zdialog *zd); // save zdialog input fields when finished int zdialog_restore_inputs(zdialog *zd); // restore zdialog inputs from prior use // write text to popup window, shell command to popup window GtkWidget * write_popup_text(cchar *action, cchar *text = 0, int ww = 0, int hh = 0, GtkWidget *parent = 0); int popup_command(cchar *command, int ww = 400, int hh = 300, GtkWidget *parent = 0); // popup message dialogs void zmessageACK(GtkWidget *parent, cchar *title, cchar *format, ... ); // display message, wait for OK void zmessLogACK(GtkWidget *parent, cchar *format, ... ); // same, with log to STDOUT int zmessageYN(GtkWidget *parent, cchar *format, ... ); // display message, wait for YES/NO zdialog * zmessage_post(GtkWidget *parent, int secs, cchar *format, ...); // show message, timeout or cancel char * zdialog_text(GtkWidget *parent, cchar * title, cchar * initext); // get short text input from user int zdialog_choose(cchar *title, GtkWidget *parent, cchar *format, ...); // show message, return choice void poptext_mouse(cchar *text, GdkDevice *, int dx, int dy, float s1, float s2); // show popup text at mouse posn void poptext_window(cchar *text, GtkWindow *, int mx, int my, float s1, float s2); // show popup text at window posn int poptext_killnow(); // kill current popup window int popup_image(cchar *imagefile, GtkWindow *parent, int size); // show image in a small popup window int move_pointer(GtkWidget *, int px, int py); // move the mouse pointer to px, py // file chooser dialogs for one file or multiple files char * zgetfile(cchar *title, cchar *action, cchar *file, cchar *butt = 0); char ** zgetfiles(cchar *title, cchar *action, cchar *file, cchar *butt = 0); // print an image file, choosing printer, paper, orientation, margins, and scale void print_image_file(GtkWidget *parent, cchar *imagefile); // drag and drop functions typedef void drag_drop_func(int x, int y, char *text); // user function, get drag_drop text void drag_drop_connect(GtkWidget *window, drag_drop_func); // connect window to user function // miscellaneous GDK/GTK functions GdkPixbuf * get_thumbnail(char *fpath, int size); // get sized thumbnail for image file GdkCursor * zmakecursor(cchar *iconfile); // make a cursor from an image file GdkPixbuf * gdk_pixbuf_rotate(GdkPixbuf *, float deg, int alfa = 0); // rotate pixbuf through any angle /**************************************************************************/ // parameter management functions int initParmlist(int max); // initz. parameter list int initz_userParms(); // load or initialize user parms int loadParms(cchar * filename); // load parameters from a file int loadParms(); // + user file select dialog int saveParms(cchar * filename); // save parameters to a file int saveParms(); // + user file select dialog int setParm(cchar * parmname, double parmval); // set parameter value double getParm(cchar * parmname); // get parameter value char * getParm(int Nth); // get Nth parameter name (0-based) int listParms(GtkWidget *textWin); // list all parameters in given window int editParms(GtkWidget *textWin = 0, int addp = 0); // parameter editor /************************************************************************** C++ classes ***************************************************************************/ // dynamic string class ================================================== class xstring { static int tcount; // total xstring count static int tmem; // total memory used int wmi; // internal ID int xcc; // actual cc (excl. NULL) int xmem; // memory allocated cc char * xpp; // memory pointer public: xstring(int cc = 0); // default constructor xstring(cchar * ); // string constructor xstring(const xstring &); // copy constructor ~xstring(); // destructor operator cchar * () const { return xpp; } // conversion operator (cchar *) xstring operator= (const xstring &); // operator = xstring operator= (cchar *); // operator = friend xstring operator+ (const xstring &, const xstring &); // operator + friend xstring operator+ (const xstring &, cchar *); // operator + friend xstring operator+ (cchar *, const xstring &); // operator + void insert(int pos, cchar * string, int cc = 0); // insert substring at position (expand) void overlay(int pos, cchar * string, int cc = 0); // overlay substring (possibly expand) static void getStats(int & tcount2, int & tmem2); // get statistics void validate() const; // verify integrity int getcc() const { return xcc; } // return string length }; // vector (array) of xstring ============================================= class Vxstring { int nd; // count xstring * pdata; // xstring[nd] public: Vxstring(int = 0); // constructor ~Vxstring(); // destructor Vxstring(const Vxstring &); // copy constructor Vxstring operator= (const Vxstring &); // operator = xstring & operator[] (int); // operator [] const xstring & operator[] (int) const; // operator [] (const) int search(cchar * string); // find element in unsorted Vxstring int bsearch(cchar * string); // find element in sorted Vxstring int sort(int nkeys, int keys[][3]); // sort by designated subfields int sort(int pos = 0, int cc = 0); // sort by 1 subfield (cc 0 = all) int getCount() const { return nd; } // get current count }; // hash table class ====================================================== class HashTab { static int trys1; // insert trys static int trys2; // find/delete trys int cap; // table capacity int count; // strings contained int cc; // string length char * table; // table[cc][cap] public: HashTab(int cc, int cap); // constructor ~HashTab(); // destructor int Add(cchar * string); // add a new string int Del(cchar * string); // delete a string int Find(cchar * string); // find a string int GetCount() { return count; }; // get string count int GetNext(int & first, char * string); // get first/next string int Dump(); // dump hash table }; // Queue class, FIFO, LIFO or mixed ====================================== class Queue { char wmi[8]; Vxstring * vd; // vector of xstrings mutex_tp qmutex; // for multi-thread access int qcap; // queue capacity int qcount; // curr. queue count int ent1; // first entry pointer int entN; // last entry pointer char * lastent; // last entry retrieved int lcc; // last entry cc private: void lock(); // auto locking and unlocking void unlock(); // (for multi-thread access) public: Queue(int cap); // create queue with capacity ~Queue(); // destroy queue int getCount(); // get current entry count int push(const xstring * entry, double secs); // add new entry with max. wait time xstring * pop1(); // get 1st entry (oldest) xstring * popN(); // get Nth entry (newest) }; /* ======================================================================= Tree class - sparse array indexed by names or numbers - every element of a Tree is a Tree put(): cc is data length to store get(): cc is max. data length to retrieve actual length is returned, = 0 if not found nn is array count for nodes[] arguments */ class Tree { int wmi; // for ID checking char *tname; // tree name int tmem; // tree data memory void *tdata; // tree data[tmem] int nsub; // no. sub-nodes (Trees) Tree **psub; // pointer to sub-nodes public: Tree(cchar * name); // create Tree ~Tree(); // destroy Tree int put(void * data, int cc, char * nodes[], int nn); // put data by node names[] int put(void * data, int cc, int nodes[], int nn); // put data by node numbers[] int get(void * data, int cc, char * nodes[], int nn); // get data by node names[] int get(void * data, int cc, int nodes[], int nn); // get data by node numbers[] void stats(int nnodes[], int ndata[]); // get nodes and data per level void dump(int level = 0); // diagnostic private: Tree * find(char * nodes[], int nn); // find a sub-node by names[] Tree * find(int nodes[], int nn); // find a sub-node by numbers[] Tree * make(char * nodes[], int nn); // find/create a sub-node by names[] Tree * make(int nodes[], int nn); // find/create a sub-node by numbers[] }; ukopp-4.9/ukopp-4.9.spec0000644000175000017500000000165612333076511013517 0ustar micomico# RPM spec file for ukopp Name: ukopp Version: 4.9 Release: 1 Summary: backup with retention of file versions Group: utils Vendor: kornelix Packager: kornelix@posteo.de License: GPL3 Source: %{name}-%{version}.tar.gz URL: http://kornelix.com %description Copy files to backup media. Copies only new and modified files and is therefore quite fast. Specify directories or files to include or exclude at any level. Report disk/backup differences at summary, directory or file level. Optionally retain prior file versions for a specified time or version count. Optionally verify backup files. %prep %setup -q %build make %install make install PREFIX=$RPM_BUILD_ROOT/usr %clean rm -rf $RPM_BUILD_ROOT %files %defattr(-,root,root) /usr/bin/%{name} /usr/share/%{name} /usr/share/doc/%{name} /usr/share/applications/%{name}.desktop /usr/share/man/man1/%{name}.1.gz ukopp-4.9/icons/0000755000175000017500000000000012333076511012300 5ustar micomicoukopp-4.9/icons/usbstick.png0000644000175000017500000000757212333076511014650 0ustar micomico‰PNG  IHDR00Wù‡sBIT|dˆtEXtTitleMade with SodipodiŸô/'tEXtAuthorUnknownŒ! òzTXtDescriptionxœKT(ÈL.)-J_~êô ,ÉIDATxœí˜{TÕÇ?çž{ûö{zºgz†˜DaFÄATT0“à&lv5[ÙÍVʘè&±Ra%š‡ŽÙ"†¸[f“Å$*Y·’MÖ%±JAÍ DˆŠ¬ Ê(ƒ(‰£ƒÛnûQceeø~×uÛ**B>ÛZ»®‡§4…|ÉÍŒæU±è !èUŠu ~¹råÍ/|àÿò/?½2 L,` ””¦/«È)zF=×e¢ípQ,KuæC}‡u6WÊ ¥Ç)}³«+ñë¿Tš™B®?fÌ0Zk~ö†Ín¨¥oÔ#5fY…0–)°-ÁpÁ §`òP¯[&˜;1/®ô§Bf1ÛÜ?päç@ú¾3¸×0ò?ìè¸åÈŸÀ¨©©üh}}5Ùl¥45!8RTŒÊv¤¨É5™¢"STŒ:ŠlIḥÁC°#â¾ÌTžµ&S3¹Þºâòó­­Ó¾í÷ÇößqÇ7ü9d{ûu_;wZC¡P¤P(€vYßmÐXi2”× çé‚"]ÔŒ8šîW·â«õ$†!0%˜R`JÁ ö³KÅ)xš¤‘-Lñ øØœ9W]ßÖÖþûÍ›×~¿ ŸË0¤A&“çŠ:—îÞÃÜöÛ4¯”È5GŠŠŒS^ý·Ÿ}Œgo™Í‘®ç1  ”'Óµ ÃHC  ƒ¢ŠõÖtžïw¨ª Ë%KZÏ‹FÃ;::¼§£ã}ï'€\ºôSmµµ•­áŸÑљыΑüç.X×åðrÊ¡;íÑ“Qôå²i‰ËþžÃ¿[åQìyÝ÷~'b¼ÅXR`I¦¤ßŽ“6üÈ^.˜QoE"áùýý©/\|ñ5Owv>Ñó¾Ì™s¥UWWu}2Y!ŠE‡ÑL­<®™"ÙxÈ$ë % ¦Ä’hùG¶?‰›N1ãó÷Pìï&Óý*É­˜c¦8¾ƒ±ò™Q¢NF´´N§RÃÿ°`ÁÇú6mzìå÷ pÙeK½ŠŠðÍ I §T"α4W4úØÐc¡9–ë–Yv,ÿæsD&M'”œÈuÿAó'oA{~ýà£cå9Rä"qFƒŠ{ßbîì)f.WlŸwñ’s.¿|îúM›6éw `¼ñF|ïðp¦¨”Â糈F‚„Ç2ÔYîió°M†À0†H!Z±÷¿n§Ø¿™Ÿ»›=¿¸¼¡ª:Þ~êWãuaŒY1£wÖÑ9:𽩩©Þ4 ˲Â@km´xžG__š„Y¢µJó¶cã&¶H ¦q\TLA$çÂÿ‹núg"••'EìX ÂÈÙÝ]x%—·÷õ°hQ‹¯ ½´¥eñÞ-[Öï<+€Y¼xÙµÉdåÄH$ˆiš@iÖš`ÀFJ)F0Jy®¨QH­È[A&EM’AIÌ–Dmƒˆ%¨´q«ðD-AØØR „†þ~œ×ö0òüË(×Ekç)º÷â²Ë/´zz†ÚçÍ»æÕ-[Öïù“³g/z¦Tr?ÛÜ\o™¦…Ö ty{ÕZø@”Ò §G922ÊÌ„É$F8蘸–MØ'ˆÙ‚„_2>³œN“Ïá À[]x¯ì Ô½wh­Ê‹¤µÆ°©ŸXC àê«.²ùXsóU«Ÿyæ1çtâä«VýlóâÅs46ÖJ)”Ò(å]Ë÷CCzSCô÷ãy.MSjH`È „DáW>'‡UÈcäsxÊÃóÊSxÊCk¨¬ŒRU§ªº’ÚÚ*„ôöJ px0M6Wr_ÝÙ}áªU_Úu:óäýý¹¿þýï_Þñ™Ï$ëL)ÆsÞSxJP:ŽRšþaví>À´¦ $e–D)ƒç9ªžW6…& RY%¯ ‘¨Š1’ÎpðPûºòüs¯Ï0¥DšSJzS#ʲÌÓ®þi#pûí÷ýÕÔ©õ÷_»´­Fi5îL`ìêi2Ù½½Cô “Éd™ÞT‡ÏgâyB"Ñ0*¢âñ |¶E_ßö÷ÒÓÓGªw×u‘Ç9,M‰4$¦%1¥Á¾·ŠƒNÓ÷¾wãÁ³ŠÀÝwÿÓck.Û¾}ÏçÏž=ÁyU¿R ¿-©IVŽGiçîƒ|dq+ µD+"¸®GÏ¡>Þzk?›6õÒ—Dk”Ó”H)±ý6ž§Éç=òù…b Ã0hjš€ßo£ZÊbþL8-@YV¼ø’ÑOD/M&cæ©(M Éê ”R4L¬¦ªª’·ídÿ}©AàD‡¥”e‡ ŠB±H©ä¯ PW_ICC-S§6räH†µkŸ¦aR@Û¶uF€Ó¦ÐQ­X±&ϵ·Ï;¯"Þ8€‡ëŽEÂS¸®‡ëi’Õqžxüir¹Ÿ"’TUÅ‚Á ‘H„H$‚Öš'žØÊáã¥o}ëÄi[î? pë­«k+*BÏ~´½ur `ãzeÏóp=…çzãE Ùð‡çÐÚsZ#C’@@`šJ•Nø})%¶mãóù°m¿ßO0$ á÷ûyá…×éîúiŠe_Ƚc€Ûo¿Z$bo\²xn½å3ÇŽO§ò½iZ¼¸íUŠŽæª«[èïïA)Çq(•J8Ž3>VJbš&–e±ÿ~¶oßÎøÊW¾‚~w×®¾½JyW¯\ùÅ=ãlî¾ûó{2™Â'7lØÞïºgþ¿Ê-•ðû}H)™5k³gÏ_UÛ¶O0)O8CñŸoì¤öH¥R<þøãlذL&sÊwC¡Ë—ÿë“ï  ±ºMJëá‹.šV[“¬4Óô¥ÒÅt~4[ð&LˆGZZ|P†H¥R8ŽÃĉÇw¦^|ñEy人ºþä7c±_þòwy_V¬x`¢Ï§Ö†œ j“ãx¿õ<õÌ=÷|1ÕÑqÿÕõõ‰µMMñØQˆl6K6›åÁäé§Ÿ¦X,žÕw&L¨£¹y¦;yòìü¤IMÿö¾ü)Ýu××Oš”x ¦ÆŽ@ùä½é¦›Ø·oßgYMMÓ™B0}ú š›g9uuÓŠuuo”Jîϵö9¹÷9Y±:^+W>tßäÉ‘Ï=üð/¬p¸:WW7­Æ6 ÎC¹\î÷ÞûÕ3öÿ'ëøÎwú¡Rb4“ÉÿϪU7íø üøPêC}¨÷¦ÿéú·šIEND®B`‚ukopp-4.9/icons/mount.png0000644000175000017500000000532012333076511014150 0ustar micomico‰PNG  IHDR00Wù‡sRGB®ÎébKGDÿÿÿ ½§“ pHYs × ×B(›xtIMEÛ N Š PIDAThÞí™[l×yÇç²;»³C®xH-u!-ÊRè´‘E¶å¸¶ 4â(°Ûª HíÆ©;v`¸Q à^„¼$öCóØzJ Id;/œƒŽíD ”DŠ"—˽ÎîÌœ“‡-)J‘‘¢HPpÀ3Ã3s¾ÿwùŸïûVÌÌÌð—|Iþ¯mÛ¶lØð']úVÍÍ͉ÿ…½nå6™ÌÌÌØ?ÀÜÜœ˜ÿø­ož5pRk•éÝÊnÞׂçÆ¬µcˆ"ƒ1k"ŒíÎMaŒÁÄkLd’w~§uâ«OýÍ["n”JÄZ—ÿý?ÿõÄàÀàìÝ=QPJáy^,TDÞI„Ž¢ˆ0 ñ}ŸB¡€1†F£AEèr™Á×_§ðóŸS>~œ+VÇ @ž>ýͽJ©ébß÷)—ËH) ‚€0 ‚€ ˆ¢îZk²Ù,J):ív;ìyFç§?½fþv›öü<•J)%BŒ1  ; þP€¼Y Ê›<Ï>2ýå=¥½*|ßgddk-ÖZ„d2\×%—Ë¡”¢Ýn³¾¾N­V£Õj¡µ&—ËE•J…paì5oðÇÆX¿óN2™ …B¡FÖâyZkï¥o½ô`¬Ì[Ы} [,öÿÝè®1íû>ÕjuËaÒjµ¨ÕjÔj5ÖÖÖ¨T*Ôëu|ßGJ™ÆA»ÝF_½ÊijÏnäâW¾BãÐ!„xžwÍ2B0yÛd¾¯à=[AÜÈ òÏÔ+¯ÌÍç C9'G«Õ¢¿¿?Õf´ÛmªÕ*•J…µµ5¬µ¸®K¡PÀqœÔR6Öxiv–ü¯n¹.Õ#Gºó(B)µ ÆÇ'„±æøƒ>8pËèY¨'&oûûC·Oe}ßÇ“úg­Vcee…V«•]__߆€îÜZ‹77G_ï'î §ëVVVRícp‡Û'owÜñ‰¿åÜ2䇕/8Ù̉¡¡a±¾¾Ž1†0 Y\\¤ÓéP,q'ež^êe¢L&ƒÖ𝽆H)Åú=÷ðû_L…O‚!BZ­G§ïÊæ]÷k1[Þ’ I@=óì33“ûfšÍ&Apᆆ†ð<÷?`ljŠÒ‘#OŸF,-aŒAJ‰ëºôõõ¥4:zæ Në´K%.|÷»Ô¦§7XKR ß÷‰¢ˆf³I©TÂqr»?ÿùw$Á¼Ù r“û@îÜùìÉ™Z­†1†……vìØ‘úgßÙ³Èzµ²Bÿo°óé§áÍ7Y]]Mz}}{îÞ÷¿°Ýó9p]~{òäu.Ö{ŸÏç©T*DQD«ÕâøÃgG¾~#+ÈÍìsòägveÌ×-°¼¼L\ºt‰¢(êRâ“Obŵo9¿ø»fgyï½÷¨V«©íšE@00ÀÙW_eõñÇ·Œk-RÊtÔëuÖÖÖ˜ššQ=¾{÷îüV”ºÙúî»?}êÎOM«„þ”R|}íÉ'¹zú4QÌÝÎÂ3óóÈ+W(—ËðÃ’]XHÿ¿têSSS×1Ôf7Bàû~JÓJi¼=søðáÏnE©ºGûP®WxjïÞ}ryy9eŸb±H­VK¹ZÁÊ¿ˆ­×ùÞ÷ºÏ¬eä;ß!ÿÎ;´÷ícøõ×SíGžÇÚ£^ô½Á/„HOãF£Áž={BP¯×¹ÿþ¿É~øá/_þssj!{´¯žzæ©¿îïóFr¹+++éccct:¢8ƒLþVy“Ëmðɾ·ßfø7Rá­”\|þyÚƒƒ×Ñlïèu±±±”Z­ãããXkÿêèÑ£c±Ì©döõþ‰‰¯Þyx:S.—SO¬PÅb1=|ü±1.½ôv3ĹQ”˱ð \yì±-“¾^áwìØ‘æLƒƒƒ) dͱcÇÔž‰ƒYn¶€sâäääÓzÔëõÔ•q]—òOpùߨ²2Y:uŠÅ/|aK—INîR©ÄÞ½{Ó$1Ñ|âNIJrï½÷J­õ—{ØHÌÍ͉:ñ؉ããû&k-µZ-ýÀæÑl6ãàêÖ¥R óíoãŸ?Oîg?» ›¥õÉO¦Ù©R )%Zk2™ ù|žõõuÖÖÖ‚`ÚÍÃZËèè(ÃÃÃÞC=tÿ¹sç~ Àª$q;~üá×>}ßý{Þzë-¤”i†˜ ÕC{Æ|ß§R©P­V J%vüèGˆ0ÄJIýÅÉ~éK¸išÑéth4¬¬¬°´´D£ÑHOm­u "™k­ùàƒx÷ÝwÙµkžç©………}óóógÒa*‹‹E¦§§)‹„aˆ"¥Ó^³&l”ÜwŽcáÕWqßyÿî»ñ﹆P¯§k ®ë¦¾¿Yë›­µÿ~úûûB°oß>Úíö! t«cnÍvÚ~þüùóÜwß}R)FGGY]-óãs?! C 8€D €\'Ä…ßüŠz£Æü{ó\½ºÌžÝ{¸mÿ~þí_þNСÏë£àp]—|>ã8är9œ\Žþþ~\·€ëæqr9œlÇqPJc¬¡ÙhRo4(—Ë,-]¡\©†aЦ—VM÷~ó51>ްV9è–Ò ê ˆ‚ë’Ï»È8M°¶Ûz&çä65>ØݲÒv¿f-Öö_qÃÆ§ºÎè®U¶<®Û"½J££H)Elˆ´¨ïµÆ²¶VÆX‹Ö”ÖH©8vì¾ï“u²XcY-¯R­Vãt9ƒN\EJ”ThÕ Bg›™l6¶`°q»%¶×@\—´ ÁÈÈN–/™Xø´+a€N«Ùº²¼|µTÛÅðÐ^¡@±ˆðÀ µFH‘ „-ß§ÓnÓéDa³³ÀÛ]«5Ri¬éV\N.‡›wqó.ž×G«ÕÂX[« ä†ú7Ÿ§^«ý6ñÿ$ˆC yñâÅêtÚÿúÑG¿/ ññqI f2™´ˆIFÂX½‡bÒ±Ãp=ÿ±®Ýò•¥Ëýîwÿ Tc¹­ˆ­\`¼›UA7l2i-²Ù¬ÐZ ¥”Èf³"N=lÒ~i·Û¦ÓéØ0 oErk»ùòË/gÏœ9óÐÚ€I:2¡ÓŒþýÌÿ‹+Ñ‚Xû&±À ì¦ù†133cÓÞhO‘ðç¢õê핊í߉·lذ àÿ5€?zÌ?ÉýŸâIEND®B`‚ukopp-4.9/icons/run.png0000644000175000017500000001026312333076511013614 0ustar micomico‰PNG  IHDR00Wù‡sBIT|dˆjIDAThÝYyp×þÞëî¹ÍèFB'q¶§¸]Ž/ˆ“² `DZw½k'åxw‹Ýr’òVÊE6kWùÀ6fmg½&Ž1&Bœá0‡AB tޤ¹gzfzúxûÇ:¬²üµ¯êU÷t¿~ïû~ï÷~×ÜæVW·Ñžl!Å ÄÀ4&‚ @5réàŸßîõÈížpÙÊ>h4?µL˜ž©JŸ«g >·¡áMÏí\¿“£dNIUµVR5‰z|€yN+,è …Þ©ŽÜÎõn;n¡9ËF/]í/(‚ž£°Ù|Èë­Æm&@oçd¦Í´YíH$ŽR$ ¬ö,!¬۽ÜMí@ÝêÍÿ•œšøî_þò¾w´q Ød%q8Mf#UG)IRa³ÛA8Ìg)R·jÓOØFV5xçÀxØÆÜõë×sK×lzwÒäò—/[°Há„Öe«žX1Úx>¦=d±Û$YÖÀ ŽRp…,«°Ù¬ÐTeaíšÍΑ¾]¾|CѲ5OÍ/*ü÷%+WBoÒÿOÝêM›Ç#ÀñŽäÏß=þ¼‡×­¿Ï0yr9-¯¬0¶\n[WT2+7;k^}OO“VW·5§lòìË«jÞ¥÷èÌ9sNo¢¾€˜$@€Ê²|ýx<~¾|RÍÚ’²Ùñò’/º\ ÚâU[ãy~ßœ»î,9·FÏ L6Epww®*,žÖájûúܨ G{±ôž-oÌœY½á{ë0ôûÃPUg–LÓðÉ'Ž5_lé!fƒîÁEu „üÂ"!U%ÐÓD0@@ PcCŽÓ ½ÀÁÝíÆ¥óçÄ`Àcq›Ý–¿xù “Fx´wy¡ª » ÅùVìûÓÞx0yäðWï|6ÎÑΪn]´h>TMƒª2øƒQ˜Œ:<þØZccÓ¥I11\6¿v®‰&pþr®¸ú *@žãRx|xý" :¹Ù6Üsÿw̽î>³i••´»?¯?€ã("¢“9……B8Üö€ ŒªBsN\wu|·vÁ¥ „€£É{Æ1*ÁãÀjµ °0´ux!F%PBA)G(ÊŠ³ôyYãÉÓýœJ]»Ö$Ý×µ³mÅÓ*™F¦Ï˜>‰‹Æå¤4)c@$*!“¡i ݽÄâr0—A)ÉJŸš"D Fð¢`Œ%I¦Ææ:­°[tøóÞ½1U–ï­?°£m4œcZ!Ó={èÐq_ww²¬F’ÔiJ((% „ vNfM)Ïqà¹ëç¯Ãž¥ éçt`¬ÕlÀÄìÿ*ª&´_7xïÄXÇ$ÐÐðzŠ´îÃ]{bOaÐó„&¯Œ1ôzBˆÇep)@ƒ§AòiiÀ)'7x¬A'`JEN76)>Ÿ÷›úE¿ ߸ààW;yý½ÛÝ‹IŸÜ…xB|þÎ7w¡«70DºzžƒÕd„Ù¨ÏH7 ˜´;™gé+OÑÙ~ƒiŠrÛ¶iãá7­[±ñžÂ‰…øÇø‘Ùë¡jZæCB®ºúÐæê!¥•¥¹(›˜ƒ\§±˜Žç ðÜ}!\o÷ÀÝŸ´4 $3GÒÒ&¡äf[Á{vï‰E*Ëó7JPð>t¯9‘P3‡-I iß«Êò I âq‹î®‚×ë×_|%^¸pIÇ‹ªL3ZŒ&¥æÎ9ܲºZý”I8}ö:䄚œ?å'Òæ6Œ¡ª"ÕS«…Ë-—àgcâëå’•g:ö¢)“J F¥!‡˜àʵ^ttûA)Åò…ÕøêËCñ×~·£çÌ™ ?!2É­ß÷vÖ¡/ßÑ#¡©GŸxå—/oo\¿¡-[P ƒ^Hžƒa»0ϽN@MÍl=€MضmLŒc…¨¬ªyvõšK‹‹ h\R2%èê ÂÕáAD”°äîÉøÃ÷E›¾®‹Žþ i°Ýn¿v.èj;ÓP2i懗š¯<”›“m›1­œº{ƒ)Ó:`nKŠœÈ˱B§Ó£¹¥UËk骘X5§súäž«WO©Ã1fÎ@MÍVÁìÐ&ÊTŽ£Õ„ óT%±ö/þ=Ì$IÉ|Axý".\êÄô) ØîßrÆañݽgÏžo-2¸-\µi‚I§;ÿâÏšíjÀë$­ ‚â"&9‹ÉèêêaÇO6F{Ü=j04q”ï!geE>Ž„ö^CÃ{nuk¶\ÑëøÊ²²‰Ñ²’bRP˜gÈË˦9Ù˜Í&x|hÀÐÑå!€ÃnÆùæN¬©›Ž{ù·ápØW{ðËÇŸnËVoÙZ»ð®WïYUgvµ{P<Á —«Îl òl „€i >¿ˆ„¬B¯ãÁC „ÇãÉ“'˜»ßóFýo=ÅcjÅ+¿þgBÍŠ¢BQ5¨Š†xBE4Ie×Û=]~ä:-ðú‚£¢ïfÁ§ÑÝ_oÿÁº{á5`6é0cZQf{¥¸ŒöNÂáxÆb …NÏ£¢¬ V‹|´çÓ uˆž§‚À#ŽÄÉeªÂÐï £µ­c°YŒ);Î#‹>ŸÜù›¼Ô%‹Å Å ©ZÆ¿ø}".·º¥Œãã) K*Âá z0ø•+·ÚH„}(ÇÕë}` ˜=£ñ¸Œ«×ú’Aœ¬BJÈà9 ¤LžŽç‹©´È­H •Hª¢¢¸È Àèë £»; P’1­É°<¹:åæzàÀ›AY–õ^oŠ¢Â À‚.wýžHƼE£T• Ä-”B–5dÙ¬¡·„~Û6ªjš£°À‰l‡”„‚1ôºƒCb&³YG– ùy69P^šƒ‚|;Á KN•qd;ßß™P4Á”““Cs³°Z U züÿz3tzÃ+k¸Ç&@)A„aˆzš:´´^Å®ßïG¹ß*L¹@TÚ¬â-G÷}èOÏÅ@}ýŽnØ‘RÝÆ¬þ>ϹîžÞ›Õ‚HDJécÚý'wÁåêÇý÷ÖšÎ|ýüÒÕ›NÚÿÎc_¾fËË“«ÊkfL­ ÁP šÆ ÅÁqÉ Fz§-®»c /Ôùök£Í7zBã:/¯šcÑéô «§Tò¢˜H©2V2AQ zKß©;Ýt~mqé,cAnå‰ÎÎKÊàË7UN½ûýy53æÇ™bq`@(‡–en‚ÀÁn7a÷§{%U‘^tµµ9f4º|ùæRj䛟öi£Ç+"‘P†X†ô™ „ ¬,6»ÿýÙø‘cU)TUº@@M°dÕÆ* ûì©ÍO˜ ã Å•!%“2 ýV\?øš®õp” ¯ÐƒAÈäJB…œÐ’cR–ˆi ‹ r¹o._Y1qÂ}oº\ #æÇ£˜¾~½Î©X¬{è¾ü’âbÆ2 PJa2 )‰Q2P¼JnnÓõ YR!†%:p¥zƒ02k(ІòÒ"ÒÑÓcö¯¯]iª ç¨*4At¾:cFuÉÜ;fÒp 6D]²²LÈͳ"/ß‡Ó ƒŽRë©d’V/MѠʤ˜ %®"“pî›KÌ`â`¶ê3ë3A KØøÈÃ&MÓ~1š*h…jîßjJÄO¯^¶Ь‚€€§Ç!Ëi‚ÇëÅ˯¾­ò¼Nݰþa]õä ¨Š)® ‘P ™Ì-mÓ5O‡Þ @c*ö7S?Û·?—$¿Ýnw<³ù ã„ü<„ƒ4Uƒ ãÑæriÔ«RÄoZ…zZ›ä’ÊÙ7.µ´®YxW`Ð xv§‡ŸRÞýèã¨Ï×óB àÝwùªëŽ¿6~Íõz!?ÏI²³­0™õЖqN„XlXìzt»{±¿á°üÖí–¿in>uùò©'Ï6îÛ®7;/ž¹py¹^§ç¦U—S½A@¯§¿Ûñ¾èó÷/>~ð£kH*Ú6š#£ø¥«6>Ÿãt¾ôüÓOš@ÞØùA¬£«û\Kóéçº\罩ä§ÝQ· ¿ t=¡º”@÷Òßý”sÚ²‹Ê™ íNvíù4q¼±‘FEÿÛ]W>ïjÿ¦€@ OwNUûzEié´Kv~üG±¯ß½öôÑOŽ¥Æ)ÃIŒI€°`Ù£Oô†×8Ž&B¡à¶“G>þ( |¤^»äûÛä{Ëßy'Ãegž/ýê7bã©úç®·¹˜ý­n0h‹k×ÿ˜òüÏ‚áþegOü©iÐû‰¤ÚhžXK÷ã?Ø5÷®û[¢1°åÂQwê›´È NÐPÐÓØÙÙµ«½Û‘M†Þ^ŸßÐÝy¹IÕÕF`<®9¸k;€ß¤3dlÔ·ÛX¡„’wæÔÞF ñd´.Fý®]Ç 8ާðx}à(ç•$1†¤ô‡÷Á;¡»¦ïo‰”€6¨«#Ü+P!ÙÓ£ÙÝïÕŠL¬Ãqîþ~hj¢€8àá]v?j‘÷fþfMKˆKƒÄP½ç½£‘€G¤Ñh½ 4p<t¥*ƒï_^Ùn,ÎÏ—4€IR]n«¬Ê'1t‡«Ñ¸ ÿV#‘Ühº{ü‹¥¨c–Çëˆ Y¡,D5%xæÄg­)€éCyK`ÿ_¶ÿ’Ò 0'ÜIEND®B`‚ukopp-4.9/icons/root.png0000644000175000017500000001243412333076511013775 0ustar micomico‰PNG  IHDR==baéízTXtRaw profile type exifxÚ—[r$» Dÿk^A$¸ðáxù>,µÆ£‰kûvI*6Ÿ‰ žý¯žç|¤™>jÍk¯5ñÑ®= O_Ÿþþ•÷-_]e}Æägÿó= ùçÀ÷[âg¿~‘ñÇFßÙÿØ(Üh‹~}oñÙ¨ÐûûFßÚ½%ýÏ€ï¯÷šß–T¤h Þ÷o.AÑ–âÏÛuQÕ)íâÿ,¾ËY¤Ëïý/ÆrñkþóYÒòòW<¥ü³ÿáå?ýÏïO}w—÷Tù­ïÿüÉþÿ#ÿ5ë7.s|µº D$ûÜ9ËÏÙï ­ÄZýDÑ·ñ_1q€ãa˜¢ïShµì}*”N´}ž“OOS$É<òиŸ"U¦4Þ*!öö9ãηÅsÞ§HË9kβ³2Ó·¿ gË3H³•\©Dï`š·3·%X˽ öMƒpˆàÄû{CÛÈoÆ ©än".„åÏ€½I˜m5‚Y¯&E^ãã·T[\bÚªÃO•=lú]™s³ úl\úFüé/‡ÄxJ¶¬™qcÛ~š(De o›2ŽÀM0B ”ÇòhJƯü´•íÜ„Lc–c‰/$Õ¦á²ಠ¶4wí-˜Þ¤‚`ꚥŒx¸ÿ\±&*ÛSÚlj©Õu­ÁEÅN³ÞAmH5Ž_zrl& Rq·5ìôåGšÃ³øßÝ)Ë5€¯Vz? _{å ¶àjhŠ É5¬Ý­r®ë¨e“ÍSóŠ “ß¾é'§â¼4»†uh ³²ØLÕ&è arvzV™¼´OP)<º`QÌÕQä•Ëîqnò%Ä¢®v]"¾f@2Úåg%&Ào…Ø›;ÁtŽ¡C ì‰7Xà@ᘺðëBYÙ‡kÀ2Gx XÍ”ÄÆxk×nT€ßÔ6“rÔÖÂú¦ÎL@£nøÓ*•cÕg4EØˆŠªíD/ý€h c¤=âQøÐÎE¦þ!Èv=£| >Ðd9ùA_0&—Tñ â$ëB†ê‰ÖCameáÍ‚Wæ¬5wÌ×ÌŠo¸³õüœüªX‘šM-$Þ3‡^ù¹wd5l®›.…ð@ý"$)Þ‘3vaè.>Æc×!ÝsES&ì:["³ÜVI—ºúh‰¸%–yP؉*d&¾YeèÕMÒ‹{öi{£ïr°sú첫DAÁPãÆÝ+_O#Gû d’)S•¨?W…‘Ó“&ÿBÌ ÖÅéIÝr:¤."J§÷Û¹cqëïiýž¶ž6(*ü¤UgÙìv}\X‚àyÜ@ðåic ”–Jæ6[ì>úXW@(¹\Ž£ÉÈ{6¼’ 2E—¸îj‰”­3Ä)Xç«Ut®Qc“ ð|P¿Ê¢{¯¦’ðãª&HZ+Ⱥž ð“D­qp¸­ôçߥgslkú·ysBIT|dˆ ÚIDAThí›Kp]EzÇ_÷9÷)K²–„ŸøýÆ/ðŒyÛÀ˜L(RY¤R³MUf“MìSÙ$‹¬²ÉÖ³I¥REÕ3’`ä LÛØÆ/ÉÒÕ½:Ïþ²¸WÒ½WW²±%ONw«Ï9߯ÿ_wŸÓç §Çéqzœæ!)󇸷<êê;dø–v>g”Ü—!x”><2hU ÿL'Cìvçåe³D#®Pd€8Ƈü‚»"¸Gá‹÷(n¢Šð+ºùØ{ÛEýoéàžUÉæ‚áä¿9“ß`ºIxÓª ‹  íÏ#æ yB¶§£òçn g‡yãÏ<1c’ö¬úee‰~æ¬ÝG‰3| TÚŸHT1Œ2àÊ :u[èÍxÒµB¤o/Ü,Ú‰—^7Ï:'/2A¿žÄ.´O ?{ž¢À5vºå mÓ<{¶ˆdû—Çöï`ïjá*¹äÇHìfVP\h—ZOba¹ûBN¸@VÐÛ&vý›HTAâ ’FØgÿiá#³ÍÝuG˜à }a‡ÝÂ*½˜6 [\({1’•C¯"…~HHCHCLßô¹]HI3éw2ˆÚ-$ «ö‚Aëûx–¹›ò’ûž• tˆÝðsÄÅ5àÒ!Æ{öh_Fô36¸Ûî%²,Õ!ü…òm!•nÓ1v¹/eŒø²wÉ÷Va].¬“i[޼ðäÄKÏÊ+:ÎN"-”c ­CøL°BÿWŽëº\ÖôˆÝò*B\…MÃtÕÄØ'_ÃÄÑ«,׊&õ—/”Ú £ôíŒótzQž“¢õäÐq$ßU…ÔM' )W•ÖªâRìÁƒˆ/=+Gw2B»êü?5Î;´á3Â÷¡¼É]úÜÁ¥Øµ‡@C\\&<‰ÒÐE&®ÜÆi!`¶¼‚nl‡‹,MJò">+87ÿjÏÿÒP¢ƒ2{ÝUöÊ2keûQð U`M9ýö·CHQi+0~ìiº·1Î!~óÔsèÍ7î39¨bø¸5Ÿ.ΫÒ5•W»Oø¹::ܾ•Øî§ pt‚ô›K¸8Ɖbnãü-7J@ˆ¸ lÿ´ØŽ\ '©È êX¥_’™O?ç Z¡B'w8˜^bOxÆ®1µ±ßCÆÊÈD€ft uŽ8ˆ@ªƒ5˜ý‡‘‚÷-/Ë(û¥s>Çöü)}Š,ר”~ÄÛ¤uû*LÇ&¤6y‘V°#·q9ÒBLuw`K#à&@„ ìòí¸cKá¼t¥§åO¹ÁzÞ?µçZ!¦‹aö»16éV_ì®W£@Ue4˜gÂ/“rH K<êDW ó2fà t‰¤7xŠ„g´H×|©=?JŸ&GÊúôGŽkHg6cÚWV!&)*G¤¹ vlS®´g±¹xªÍ¤Ù¶>tû2´H1ù„?‘16pšÜ|¸ûÐЪBº)³/ý‚ݺÄ7¶ÿ@u‰"¬ƒ‰°]‚ŠBUá3—5ØL­í”%캽à{¸ ¶ª²‡nÕ‡÷ùá•þ5y|».ã)“—­ë1]=Õp%¨YaÚCš iJ’Ï‚$ØlP×9ÁÔÑ.ÊÃŽ'a”||‰$lä×äÖ凂VÅ¡—késü+{µ;cìþCˆ—V×:5ÄdBœ§˜òiÎGŧ˜L@£Òµá@ oõV44ÂïØå†9D=«öÃ)}Šj7D—9ªJVW¯Âä L+V ` °¥2GÇHÑ!“Q!Auéš4L!Ý«p1¹ô'(±‰Sþ Ðz‹OŸû1=¢gxJLÖxƒû!—N¯¹SÎש]HHò>q1‡ËX¤0Átû©óª ¶„·kF=“„lt1ñé{˜m¥WzE~d[zŠ7#§ñ ¦¯ˆ4ŒËIˆé²çE˜(Æ„©gðrQuÔ+ÜÔi¦'@w¬„d£ïy•€Í<ñàj?´¾GÂ@ð¬»Âj–,oÙª¸zµ¦Êb²©;V&ñÁv'°¦vž™,àá=¿ iˉ±%¹Ëº­ô`J')ÛmÉWÃáq`¦Òj\ÖHˆm«ÎÞv¼LñÒM<©ÑÌöS!`Šwaà ÄI.s”ÈnÚ ôä6Pr1=*_²NÛ:ÄÛº ñ¢ê25¥p xîà_cÑíäÇÆÉ߯¿rÒñiUM&®~é²¶ú†VX‡,>DæÀ[ˆYŒ~ÇÚ¤”Á>Ø&âOEÜæ©tˆA<ñdp%¦3›ñë>¹š¥UÓP¼d )&à)Ù¶I†Á,â“ 09"˜ˆ!¾ Ñ×Há&z0àG7ôÚÌGäÜu`dÁ uŸ’¿4©Ä‡õOè²™u‘ÊY ˜|ÞÔœ7¹º:ZÁà‘æ<ÌX © >À8$ç«šàÆ ½[ë(˜üÐ#þæ¢2èGkã.÷²ßËW:ĸì!^hFh'Žw„ç´mÆÊ ˜Îk`q eÐrU`…©ÏqRwÜ¥²A²¤ Ý#0ã V«§Š)Ž`zÁµ;^x—#f‰yߎ¸k?å;Ø}iÂ'ï/G8Â7,×¾vü5e$£Õ®³5óꬹLuT–fmÏ.Î`nÖ:Ç›Ãê®-ùïÍ<¸,îV&?ºɱŒ3÷/à}A«"”è`,Þžå¨XÏÊë]˜ÎRkØVÀé†L¿#Ûéð—*,2÷n€WLï0æp/ölxÁ´ÂN*tÞ/ôýõίðµá0/Ê9úY¿àbÓjÈÞ˨˯†®¿ «SO7°ªVߘÚ"¯USÁÛѾ—ÇŸˆWò’]Îz’Qy‹è^8÷TZa)’šáMŽˆóÝÚ†)ŽN«ÜJÕÙ”2à­o7xkjõ†Æ0¶-ÊMfúo#{»1£˜ð"/§‘ÙE?÷³Ñpïð~— !OÝqÎÒǺ^ü-#Hó8žÍÁz`¿fY P;ú³tN³™:³ ™ ü,&׆œ£7ºæŽ#¬¸Ÿm¥9¡U-Ò¥˜ÝÑWñ¯gÄî.bzî6:zŸÀêCùŸ«ßÁ÷³˜ëÿ´ˆð¦E3ÌŒŒzØfh©Mïu̾~LY$¸ÀR³›<‹ï¥öÜcú491¬©œu'äs:Y߇¿åÇéœt@æÈOšå3nüc;W}Jnx„ßú üí(Ù¥éôR×ÊL]~r)ÌDø¯9âïºÈ|p§£ÜéÞ,îôÎq:&fÚUiUŒÆô¤wx&ùO?'ög혞RËp›5_SN Œ½[@bi8ÕXJ¿Éϼfs§6OŠ“ùðW"Ξæ@z7Ùté;³³ÍÞgȉڵ• ¼Æ0¶® ³öb£sÒtlÎ×™F‚»k‘–Þ°­áT qŸª‹Èn#}j :L¡ü9o°–×gßDl ­ï`éo§õ?xF ‹Ä?‘Gr•û[¢ZHNñ{Ó™ý!JfyÒÊs…y {™ì›«1©ý”=ñ-2AÏlj·Vúy vCå<Ç(È+ñ—ý¾æ}]»{-õŽ)tœ¨à·;¬P3%»4eÑKSï%¤4¾«LæµE~ª.Â_~ݳJdÊ_ñ3R»­7g@ëI,†Þh؈¯°Ý¶÷Jvo¶< Ò Õ8å\½ó d7Ætÿr”âÁ r›#Ú^š û—£x]Á›ág댚Iz™Ü¡¥à{’þžíÁ•ô92ô¶ÚVš9{?A€Ÿêëæ²-ؿ܂×ý)Òê i29æ~ «+fWÇdVÇUÇm5ˆ¹w7‡üŒrŠ¿æѦ•ÈGß傜{Ã~DGz (×»Û ´*eq|§õ›¼ÎÉ­®ž3—’ÍŠÔ[³‚ H\{)‹kÀI ›ë³E^&»§]R”d˜UÑÿ¤/h…®æ-ãÆð>…á¶ß›~"ûDó9ïØFlûd6ÈV7ž ¼è~@[Õ·‚¯å‡·íþÒ²×2ÙäŠl.N5r6†÷- \IJȴe°>$iµÕ½Â·Å:ëÒÓª<ÛüÐ\w¯™ÝƘUð…;lzt<-Ê@£'н8:Ò&•«Þ(‘¯/ù˜MèÚËÕŽ)0™Ê«Ô±ÝÏÌ^ÿFU;ªêôœÑòiL@ÄÈT½ÖV…©²óQ7šü—J:/Ö€ J]Ýý<)¿á–¿Q?ÖÏÂ#îó‹½Ay¥pciG7¢©ƒœ©‚ÏGR…°îÏ“[C•dºª3Q­MÖÌŒl.ƒù~ WùBí·%l1‘v7$>7yž´¡ù †(Pñ¶¹éÛßþ(ýÚö'‘z„3oX—œ_ÝȬ?6'Ó´“%)h‹oõçÚ`º¤3Ûà[²aÔïIïdº¢Í~ýžäŒh|Ÿ ­ïÑNÎÛDèöÅwd{4î£ØÉñ$AuíkÒGL q|Š$”ñ(’дoåÚˆêr¤S¯õ-§çe6Ðê§ÎÅeŒ÷ûÒÏÔñ‰ ð%{)5﵌QU„3ä)Ñ…õ»-¨›ž%Hdò‡N:yS$E,JZ;6_Û«sÒ¡ä¼¹7ót®Øjú»Å'>ÃFØOÐj³pΩŠá†^ —›ÚŽ×.¶yN—fO_ý„¶÷{¥„ò<îQýkÄãô8ý?Kÿ$ Ó¯mÖIEND®B`‚ukopp-4.9/icons/quit.png0000644000175000017500000001137512333076511013777 0ustar micomico‰PNG  IHDR00Wù‡æzTXtRaw profile type exifxÚ–a’ì(„ÿû{ ¡ã‚ˆ½Á?»ªzºß¼˜y¦£ì R*3ñµÿ÷ßsý‡+·Ü¯Ú¬««&®êÕeðÐÓëòç7?÷üê*ï!þÿÑ}²üøÜóøÙ_ß›äùËBŸé¿,$ïî xÎõõ¿÷B…Þï }Ô»¥ú×@߯{¬Ï‚%•\jÜï_)$AO湕~qËOzúÅîø?x0Röü½ÿ‰ñyi|Í¿Þ/°XyùNI~ö^þê¿~ |.}:ó3¿õýø×¯èÿøOZß°”ñzºèþ10÷ÏuNôsö3¡ŽªpMß,ú$ÿZˆ‰“8î$©Ô§ž_Oíi ¤ šÚ»4ižVÎéd¡å‹‡û*YóÊÆ½æåï«3Þù/hçi%›ˆT‘¼¥2Ó·_¡\õõµ¯=;ovîi3©²XÍûÏÚõ§×ιÙJzý­µò[îºÍ]öû—iÀž×»fò!áo.2¹H©¿§P–×Ë¥äWEn–Ê~cæ÷ëÅîúé^ÀîÆ8‘æ/|SÏ }Ô*M”C&.ë§=È_4x1ûïÞå½";ç/¾š}‘ô[t×3‘2!f²Êµæ;2"‚‹€n1éyÞ¸)z[ÉzDôBµÜvö©—>1Ž,eE[gÖ–Ø®§nI—çÖ1­§ím³&¯9ŠÏºT—õ½&3çD2ÎhêÓwÊjF™K]E{‰~ ^‹z–»—4V·\gͰžÄZ/²×°¦9Åé}~¶§Ò{(hŒ1uϾ–œÜ.’TñRTµM‹QŽÓ–#Nw]2f‹5XÑû)é ð‹N`9uhìj×$¾FÈeÎ24UÖkÔ‡Pœ4MKõþøòöqƪN)O JÕrÝ®HÄn°v™¤KÂÍu‡÷TÍɹïbK<@µ•é¸ÀËØYS÷\„íåôªùÂN&)-¼!/«3Ž™AaЊ%»eÏÍ6#íݦ®‘k‰N)½Ç6¿¤‘ð±NvÄ€6lKY}ÌЊh?£Jo*¡k ä[|öZ§Ca8)ë#*A’L?¢YÁ×ú¤Ì3`©jÔ¡wÙ>­ªgóÞ™‘[Óˆ™Úµ)gÛóVSÜrœ¤5£ꑭÒMx1†µTƒŒQ·úÐr„j-d~5‡„\bs¢iËØÃ'êæï˜a rrØ)y‰(h¤ÆV˜) ®|iõ¶[3KþšlcGkH#PÉ^ ²šª {/ ÀÙ\Õˆs^V¦Õª 18:L‡°s•R±4ZÚ¥G›€#Q¶vïaÁ«uª¥0š²ø,ÊÓõ(ãØZa¦®ieé ±Y‡–µX}çF¾ÃUI‡ï+I{:ÌÔ—zÜuOý¼å9 `H0Û‰-«¡_0°ßjâÐ2ܳcƒÈW¯ÍÉÏêTÛ&Q` Ĩ5Š1—fQôPÔ>±$,¥Ô} †ìÊÕ`Ÿ‡Î¿Ö–p0i‡"¬ò ‰Zô “Û¸Ñ"{žm˜â¡HdÏ\fÇÍ&u%<8?§û¨ šâÑâÔÅ¡{fFä~F&¬2¢ ôwŠN4-·±^7ràÝØýBbÝR>=ë6Wè1Š3M0–»$j,’¸Öð:±O";ËH¬• ? VÛŽ{ËÚøi÷"»l6ÅS‡4D©¬Áµng^|k–†ž[몛ï@;À,UÛâ¬[2a„Ô´YÆSY>C®½¶ŸãÕ;cà;-ÔŠ]%ìG7~nÒíæ†ÆG‚Ofß”Öü·³ÕÛµqú‹#qðå1¨öZ¶‘ÖÊ9Sõ6ŽãwjÙñNÜ¢c{êÂéà“‰ à ^={~ý•õŠã çesBIT|dˆ ÂIDAThµÚ{ÞUyðÏïò¾ï¾»ÙKî $‚XÁ€õ±ZFíh*¥3ÕÞ ^Zu:ÔÑ¿¦ÒªÃ8Që8ÓÎX¦‚ñBEÄH#—Ê5„$rÛû{ùýNÿ8ïî¾»ÙM"Ð3sæÝÙ÷œ÷|¿Ïynç9'ñÒZŽåx%.Â؈U ý¨tƵ#xc~‰‡°í y‘óa.Ç[2Πٜ"²Z„€Q¼€g±û(†)x w`~-’ü%°oÁRÞ´œ%¯&ùü>Öb‰(úTQ¢‰ ŒávãAüp¸à.|ÿÝáü²Èñ\sÙ:/Æ›±®¸Ö\M¨¥T’ØKQšfÙùìÅÓønÇN޶ùnÕ섪u2ðç [Ééï ¹XT“:ª)½5ê½ÔzÉk$¤ñ×Ë@QR¶)Z´´&hN0ÙŠ¤x®CâVÂ>ž ü³¸#Ã/…À\SáÊÍô¾g¡'¡¯Ê’!Së#Í£¾—(Ë<„øyL/#™Ö“#ŒF2“xÿ‰_1Þäßñ9ÑtæmÙqÀŸ//â}ï¢öþŽÔû{X³šµ§³x9•^’ìXðÇ#R’*y•EIÑfQ]Z•ÊÓü^“Wˆêtðw!°_â²?!ûC f¬]ÉYX¶Œ¼Ò=¨™>Mj.9ã¥ä=T{;äZ¬ >Å™“œ-Úüá“!°ÿ4Àå[ÉÞ€%uÎ[ÏúS©V;‹„™Þ­Óÿ/ Ed0zjÞ\2Sß%)Õ:yN«Éª’A’ÇYßàTÑÞÇŽG Ÿéáƒï¦òF¬äü3Y±Ø´QÎ’bÁ& ò*Gè9Hý½‡¨¡w˜Ú(•I’²³+É»ƒ,§ÚC«ÅÒ65ÒÇØØŽ6{—.ïÔM Á¥|îM,z{Âé‹yí™ öͨËôB%¡M6Lí9êÏR;Hu˜|‚¼AÖ$oÆ¿«“ÔÇX4Ì¢Qj“›R$sˆ IâníHb”lç~+öc¬Á¿¬gã{qæ I}ží.HRÛMmùIAæQÈ9-AVÐÓ`ѽ“´YÇnÂŒ#Hª•‰‚Ôsº+†» ¤øP?}7Ù¦^^¿‘¡¾ÙÀË@˜¤º7öl\ÜšÙ’@µÅÀ8yÁx…–Ù“PÉ Mª%°²3“{¦œŽk_Ū·ç¼a=§ ÍV›PbŒêNòC:îãåiI ¯A½ÉhN#íXˆÆ¦Ôšì ²}¬Â÷q¤/]ÚÃÆ‹pî2N[2øQ*O‘wy”—¹÷O²á‹ÇwO…%5.DOŒ§—Š!È®9³ÞUgËz«³Ýd˜ ²“t–C½.9í4V®Š‚Fã$İl™dÓ&6nad¤£/TJz[­Òè2nÈRÒÙᘟmKqN«ÏÅ«–1Ô1Ú¢ÓËÙn’ÑÙÒröÙÒo”ýèG²ÿXvÓM\v™e K9MÙ²Eöío˶m“Ý|³ì{ß“|àBµ:=®¯Åi#ÑØ»w!MYZ¡9áÕxE†­u.¿¼*}ûº¸UÝÁ(ÙO¾ u§­\)ÿêWeïydéRÉÀ€dýzéß(ìÝ+<úht%Ý-M¥_,ûÊW¤\ ©×%µšdùrÉ…*ï½WعszxOA3áèTš2ì&ZÜÔÚü6ÅëÉÏ` gޝŸ Ýßqkfz²e‹tË–(’níX»V~à ’­[•y>3'M%—\"¿ñFéÙgGÝóV¬^z©2M§ç¬ž §=S–pJÎ`<í½.Å9Ë6ÅßíN ’ƒ˜œ£IºuÔjó«øêÕ*×_/ݺUÈs!ˤ—\"ÿÒ—$6>NJ„~!If­ÕS°¼ÑñHf‚Ü`N8'Çê•9+ûæøûOÛÖL A{ßn•VSRëYDõúë5«UáèQ•뮓žqƼc!4Zïæª–5y¦F+™q«µ”¡Áê+kôVf$_ŒÅ 5Ÿ»oß}‡äîÔóº·Jæ¨Ñ,_ü"í¶dÙ²…Á…‰Ÿßä­·¨#-ê%}mÆóᦠý) ýiB6T.ªìÚ*£Ñˆ»uZ?wísô?fbÇóJmšÄÐÐ Áýt›áø¸t×þy×J­c3ØZ Áµê•ͺ³Ë0±pÀÑ&¿ûa‡>õã;î<ÖãœD EaäŽmŽ|æj•_?M{áõz§Òò©Ôµæòl¶ú”EÌ=æ“ÈÔA%mP½ûa/ü݇lÿéqwâXðm÷ßâЧ®V»o—¤½ðZ%*&³0vêÚa†Ùô Ô§»§Mª¿xÄþO|ØÈ=?9)¡,ùáÍ^øû«Ô81ø)5š:M[‹Íf»…އ õ¬Eýþ§Œßþ#¡8‰[QjÿïCzŸ8 )NniÁv‚kby&P¤áÃí™]˜Þ‰ôä~¼Ìs½¼Õò¿ù˜´R=!þ¤R±ô¯¯Ò÷WRöôœÔ-f„[F¢ÃÎðgËr+/Œ‡ƒéƒöp´ƒãe*=W\aàÚke«WŸXúS$êu• /TŽk>ð€²Ý>î:à Ïfܬ#ý»Kö³3ÖJâ¼w Ñ×åJÃÆ¾~啯½V¶jÕIƒï&Q½è"e£¡yß}ÂqH<Ÿr ÑáÀO£ü<ÚfðÖ7÷ËN©ÌøÙ² Y|ï•WúÂd+W.2tS2_ú€¤§Guófåø¸ÆŽBQ³VgòXõ"°§ä® Ýæ)î+Ü>:³Ee ¨QTæ×É|ófƒŸÿ¼lÅŠÁ—­¦=7Û¾ÛoSÅ‚ãÒÁAýŸü¤üüóç]k,áHø2ðD`2º~‘â·%ül„#í./”ÓÕÓTýzê‚ ŠfÓ®ÿø–§¯þ[O|ôÞ½m›²½°‡ÊW¬Pݼùصp ‰•í)ðcßDGs?~“Чû[™ÐüõØlZ Päs¤’$,]:V‰¢Ñðä7¿f÷§?méîý?¶Ë“W}Ôî[þkA­²Ø‹Û·`8íÌ¿m´ôøMGí:µ«´ûçH¥,~èAe³9/øÇ¿ùu{®¹Æâ}/HY`ñÎ=žøäÇíºå;ó’˜x~Ÿ¶ÿrÖ:m<—ÎÖý‰Àö¨>á6fªÃXô|Ë–³j²uSgbQ•Bì\åÐÞ½’ó^©ýúél´56æ±o|ÍžÏ~ÖòýefƧ¨±{û=ªÕÏ8SšçB&öï÷Èu_ÐÞö=yQNÏ9šðTc@èøþ‡?£ÙŽ÷?@èÖƒ5¸iSÝ…×­bE>ÃÜ8Ù’Žðš Ï­]kéWXqÁkµ''ìýá4¾»Íª£Ãòy%‚ynÉbÕ·½Í’M›´ÇǸã'꿺×òFsºÖ?‰Ç2ŽvIÿPà[Ø‹¼ïÃ^fß$xOÆ¿¾È’,‰·-Ý5¡üPgE%|!MŒUª’²4ÔnYŽ_¯×™~£I"Ã`ui`gÊ¡®Óa#p+îâ`É_Šú8v½§O6½v(•m¬vÒl´óhКñ)úCEaqYªw@/¢†Î˜ºxíÓ/^Ké¬1‘ðtÆá.ð­ÀvÜUçËâ­Í¼Å]/lgü¦á+2éi]'¡"±aªl:“NúD½uþ™4ãTbÙÆ}¸bœ›ðYs®œæÛñ1ì˜Î}¨aýP*9­SÚ2P¤´j±¢Š®÷‹èEGêϧ<—E?%¬¦x-s+åH4ØOˆ7µ³ÚB*{¿6<аéº<–¦·+´ª‘P÷%ÅÉJ|"á`Á'3¾~*X݉ÒwÈÇÎù€Ïæâ®F0ôPÓ9{Ú*«3º ¯eB+§Q¥Y™©õÏêh§Ñs§1ÿ=Ôé£ÉŒ›,CLé÷„ˆx;c þ ŸÂ3 <™kÖAüEÂU«3k/«KÞTcq'v¦/>º?;>¼0;×êîEàp`GàÂ!v†èë¿é·÷'òzDÏv/î †lZsKm<ЛÄ+×é‚ÀTð3;¢.¼x>ðËÀ­û92ÆwD}ß&†„ã¶ßõ©A?.ÆS^¿45tv.9/g]ÆbñÆ> fɦ¤Ü Œ—1(=x4°3(r¨ŒjÿuñíÄÜ:øËF`ª à|ñ±Ç›3Ö÷%ú'Ò%ªYŸÎS•Å88RÆ·Gƒbœá’';€¿+zÌ“þR Lµ+ñ*ñ¹ÍkÄç6+0Äï…h«#Ø'>·¹WŒO〗ðÜæÿ]S†®ø¼,7IEND®B`‚ukopp-4.9/icons/media-play.png0000644000175000017500000000641712333076511015040 0ustar micomico‰PNG  IHDR00Wù‡ ÖIDATxÚÕZ pUÕþÎ[’—å…$$$µˆ ©Ek™ºT«RAj«ÇÁ—A± ¶hÕŽ‘Ä–ÔjÕŽEP¬u-ŠZE FQFÂbÉB²¼äå-§ß¹Ë»÷-I#`ž™?÷ä.çþßù¿9ç>c×Ò)'Q&QÆRr)IƵJ+e?eåJϱx©8ÊçGS.£\L9•’jWÑ÷Q¶QþFyžÒø¿ð]Ê"ÊE”díLr†@ÞD‰¬1@F@J–„Ã¥¿#”èëº thþ ðw™`ü”—)SþõuGyr)ÅA%â³H˜08š>œàÑípÿ‡¥D€¤4 ¡Ž­{H¨w½[ ãm”(wRökÊ ”åP\OL&sÆÏ ¦É2ÉåÀI£½¢¤0CNÈKÃèLðz\ÒíÔ†”ÝýA4vö‹=ͽ²¦¡ Ÿ|Ù-a"ó Ôn–ØI&õ´¨º)K(« PG ÀKyŠ2“”˜2GâäY€ÛƒÂìqþ¤\yzq¼çã}T¿»?$ÞßÛ!_ßÕ*Úû¤øh½@õZIÊ©[6P怎€rRÅÏ)È,~Hëæ#'= ³NÉÇ©c2MÖÄŒ:Ȱ&…bNm;Љõÿn¡«Ö½À›digƒº\ ÝÏuò¡(å7Q&¢hªÀ¹wJx¼âûÅYò’’<$»|XÅl¥0b_ŸÝa÷ÙÌ1ü0^ªiÿ¤UÐÏIßô @ýu/=çb0Š6ô.LÆx:è9‹„+9YÎ,ÉSŠ2¤þz›B‘¾†+-;4@F®kwHQ]ß%7Ô4#è§o¼ñ°Dí»êÂNÊY‰è”€rØu”Ÿà¸©4àRú©³'çaìÈTý ó);UN…y2ž:Q§"ÑI—ým>¬ÛÙLÿ¦_¼¼¨Û¡®¾H™ÇNôÚ›(åÈ*øéJ)R½¸àÄ\Qœ›ª›[¯VÓ,ìÖL‹¡h“À꜔Q×÷¶úÄ«Ÿ¶Jéã¤ÿõæz5ÆBʪ¡ŒÓÌåp¥ã竘˜ÆË)…(-ÊÐõFÂ|R4uú¥“Á)7#9:Úˆ˜È#m}}¶óÒv]›½ÏóÛI§j†\4× üå&z4ZÛòD,€ç53ö àŒ¹ÈNsã¼r †Â–âú\=yšÚûñÇMÈÎò %Ù‰![,u¤­¡‘á)L?oC»ŠNï1’ð4 z_–€*¶bD¾À5U, RÅ™ã²äH‚ÐR«€nÝ&1ÿ”´€E¡DT2¬ðÈ”|\C ‹¢²ëH©eŸ´âK B%´úB¼‰ÿïauZqºãIÀ\ÜX |«T›i•OŽ¥iÓ±èT>9O^3fÄQ[z4/ÖwË_±”®ëöGQˆ  †Ï·K<¦xÊ¢Ðõ¬R'¦+ètÀ£8‘s…=y”,;þèTwú±øãñý¦ÒTº_m¨D¦Îïú@¢â¦…”7,ÁUË€Ó~¬QH…U";Dÿ?•ôãc%¹¸–Ž´µøƒ¸gwžþ¢ ]Áh!eÂ< J=) }ðwàÏ÷ªÇ—›at .`½ä:κCJ ––œ6 Y Äê“råµEÞ¯l?g²¢®KÞ_Û!:é¸f¼·Ó†Êj}–z?¤ØP)ñê5Ì<+‘àŒ0òÓ—Óü¯ªÍ'–6öÈ–V­{¢JÈ{Mé°¡tÈ8¯œü™‡^{*®œÖ4#rßÜ:‹!4 öF‡ÜØRMßZ,*X¹N"3K³HV©öDg…Z{½d_‚FYÀ¶ÊŠ®u¬iX£+ ‚¼~¸ƒŽ;[ ~ï°¶U³6wKÏdµQŨäÑ,‘íq2ÏÙ\‚ZIhí&ˆ$. u‚œñö~†\e~›%edû;jaoîªfm¯O¿¸§\ Õ#].‡(ð¸´m«Ü0"Õp,¶­yÍ™©yöõ²Ù×/pßB‰·_¾âöºÙ¬ß;K`9 ¨¬,‘D*—âBŠË˜Ä¿ -VÈÔŽ}Tº®?¨7ë m–°°üpËà°ƒÐ?1aÞx€‹žI%Z™=št*LviI:žF¶•›éÄ‘º'¦Tæ±%u#i£Êfìªs±r V=yTŸ˜ìtÒ?ò¹“Ên–(£Ÿ{ÓUdEINYÈ%¨—Ž.T½¤zÂÚV(bb¿$练R4p[ÚÂëîaݱJ êQ‰€ú°l>ò™-ú3ëè"`Á­À¬ËÒÓ´Ä–îvŠ\Ò+‹ÔòÒ7RÔÒÔØÁf0‘}JGÒ¥ƒÊ¶’.=Îýž^õÏI<¾‚ó\ÿµ|fµ7•'‚ŠP Tn{Ìè—\ L-˜ô¼a‹N€­,ëÑEv;¶/½ÀØÂ ¦µøš?tǶøŸŒÌ8ušDI ˬñùR ¿*+ûú$6±\¬¨©‘ضh;ôüÔ ¶ýßþØ#QûF~nóe¾þ`w!‡3IEND®B`‚ukopp-4.9/icons/stop.png0000644000175000017500000001011712333076511013773 0ustar micomico‰PNG  IHDR00Wù‡UzTXtRaw profile type exifxÚVk’ô& üÏ)rÐ q¢*7ÈñÓxf¾ìnRILíá)µZ-—üã÷S~ÃÕŒ¬ˆv·aVqÉA¯¯k<÷ö<Û«‹ßCøÿ­¿|}ø<[|ï—÷!mþØè3@þc#z[pÀ{“×ÿï½_7ú¼Øð^å¯Ï×s¯Ï†\¹±pàyïÄp=„we¸v»žÞ ôkÿŒaAm£}íl|ůùå½–òÈ?áTé{ÿ/Àù¯þòmàsÙÓÙžö¥ï_À/?Ñÿ/ð·¾`Iñz+èþ9 _;gû9ùL×ìÍ¢ó¯0qÂŽë„Âyãýõ¦O3@º@Óþn§N´QWkõ4Bk/÷âfmµŽ§´húô9Æÿ6Úy·NDBÔ’ó}ù2¥ÈõunNNœì51I°™´ü­ü߉ÿÔιl…{þÎ5~‚M7.ðæ†ýÞ1M^ÔzbÆ¿þûËÃË #½Þ=žHÜ\ÒgyÀÿz½˜-OÀm@@ ™„qëÚÔF2‰ÙÛAÊAk&ö±Ï»>íËU¾¼ ö¿'ékEkoÛFì ÊÏ«=nË ?WkHLE4›À6œÞ‘¤J°1îäpz_¤Ö³å džɫO±0)'ÖVb¥ðuΦðjš1,x]è¢ÆÖè¢+Ò%ë‡ìå3vJî®ÚÕ0òÃm»ö”ÙÖ©<;£ÏβÇAEŸ óD‘4Q§ªŒaÝ£.m8§O=#k·”ξ8÷›áao4y‹/OX½îvÁ&kï3gÂqY£íU ¹Ç™Kgδ³ïÕ–œîxëkwµ3]C1”Tõ¢Ö¼ÕqYX[FÎŽ9°ƒÔŽ4wYnÕ~êh3ë>7]&Û­³ :FÛHat&àäie•î^Ç^ÒûÖã±ÎÍȽ¬oÄ‚©ï†MÈFWqÏ%c¶±hϵ&6*±úê0‚%†ÛÔzÜe°“*üK6sa?7PP†Ìeàc„®£57D0 (œ8°Î·”¼T Q't¥/­kÿå00Áý˜ \ÈìgË@u½÷›"gtÓBOÂzm¾Äî3OSFÕ9 8º+‚ï–¬»É4{ÐVxSÐ šŒ ½Ù-6â=ŽìÞ¦Sr.†sGµKÞšµëc…F“‰îzfžô ‘,¤Ÿ>÷Ä4îk Ô¿‘Æ ½Bip©â8ÜúE°Ð:rGbmƒÕ@“ ûU›¶dcã[!¯,×¼Ø|;PÒA´ÇÚ¥ƒ¤s¨õëÈC ¸Ár”n¯j Ú0Hàx˜ëL©»çšcùJ•tIŒÖ ºútWBiñs-äŸÅ ÀJX‚à`zT×°!«²˜×31\¥âÌ­÷³Id" Zg_ º&"R"8Û{FÎ9QŸ þ`G$% b+áÜ ó¡[6sœ¹Eù?T‡•÷~ãŸÛC 9 àÓ†Ýâë‡ è.´zy»TB¢3;l’º¤_uºÐÞØ \!©û1<ñ»U#Àä£ÍU5ˆd üH.ˆÂP&]siÈK.I®Þò˜?kC«’Dá$äE5½ï M"³LæêdÇo÷sS~vÌV(dW¤p ¨]lºqZ‡0$rôâ½}‘Ì­—Ým­êdtnB9#ØkÂ|Ë $W Pè!ócPýª<ll¨]7jš6tBiô~ö Æ#dêºrù'a ²– [~Ù 4ðIB×ÕRߦ 8„aK›"{¿klPŸ¨¨ÑìRTx&Xésè®ÁW ¡3Þ㉼ïtÅ‘;&œ SQÐòâ 0ð@qy{ƒGÀ¡Í²è•†3ñ¾ù<íYšå‘wÚÚ-^ø¡ìÙMê F]ÝEc_ s@:ž®©zKñs!&½¬±vÔ÷Qþú8µÝ5úsBIT|dˆ ¥IDAThÕškpTåÆç²×ÛæN. ƒA vG­E¥SdZ±Ei• ÖŠS[Á©mMë´FÇ ~ùP¼àt4­µÚVëXƒ$¹ZÌ’H¹î&»ÙÍî9{Îé‡%K’ÝÜ´@ú|ÚyÏ{yž÷}Þÿyßó_)¢¦¦Æ^RRòxB\Ü#†2Oµ},&Yqõ÷?wìØ±Ê•+Wú§Ôv*•÷ìÙc+++Û”,É[Níx%ÁëlšÓ1_4‡Ùë×z\!¥²®®î…Õ«WN¶í¤œ'¿1I”·6VmOh{篡ÐWc<š„,“sû­ÌÝücO¬:?ú7-¯ï%ÐÕ5VL)K±àW?Ç–‘NÈïG ±Í̤èuèŠBËî·0ͰcNLA@ž‡ ŠhþAtUE äp×É‹2ÿ±Ÿ`MKEé÷ + ÖôT Ö}ÁöN½ºCב¬Vì¹3‘,tUÅ0 Q$./‡¤âùÄæshËS¨ýžñˆ&³îZ…-3ƒ¾#ÇùOÕ6w?³ïþyß^IîªoÑVó7=þ’ÍŠ%5…’ß>5#ÆwÒùñ§ ëøN·"Y,äß½kZ*îCG9öÛgQû<>ðrï¼YkVÑþÁ‡:º"ã+}ýúyHV é×-'÷ÎÛȼñz2>øˆ¶š÷Æ ÇÙ‰/*ÀÐ4Zv¿Iç¿>@õ`Ÿ™…h6#ÇÅÑÿÅ l33Ã^6 ü­gé;r<ÒW|a)K£+ 'w¾Jo]'_ÚEZE)3 óI^X̹Ž#m´`Þºƒ 4·Ð][‡=;‹ôo”“Rz5gÿòþˆý Ž`hZ˜ `NJDÂ/k_Ëê6læ³õám<Ãxш/*À’ê ÐÕCßác‘r_Ë<'œH ŽEWظ£¡z"bÌÉI ޤ% ä÷Ó[ÿ9ë¾KÁ=k0%&`è:ªÇ‹ÚïAŸd¸µçå Ê2¾¶³{]‘r-ÄÛ¾OÇåÏB”cnEd» {nöy1^6ûÃB†¦Ó²ûMR®)Áqu WþâaRË®áËm;è?~bÄòM‹#Åݼ21 Ïu‚a„WyX0Íf‹®Â–™Žh1“Z¶„Ô²%èjˆ¾CÇ¢Æ)Ýwº•†Ÿ=ɼ‡7uÓudݼ‚„yEÿÝsœûûGÚ$DáØ  bƈǚßaˆ&Â0[X’“XX¹ÃÐD Ñ«=ûêèøð“¨ab¯a0ÐtŠÃÿšÞºƒm¸—¸Yy,øåc(ýzöÕO,`8t#ªÈˆñ ÀÐu—;lSÃ@õÐ[×@ó®7tvîf ç¡zhy­¯³‰O>Fâ¼¹þðúŽ|AÈ矴Z €d·EmTÉjAtE±¢A—›ƒlÅw¦ m0€êÀд˜ÃDmbA–ÂËz>úºNÏþƒœ¨z‘ÏOòÂbâò²Ç'?D¦×†%Å4ü +X3ÒÃû£¯Ä1ÆÐ4]Ý ¶w0x®3ü| òQIdνw³dû˜µfÕ…Y3 <_:Qúú‘ì¶p8›|§[ÑUÛÌLÌŽäH¹d6“0·ðB¯qˆ¹XÓÓȺyÙ·Ý„)~Fä‘-; SBçáu6ìîÅ–‘FrIq¤ÖŒtNžÂuèèW&16qïþνÿOrîø&óÞ@þÚÕHV+æ¤D‚.7M;_AéóÄê+ zP¡åµ½¤U”’\²€ÒÏ£+ –GøPøú^‚Ñ'̯% äóqü÷$äó“yÓõ˜““0BîÃÇ8ùÒŸèøÇÇ#–ÜÐ4Û;À0ù¢¿Œ»åøÓÏSôÀ:ìÙYH3Î.Îì­¡õíw#ï-ÀßÚŽêõNiOîŽÎЛŸº>Ù7âh1——‹-3-dàÔéHTÑ$aMKE%—›?ú£² ŠX3Ó±çÌD”M:»ðiCWÕH¡ ¡ë»{'‘~í2æWýF“‘$5¾hŽÔ½¯~D8Óƒ ^gå Mc°£sü:º‹ícÖÑAügÏÛÏY&¾hH’*º½žgg¯_ëɽý–È%d:cèR?{ýZÛëyVºá†>KHK æ,/+U{ݯ³9êÄ7] È29+oaî£z\!µòèÑ£UÒîÝ»Õòòò†¤ŒŒ`îµËJUWŸÅëlšv""YœGôô(ÁÊúúúV­Z5(TWW‡***YYÁœåËJC.·Å3VbÈ6s7oô¸Bje}}}$ù6Z]]*//opdeN+;¶Íè$àˆ¯ÓC"¦‹bÙftò/*?0]ì4žm†#f†ærÛi"Û Ç˜9²Ëe§ÉØf8ÆÍR^j;MÖ6Ã1ažøRÙi*¶ŽIeê/¶¦j›m§2Ð…T”yKËËÿgöBMM½¸¸x‹#1ñ§Óáï6ÿgx—ŒkBšçIEND®B`‚ukopp-4.9/icons/ukopp.png0000644000175000017500000000757212333076511014157 0ustar micomico‰PNG  IHDR00Wù‡sBIT|dˆtEXtTitleMade with SodipodiŸô/'tEXtAuthorUnknownŒ! òzTXtDescriptionxœKT(ÈL.)-J_~êô ,ÉIDATxœí˜{TÕÇ?çž{ûö{zºgz†˜DaFÄATT0“à&lv5[ÙÍVʘè&±Ra%š‡ŽÙ"†¸[f“Å$*Y·’MÖ%±JAÍ DˆŠ¬ Ê(ƒ(‰£ƒÛnûQceeø~×uÛ**B>ÛZ»®‡§4…|ÉÍŒæU±è !èUŠu ~¹råÍ/|àÿò/?½2 L,` ””¦/«È)zF=×e¢ípQ,KuæC}‡u6WÊ ¥Ç)}³«+ñë¿Tš™B®?fÌ0Zk~ö†Ín¨¥oÔ#5fY…0–)°-ÁpÁ §`òP¯[&˜;1/®ô§Bf1ÛÜ?päç@ú¾3¸×0ò?ìè¸åÈŸÀ¨©©üh}}5Ùl¥45!8RTŒÊv¤¨É5™¢"STŒ:ŠlIḥÁC°#â¾ÌTžµ&S3¹Þºâòó­­Ó¾í÷ÇößqÇ7ü9d{ûu_;wZC¡P¤P(€vYßmÐXi2”× çé‚"]ÔŒ8šîW·â«õ$†!0%˜R`JÁ ö³KÅ)xš¤‘-Lñ øØœ9W]ßÖÖþûÍ›×~¿ ŸË0¤A&“çŠ:—îÞÃÜöÛ4¯”È5GŠŠŒS^ý·Ÿ}Œgo™Í‘®ç1  ”'Óµ ÃHC  ƒ¢ŠõÖtžïw¨ª Ë%KZÏ‹FÃ;::¼§£ã}ï'€\ºôSmµµ•­áŸÑљыΑüç.X×åðrÊ¡;íÑ“Qôå²i‰ËþžÃ¿[åQìyÝ÷~'b¼ÅXR`I¦¤ßŽ“6üÈ^.˜QoE"áùýý©/\|ñ5Owv>Ñó¾Ì™s¥UWWu}2Y!ŠE‡ÑL­<®™"ÙxÈ$ë % ¦Ä’hùG¶?‰›N1ãó÷Pìï&Óý*É­˜c¦8¾ƒ±ò™Q¢NF´´N§RÃÿ°`ÁÇú6mzìå÷ pÙeK½ŠŠðÍ I §T"α4W4úØÐc¡9–ë–Yv,ÿæsD&M'”œÈuÿAó'oA{~ýà£cå9Rä"qFƒŠ{ßbîì)f.WlŸwñ’s.¿|îúM›6éw `¼ñF|ïðp¦¨”Â糈F‚„Ç2ÔYîió°M†À0†H!Z±÷¿n§Ø¿™Ÿ»›=¿¸¼¡ª:Þ~êWãuaŒY1£wÖÑ9:𽩩©Þ4 ˲Â@km´xžG__š„Y¢µJó¶cã&¶H ¦q\TLA$çÂÿ‹núg"••'EìX ÂÈÙÝ]x%—·÷õ°hQ‹¯ ½´¥eñÞ-[Öï<+€Y¼xÙµÉdåÄH$ˆiš@iÖš`ÀFJ)F0Jy®¨QH­È[A&EM’AIÌ–Dmƒˆ%¨´q«ðD-AØØR „†þ~œ×ö0òüË(×Ekç)º÷â²Ë/´zz†ÚçÍ»æÕ-[Öïù“³g/z¦Tr?ÛÜ\o™¦…Ö ty{ÕZø@”Ò §G922ÊÌ„É$F8蘸–MØ'ˆÙ‚„_2>³œN“Ïá À[]x¯ì Ô½wh­Ê‹¤µÆ°©ŸXC àê«.²ùXsóU«Ÿyæ1çtâä«VýlóâÅs46ÖJ)”Ò(å]Ë÷CCzSCô÷ãy.MSjH`È „DáW>'‡UÈcäsxÊÃóÊSxÊCk¨¬ŒRU§ªº’ÚÚ*„ôöJ px0M6Wr_ÝÙ}áªU_Úu:óäýý¹¿þýï_Þñ™Ï$ëL)ÆsÞSxJP:ŽRšþaví>À´¦ $e–D)ƒç9ªžW6…& RY%¯ ‘¨Š1’ÎpðPûºòüs¯Ï0¥DšSJzS#ʲÌÓ®þi#pûí÷ýÕÔ©õ÷_»´­Fi5îL`ìêi2Ù½½Cô “Éd™ÞT‡ÏgâyB"Ñ0*¢âñ |¶E_ßö÷ÒÓÓGªw×u‘Ç9,M‰4$¦%1¥Á¾·ŠƒNÓ÷¾wãÁ³ŠÀÝwÿÓck.Û¾}ÏçÏž=ÁyU¿R ¿-©IVŽGiçîƒ|dq+ µD+"¸®GÏ¡>Þzk?›6õÒ—Dk”Ó”H)±ý6ž§Éç=òù…b Ã0hjš€ßo£ZÊbþL8-@YV¼ø’ÑOD/M&cæ©(M Éê ”R4L¬¦ªª’·ídÿ}©AàD‡¥”e‡ ŠB±H©ä¯ PW_ICC-S§6räH†µkŸ¦aR@Û¶uF€Ó¦ÐQ­X±&ϵ·Ï;¯"Þ8€‡ëŽEÂS¸®‡ëi’Õqžxüir¹Ÿ"’TUÅ‚Á ‘H„H$‚Öš'žØÊáã¥o}ëÄi[î? pë­«k+*BÏ~´½ur `ãzeÏóp=…çzãE Ùð‡çÐÚsZ#C’@@`šJ•Nø})%¶mãóù°m¿ßO0$ á÷ûyá…×éîúiŠe_Ƚc€Ûo¿Z$bo\²xn½å3ÇŽO§ò½iZ¼¸íUŠŽæª«[èïïA)Çq(•J8Ž3>VJbš&–e±ÿ~¶oßÎøÊW¾‚~w×®¾½JyW¯\ùÅ=ãlî¾ûó{2™Â'7lØÞïºgþ¿Ê-•ðû}H)™5k³gÏ_UÛ¶O0)O8CñŸoì¤öH¥R<þøãlذL&sÊwC¡Ë—ÿë“ï  ±ºMJëá‹.šV[“¬4Óô¥ÒÅt~4[ð&LˆGZZ|P†H¥R8ŽÃĉÇw¦^|ñEy人ºþä7c±_þòwy_V¬x`¢Ï§Ö†œ j“ãx¿õ<õÌ=÷|1ÕÑqÿÕõõ‰µMMñØQˆl6K6›åÁäé§Ÿ¦X,žÕw&L¨£¹y¦;yòìü¤IMÿö¾ü)Ýu××Oš”x ¦ÆŽ@ùä½é¦›Ø·oßgYMMÓ™B0}ú š›g9uuÓŠuuo”Jîϵö9¹÷9Y±:^+W>tßäÉ‘Ï=üð/¬p¸:WW7­Æ6 ÎC¹\î÷ÞûÕ3öÿ'ëøÎwú¡Rb4“ÉÿϪU7íø üøPêC}¨÷¦ÿéú·šIEND®B`‚ukopp-4.9/icons/clear.png0000644000175000017500000000532212333076511014076 0ustar micomico‰PNG  IHDR00Wù‡sBIT|dˆ ‰IDAThí˜ilåÇÿï1³;»ëÛ±c{m'v GI(DT”DŽBJ P¨R¡¶@©Ô¦m*(ýå*>”R¤ÒªÐ¦)Q•ƒp%M£8ÄŽãøˆÛñµ§wwfÞ«Ö9Hâ+î‡ö‘Íìhæyþ¿™çyþo3c SynJÍ”ýú.|ž[Ÿ+c®RRQŸröÇê=üøËp'ƒŸk‘g²çï¶×2nÿâòå+횆‹hÐ ÃË¥»·¿~7mß_¸wL$=×BdzçWÛ–V>v˽ë‚R)ó+Ogw¼ùB–‰8–]½2(…\9ÑX³ðÜü+PÑÚëV=î:ø±Ú³ãõv7[ÒÝÕF³£1@Kc&\³ ðÔ}(°lºáگݡj{?ÜâiåÞ®FŒ% îïµìŽ‰ÆœU€°<ïü¥ÁH¤n²Bú–RȲ`àåhm½¡èhkò„ 'sÖž» Ž1æ¡‹–^éx‰#Þ(æÍk”„ZÊË*®½pñ"Çórh=Ô¦}£_œhÜY…˜mÝSmd Þè0ŒVX´p¾s^c=ŒV ÌÆ¦}ž6dãO7¸];k„еó^v“ý^F+£a´EN¦ÑÒÞ£}¯›LÜYxúû"b…*‹ "pã0J@ŸÀ‚EhúäpN)úÂã›bG'{VãwF£µ–7:á¦óo €ð †Ž ›ž¡d"°lìYІÞ[Q^lyé“壥>nt…Æ­ÏlÈL6ö9xâ¶‚óÁí"‡I¸‰£N”³Âhje…†'6Æ>œJüs@(n¬(‰PwtÂË0j9èû¦/®ã¾o™jüØ»µ¸8È0N©•&”ëlVÅ,¥•Mã¸xB9²>Á¿ºtÎW¸î™7‡ÓSÍ9ãËéCï—©_¼®Žò¤ŸD6uÉáOMw‹ùp» TÆÁÎVŒ¦<òø“{ŸN¾/!mL)ea¸ÉÝàvEàlªnF:ÖŠÁ’0š6Âê^÷ÄÀsÓÍ7c_ছnú¥Öê‡Õsõ…eçÑÆª6\¼8'â`àÈ”U׃ÐB€ÎA.›@_çöœ›Ú%<²ú‹·Ç»ÿ«+V¬àÁ`0·víZ>00€C­-ho?×ÍbÙŠ% qÙ²ÀøÞ(<#Rv9ú:¶ëøÀá$\³èÊÕ£ƒSÉÍfàíõ# v7ÕÝ×ÜÒК ­Ãe—]ªª:ôôlûÈÃ;[5”ôPÀ"R\ÏóÌ`÷ž´4øêUw¥M5÷´¿@ËÎ’±ž+©ø²ÝÖÝÀ?ü¨ZSH¤ êëëQ]]‡p8Œ¡¡!í9Œx<†®-BcÙÖc¹ËoOíšNþ)zèƒò—íàÜ[«Ö„8gÈ&öÃË ‚ÚZZز5†æÎDkÖ"—Ë¡£ý b#鿽ñnátÄOÀÐÖeëC‘·T/¸ß!z"× €R­\¤FzÆ10˜Ážý{> áÈÑêê磪ªïíØ!]Ïs¶mÛ&§0¥¸óKeO:‘yw×4® }0)0n@ÃhÆøà6¥–Eã| 1C]ÕúlìÝ×BùS[¶lÙ2ñÀæ–%wq«ôÁšÆo‡ˆîL~ýe´„ÖÚh-`´@.·)8•¨‹2”»¨ŸÇÐÙÒ”­(Ìmzë­éÊŸä–²u[áåŒ_¬]ø1#€Í‹7^.còÂPÒƒp3 Ìä+Á€Æ¼ê–.i€©{§/m[K£†YïFÖ„9 GZÐ*dýÌ ñF äÒ1°ÏíC Fû¢NA£š\3kÀÖÝ<§æ†H0T¨^€U‚°zdS½0F‚q £óå#¥‹\&+ÀÇ€$„0FB‹8N)„2³`%æ¼)Y²¸¤b97ªàõ t.r£½ð2°œÐ‰ú7F ›ŽÃ²9Ô‰ò1ÒûYP€Í6ÿ¶:tÎZß+ù.·Šn¨¬]0ª„ÕBø>|onj?¸eƒs²þI9|ÏF `Á˜<”ô]h-a´8—QK84YpNZw2çéèÂï…² ¬ ñ¡½PÒE.Ù £2°ÆH-†R•` òo\úPR@+ @Œ!&'õ´WÃghÞQx#aΫµç=âÜHǺހV.´”PþQ0Ë!ùæÕo€—·S'šY Z œœ¯ C I70é=ð„î,ù–eþ¹nÑ#!;X ­îC/¹ÙL,…ÈŒBÀqÆêZ€Z¾„ôºÁ­|C+áA+Úœ2ÙÀ†’Ò*N&Fgààö¢ùF›—çÖß²s!ü$ºZžÍf’m|nÝ*KdÛ¡T”s`ìí^ ßð3£ f0ùIM)JJPZ§hy>!+‚YP‚Ô5cZˈq]ì!ßè?òJ:ø'ºš•ËeB•u7S-¤ß ;„1 !h” 9rÂë*h“ŸÈ”0Fà ]ÊÆËàgc „t°_PLr}6n -¾*¾Y{Ù•½íë“Rdv9¡ú¬ã,¦ÒmƒÑ̲A` @x޶¾€S¨£ DA+%|-Àí ¼Fç÷íÔªDntظžjoŸr<îò봳œ±‰­Hìxo§¿Xi²$^4FÀP° ´– V#†z÷i­´†Q`V*¿&’>Œ–0€9çCÉ4ŒI˜]ƒØPv8avž&ø¸óÓü3AÎ`<ð³lnûûÞÇzÿÑ}¬ûïµ(¼ „UÁÍdÐ×¾••_H•ׯ ´ò¡¤­ìХȤzÃaT „Ø`¼‰Á>¾s—Ø3ŽÐÏòqµŽw‘Ž=Àð‡Ëu®þ~ìú®Î¯uø›NÓĺå8Ü´Ñ#,ìY¹ èÉHJ@k jU‚°2ô¶5nG6 “ì…~Ïï7‹¡±\Ç랎ãdÿ”· §ÓŽÀ¬ßä~`q{EÁ‘ù‰þ=sâCµ—Ùsë®`–ínÅ¡U Òw¡”D ²Ýû}σšSåZ4#X¸}m^[ûàoÞÜêïEþŸ®ÉúŸÍî}jäu7 O¼q2±Â²D+¢ì`˜;E—;x ¯µ¢B ³y),ŠÒŠr‹ X‚¶½ïÊ¿¼íþ¨¹CgŸšõXN=ŽŸ೺œâdƒW«ôs‹íðš[ùUó¢ìúP¬`,ÄCe:)µ†uèD,ÙVU×xAMã¥,*DZ®Ý¢µi÷ŸVý ó“³?ÌØDÆ\Š“ÃÚq€qëôþ[­Ê‹òó Â4Ê) %Ó¦£¶ß´8Ç*ß´Õ»ò¥×üÓÄŸéüŒÂ'púý§Ž §6 9åžãfÆ9w=Îù©×&,h&쬣> ¡O»ö¿kÿ~·“" A®âIEND®B`‚ukopp-4.9/icons/unmount.png0000644000175000017500000000541112333076511014514 0ustar micomico‰PNG  IHDR00Wù‡sRGB®ÎébKGDÿÿÿ ½§“ pHYs × ×B(›xtIMEÛ .]‡›½ ‰IDAThÞí™ml\W™ÇçeæÎ{b¿$qœdš4­ nà ڒ&A‰Œ¶ÚTáKøm•]Zª6E…nQÙTÙ]£DB‚OûaÅ´|@ÐJ…Å"ÐFo‘@mÒ(ë:+DZã÷xÞî̽÷œýà;73NœMÅjZ_éHg®Ï=çù?ÿçí<CCCü5?’¿òg À€5kÖüY¾•E###âá¬[ÙÃ6&CCCöÏ022"Ž?.¾ü•cß5pHk•h>Ê®<ׂæÆ¬µcCƒ1kBŒ]ž›0ă‰Ö˜Ð4¾){õêÁ¿{âé7oˆX­”ˆ´._ÿ×>ÓÙÑùoý·d”Rd³ÙH¨Þz‹äéÓ„ýýÔ>ýi®.¬µ„aHxžG&“ÁC¹\& C„P*‘>{–䨋ƒƒÌô÷#„ \.ãûufç¦+³S;¿xü`oBßLx@%ú™-…LÊI199 @:FkûÍo¢Þx„ sò$Õ'ž úüó(¥p)%•J…0 ‘R’Ëå`r’܉d~ò‚ÜC¼ò —/_Æu]²Ù,^­ªÚK¥'€@ø¡œ8^òÅõ+¥våÚsxžÇüü·©¯_ù¾çytwwc­ÅZ‹ÿèQ×öÆ|é%*££T«U´Ö¤R)Â0ÄÿéOé=v ]*µxÎÔ#H$Èd2Ëï¬%›Í¢µÎ~é+_Ú)óÖ4kHæríŸïéÝ =Ïcii©åà (>ú(õlyŸ<ž®'ŸDJûA­V£ÿ©§¾ß²v~Ï®~â!–j(B [ n[&ûň± r•wêĉãsÝL>夨V«´··_Ó¦ïS«ÕXZZbüÄ Â¦ÃRçÏsÛ³Ï"¯\á¶áav~ä#(ÏkYãg³üéë_ C”Rñߌ1 lÆš{÷îí¸ešê-…­OÞ±ýΤçycbû,‹ÌÎÎR­Vq]wëV–¾úUl"ѲIÛë¯ÓýòËt|ç;cZ S)ÆŸ{.6€ÙÙÙXûÆÇa{a»¸ë®G"9oè òÉJ dœdâ`>ß%®^½Š1† ˜œœ¤^¯“Ëåpg9~CñÈ&~ñ ‚õë[6ϼù&¶f‹êÀïþð‡Ì<üp,|Cpß÷B „ Z­ò±]“tÓé§¢hyK&$uô G‡ Ûv$*• ¾ïóþûï“Ïç[ò@ccð»ºXxåL³-¯Ø¼´w/¾ÿ}êù|¬ýæ  ”Âó<Â0¤R©Ð×ׇã¤6>öØÁ»μ’¹Â| zÖ¯ÿÂí…ÛÅbc ããã¬[·.¶Ï†Ð†!år™b±ˆqn”uÂTŠß=Êüü|,ôJ®ë²¸¸H†T«Uìߟììì~z5äÊèsèÐßö&Ľét†ééi|ßgbb‚ŽŽŽ¯d º‡‡Ñss7äZyÛNž$•J]'tc.¥ŒG©Tbaa;ï¼S„axxãÆîBêJôÇ?þÉÏÞs÷.ÕJ©ë4ÞüÛCá¹çÈ,,´VfB´°±ñwØø£µXiFB<Ï#ªÕ*JivìØž|øF!U6i_*Í<Þß¿YNOOÇY.—£aN+p~õ+6?ôÙ3gZ„/òÞÈ‹\Óµ œ:Åm?øA ø†ðl\.—Y·nBJ¥»w?˜Ìçóω• ¨¦Ä¥?úø=Û …§ï½g—‹µµµáyÉd²%ô¹¿þ5ýÏüØ?îÙ½÷¥4 ± B’Éd\lÕj5òßþ6›^zéºìêwtðû×^#tÝeßp]–ÑssdFGc[M_ºÄºßýŽñÇ‘RÒÑÑÖ)%mmm1 ÆÙÕjU‹EÿÝwß=À …Øä† 8T(ÜÎääd‹½7̦T*Q,Éf³¬ÿÞ÷Z¸´@é£åƒáá–oŒ1Ô:;ùÓ×¾Æä#´€uff(\ºD\$6ªØ†95J’ûî»Oj­?×ÄÈȈhh_|ôàÍ[kmlï7•J…¹¹9ôÔTk‚Ú·™W_E~êSd2ÇA)E2™$N“ËåXÆß´©%jÈ·Þbrr’r¹ ¼rXkééé¡««+»oß¾ÝM¦›PòÀýÿúÉvo8sæ RʸBŒm {²^§íw˜Ý¿ŸÑ“'™;r$®iÇ¡­­|>O&“Ak1†z½Îôž=èÑQRSSTûú˜8tôֱÙ4æZk.\¸ÀÛo¿Moo/ÙlVo>wîÜw q¡‘€‚ðŽ\.G.—c×®]är9‚ @‡ÓfZ§cæùç[´&„ C¬µø¾O¥Ri)´ÖÔ{{ûÖ·ø ŠûJ)2MÚn€R¢µfÛ¶m´··#„`óæÍÔjµ;¢hT¬ŽXHÖkž{öìYxàvìØG­5ï½÷^ @k}ýHh”Tñá N(¢|`"ß‚€ ®Ý‰›BóòoCh ¾ç!•¢§§‡¹¹y~öÆÏ ‚@àa@ªîûâýÿü#¥r‘sçÏ133ͦ›ØºmÿòOÃÔý:mÙ62Ù ét×uq‡T*…“JÑÞÞN:!vqR)œd2ò±†J¹B©\f~~ž©©+Ì/.AœÄLsR3Ë¿W>[ÖJ ”!دû"“Nãºidt˲v¹õÐÕÕEÊI­h|\ó‹ø’]ÞÍZ¬…X†¨ƒa£¬®z™•fdXíß×Óƒ”RD (@Ä—úºï[k, ókÑ:Ò)÷ßÿr"s’Xc™›Ÿcii‰D"Ö tÃT¤DI…VËN(£ £j3E%×-6j·D,p ÄuE›tw¯gjrÂDÂÇ] Ô«•ê•é陾¾ ½tåód3Ús9„„={ö µFH ç>UÏ£^«Q¯û„AEg5vy­ÖH¥±&ŠN©i7MÚM“ͶQ­V1ÖDl-YUÿB’v]JÅâ ûo8qT.]ºôõzíŸ/^ü¯>!>\#®á¨‰D"b嚃K)[Êð0 ãŽE-áùêÚM_™º|qlìe`)’ÛŠˆ…:€N {³[ЪM&­E2™Zk¡”ÉdRD÷^»ì'P«ÕL½^·AÜŠä6Òvå…^Hž:uêPj€Mµ’ý!ú™ÿODð#훫 þ@Øó–144dãÞhÓ%á/Eë«jµÿ¯X°` Àÿkÿ çf4(”m+IEND®B`‚ukopp-4.9/icons/target.png0000644000175000017500000000442612333076511014302 0ustar micomico‰PNG  IHDR00Wù‡sBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î<tEXtTitleHard Drive©O·2tEXtAuthorJakub Steineræû÷/#tEXtSourcehttp://tango.freedesktop.org}²Œ§(IDAThÞíYÛkW?sÙû&›&æ³I4Ò6šQk¢J´/í‹Õkm)V¬úPØËƒÿBßûV(Š6¥¾{ ¥ZÈes×\v“Ùd§ßïì|ëÙqgI±P¥;äcfgΜóû}÷3ÑÄó|èÏ5ú§=Ìí ºyó¦ö/¬µ9l¾ k?5Ñ>½rñ›ŒC¦iøÔ¥l÷ºò^ö:“ÉÛ¶åyk+Cgz’Ù;{ÙÚ’Ï2ΘŒ#ßY[·RƒçÎ^øi;D4¯VÂѺþýß½UUYõusc[Ä0 FP["Nç½Ã ñlssS¬¯¯‹H$"ﯭ­Éûš¦e;s`\2™”÷1&¶ÄüÂlrr>±gäêÈÔRŒ„Y <‰áó™µµÆ#Á@PLOOËçápX˜¦)EÕ4_ƒh º®KpŠëX,–Çc!+++bjjJ„B!© õ”Q¾ºz––ºN²õ‚Ø/ \½z±™ÀôÆÊcR›‹‹‹4Íá @© "ä÷û%Ë²ÄÆÆFŽ0€Aà b~~^Êì쬘™™>”ó²uª*ká}(8ŠÅ ^侯gïé¦úf A ¶¶6§5,æóù$¸`0(Að£G¤FS©”$„g ˜ïºÌW“aDÏA–Þ^ºr©ßQæö¨Ú'ñÇbåïÖíl0±ðòòrÞ‹°@,diiI‚\]]•@¡Q޶„g ’2Zýß•E¢:Vм¬ {Ü3®_é …"Õð}-//Ïi(h€‡æ` hþÏ–R5îu9aA5´¶¶i”±Nö÷÷WnÛÊ@³-¾ë½ÝÝ~h²BÛð_â +++Ë høv‰`NÖ>æ:ãÚÞ½{ÞvpŒ½@±2Z[[#¿o°ººFƒOcB¸²‚Ù ¨EÍFœ‰àÛ P^ÄT·eq†@A}½/ûCáðN¶Ü– I÷>?<oï’E i‹êêê¼:ÀÂà÷p#X„Ó(Ü„«ªª¤Å¼¬Ð ‹cN¬[__OóOÜËÁì¶‚érˆQ·cÇùŽx‡î`¢¢¢"矬1Õ  1e‚îúAÚÄ{ äåb ‰¸‚0çÉ'ü3‰™ ôè<–WÛœÔì34ôÚNÀ·?ŽÈjrrRTVVæiÜm¸ØØØ˜ ìBÅ `‰„´ˆ—;Áb,ÈfHÝÝÝ­ñfccc¨PJÕ]0xå}/õjúƒiUMªnÃ`}}}ò ‚E‹S+@˜'2”Û X›Ó´a˜¢««Ó×ÓÓóz¡”j*Ú—îŽFÎ47·èÐ>g˜îĹZuŸ¼VÒI¥ ÈØnÒª…0'WcT즦&y òG޼êÿõ×ß>¦%¾u·º¢}ãÌð™ËË¢µÐ&4È 444È죺D¡{…bÁKûªë`=N°eEŒy,Üà`ÎYAW´o¶·µÛ×Óëƒ ¨þ Mp‚5¸ø¸ã ‰bDI‚{&d,&Äc:dttt ;‰Gw[@'ÖòÀ¡x¼Cæ{U³ ÚaWÂ"X3“Wp{awCªlnnÎ5‰¬yv'nI<¨SM9­Ôifc ¾1x²µ¥-Àí-Oàäçlpe÷PxîïÕNó`,×(l(pH—(’È4§Žq 0ÕÕÕ‰šššè±cÇŽŒŽŽÞâ”ÊuÀ¨ŒU|ÒÑÑiÎÍ͉;wîH?D÷©‚W ±8÷ÐFs; pp5\ƒ ÈqVƒ¡ÜgЪ0a>ß»wOzÄñãÇõE~Âw–ÿý$à°ÌÍÍ­ÝXÒÛÛ+ÏXà8ªfe2ü›ŸÁ д  j«Àýg*,‹ÛZííí²‘Äû---À²8‰Å@nõ[ë¡Û·o‹Ã‡SÞíÊõ3˜Z`¼ËŸ) ½°&™$JVƉ v³ÜžXMò7]Óï4Úršî³°°(nþˆ÷€™6/Öž|¨É›B¾jçÁ°ª‹¹;ÔÇòĹ£ž,A–Õ ò Ø`¾´´(Ù›¦Oä:¹å`„þ€_jgaqAö<Ùv™„]nCãMÒºt#G¤û9ݦ,“µàš¤”ýÜâXA<&ñDÛLï×Öî‰éÉŒ>×↕J¦ffgçêëvŠ´ÍT¸Ê)a££Gfûz"ÏàÒ›i‘"R¸eÑÆžÜ!›5 HŽ…h<¾É/äZa²0$Jîˆt ÷ÊZ+KÄSÿ$Ln»º²ò'û?1ÒQ’Zæk–µñŃÕ{õ:Åö´¼ÉçM g,µÈñ Ä€{‡Vä°ggSîßÿŒ®—ܶæXi eûÏ*’h±]çG&ÓÔ(÷k8“Æå»øí´6~!ð"aí ·m'/_¾ì¿qãÆ/¸Æ4Ð Ð9:dL×ù¿>˜rÚÑ~†-à%â a»®óŸsßF•M³¢uOBê·R­ôâÿ5¿!Pž±×ÇyIEND®B`‚ukopp-4.9/icons/media-pause.png0000644000175000017500000000641112333076511015202 0ustar micomico‰PNG  IHDR00Wù‡ ÐIDATxÚÕZ pUÅþö>’›'È“ ¨ÚH ä¡@D­(J™j‹ÓÂŒo°3Ö±t,:µu¦‚(ÚÂTm±CQ,VE@ ÈCÞF©$DB ¯îcûíÙsï=7¹pRÅ:Ý™Îæœ³{÷ÛïûÿýwO.]I¦]M˧ ¢¥ÓâÌgçi ´ã´Ã´/h­—âGÅ·lŸM»›ö#Úµ´DK¿ò"õvÚÚ¿hoÓjÿ×FѧÝF‹7îħ d^%‘6Hí/&ápéßú%:šs'š+%ê¿:Ï…ÀtÒÞ§½@Ûû]L{Žv'ÍÁAyÅÌ ÌPö¦»¼º]óï ”ðƒ”&êÚp”‚ÚT”ÂÇ×hïÒž »Ô´‡hÏBi=9Aå ™Ä‘ÆË8—Wg§ˆ‚œT943 Ù½="Åã’n§Ñ½ð¤lñúQ{Æ+ŽÖ·É²šsøâëqÞ$²Nò-‡¨¤ÖSªA m!íÔ·B{“6ƒ’(œ%1|&àö §O‚˜–Ÿ.Çå¥!Åã´Ó}T½Å;+šåúà ¢¦©ƒ@¼Àgk®–”œze-mŽ èPNªôYˆÞ9Àd²›ž‡~Éq˜yM®Ø;¤š.½^ Û„ºÜÚSykÔát+ƒUC°‰*=S£„ö³ :ùŨÁo¤]…Ü‘7=!áI×å¥ÉÛ 2ïv°±JÙjÀìˆu=»=®³„úèôñ^Y½ØAVàå¤o|N z¿z—›.âB”lè]!tÐ) W|¼œQ) sS¥þyË€Âu!MbZ†êÒ~n¼!ÅÁêsrmY=üô_(ß®¢Ç’S,Êaß¡Ý#Ià"ú©wÈÄ ¾‰ºE¨•U*1§"t³»t¢n…£“¶ãíxçP=ý›~ñþ"àÄ~õôŸ´»Ðűcýì#´%HËøñb)S0ý‡é"/=QÓ-ÌŸVÓ,¬ ˆÈL‹‹É&êž”QÏ+Úź7HÙÎIÿǮժù´¥0Ø ËáJÆO–ra" sRQ”›ªÇ'Ì("D¨¥9n]ƒ5Úˆ.‘GZêz¶ÍûÒòܘ]çý}”ÓA†\Ô— üýZ Y[Ö‰®Þ6hûSàú9è“äÆÔ+úA8ÌsàÁ€Dg‡vÅÓ+^·Sca¸÷ží´mŸà†CEciz Ûm8Òˆ&>a$ßõ7õš’÷ݱ¨ô`7ze ü|{Kã§É¾a,­Â˜3‘Ÿ‘$Ÿ™0ÀT‘0"I¬úýŽÉSíP‘‘è–¯Þ4(æûÖ{O•œ‡OµEØâÞØæÃ¶cÍí™ œ­SOÇÀL;¬VȦ=&Qxú¥Ä‰"®¬¦\L 1,3Q>7ñr[÷­¯õm>ã¯Ì$—|}ê`[Ol­Ÿ×·KÍ@HNûjΉÓ-ç%rIZÿ¢0•2Ë @Åür$õñà¡·˜{0¬Š!!€EB#˜*,¾q ­fTº6-µ,ö³êæ<Û6 6WâP}[XBdÀ¨7±ŸÏëèÌ**½rÐÖÄ †ÐjCÐþˆ1³¦<(Ìm8û*Ó¡AÍëJH…™IòÀŸc]¹<Ùî7šg%ºä»`ÇÀC›+Åz-!”ú¾"È‚ A7.ؽZÝý%mqÀÚDÌ} ȹ ½ÝÔ'Q¯]("˧ ²Í[ÖUàkí¸Œý}8Ýžy›Žc_]wTä?ÞÔŽ³*xTsa^q¿z}+m’–ÚIÕ!±w"~µVÀé”)qè›g2 ˜>Àüxxc²=SÖUÈZÓ‰³éÄo¶÷û6UнРéAíŠÆ¶óò”ŠFþ€Àó3$ÚϨMQ–0=z'®¼¸÷Æ 3Sã‘ï²0£¯£³’äÊH¨ø#h3$¹eÉ4{÷RBŸÖ…è(ÐÚé—õç::V>)ñÕ'ªÉ8à´˜xS¦y†\Ò™mzâœZ:†|DXNc²’°z²½„Æ­¯@M›ß^N¢ ;§ÙKh%´;$!•0èÁæõÐР*?XN½¡šÌU~Gû5fý(ºÅ˜õ4j6ÎíÐ 8B ¨:ĸÌd¹æFû0:Šë@µ)¡\JhOÖ™›«ÄÎúVi^3 ›ŸfÕŸb`߇«Õ°ñ{àOÊ0w1ç1Æ S=.¸­ÔB¦ëâ:úÀÚIö 7*"7Á%L±0cK•Øå! €€±«3|¹[b… œX®¼®ü.†\Ê(2‘EJ×3 }`¸X) N´;+  „Êz »[·Vá“°„¢  ã|`*ƒ£ÌN—= š¼0¿ ü È˜ißUB&7uÅö©Dþ–JYÕa—“/¸zÛ10½ô„Øb -!nx¤ßTŽì“xÙðfDB2KÍ«êtÀ£8»Ï\hýø\[W–TEø’ íLÛV-¶Š€ƒöªµ©û‡wI,{$,!å ñ³§±·‚Ã§Š¢¢ÿŽXqF>¾!×VC·U£Ê«%4>ud¼}›)Û«Qzª-Z>”Lø¥Ž@JB»>þú[ÕäÙP]Žé ¡·?ÀYwHsЂ)E„͆(æÆfÓu9¶ äí¨‘&Aòè¸l[&逸 š¦º0@¬}UbÝrÕd^d!>{±ÅÙ8•lR æõ:â½â~‰róØl[ƒw×ÊJ¯Î….'€ŠÑ—Ù¸qW­(9Ýn ÔÈ…(!é7ˆÈ KH|V¢šŒ‹¤ÉiIX”Èå ËȸZ%DIMè—€­c²må0hïרôL 9q|Ôe¶m&î®EÉ鎈TL ™3¯¯~ö9ÐÚL­éTB•ÌMÀSo -ÐršQÈaC]‹û&È­£úÛ20ð@¬:ÐNç”Ç 3m˜¸÷¤(mìˆHÅ:ë ¾_^&ðô=ê}EÁ¤ètúæ9÷>à48PFfÙ(«—U&€x–a`tÐtÀô…V¾ ðÑ›ÝÒi½¡éÕσ—6 †\L ñZÜ'%×dÙÊaàᘠ€  2?ݶ̈́ƒu(môFäbX0"¥v& ó§gNwÛШ¢·”óILM_á5ÁÁ†C§yäÖáö |Õ(«|&n2pE{ÊN‰Ò&¯9ë! # lXüyQÌ-¥*zSŸ‘-°„q6)É”M€ö °eXº-€AG›d¥/¨Ã¨Û! I³0éóQÒìö€  áõQ®S µÜÔ«¢Ž,îÄì‡{æ ²  .jFíM=ç†'j…ŽÚøëÑ}êõK¯9Vïö¨–MŠzSN^q?k9/ÎtrÔzñаr‰Äª—UkpcUôÁ–Ë‚ùÞÐ|^û€Û¥Vç~#á m?£NCÛÄn‰ºi>ÀœÇçF|àÈaà±™ ¡>u6zу-UôÑbnžÀâw$z§ €d–j]è"¡¶+ˉ]ødβˊÎu"!Ò\};è;>ðóùÙf:î]Õ=:ZT%r¸[4žÙÆ F%ÁD.H.§u‹Á„ê1tµRÞ¤£[®ceÀÏoR  bÀË`³p.Õ¾MuÒãÃ]U"ÇënžZ"è‘.—CôgZ`H)œn˜‘ª' úGxæ»ä<';ü:mn÷ <3_¢äCà¿<^•ÈŽÑÅÏ2JKq”Ò€\k’gM¹#[Ðnd@À”yíà Oxýú»Y3e³‰å§¥ßø‡„þÄ4ëÆ¸éÉ/0ÒìlÊ)'Þe,ÒÝe9;qøŒ§KªÌkM§µ”J›q¸ x’›•ÊrÕò[}b²ÊIäsÇ Ì}Tb.ý<%YE&‘Ë<'‡+m ]¨|ÉÚô¬‡$¤2Lj¾Å/E ÷¹ÕÁˆ#ÑÒ ¬X*°â% Ÿú°i>ò…JôgÖlnNî ˜9[ 9ÉÈ—’ÝN‘Ny¥QZ)ôµ5Õ Ù¡ÆH¹4s° ”K«/ µßÚ&°f•Äk/rž«¿“Ϭ֢։ç¡"”•žÉWôÛïFn§¹nX¢`I‹ƒ:º¨cÿ>à½w[˜Á4Ôßñ‡î®¥û¿ôí'p퉂¦YCúgI#üª#™vf˜'ë˜.– ”•IìÙ 4žþ^þÕ kù¿ýgXå{ùw›ÿ‹OÉ`8ÿNÔIEND®B`‚ukopp-4.9/icons/edit.png0000644000175000017500000000501712333076511013736 0ustar micomico‰PNG  IHDR00Wù‡sBIT|dˆtEXtSoftwarewww.inkscape.org›î< ¡IDAThÕš}påÇ¿ÏîÞîÞëîåòÂ%10y•bí8UgŠˆJ‡©­Su¤¥3N‹Š0tpŠÕú2­m¥1­(R’–RhDg,ˆ ØHHä\LîÈq·÷¾w»÷ëIÎ\ H ão晹}™çù|žý=/{³ŒˆðMîr|ÝF»AƘà65¢(*étú(€íDtü¢ê­bŒ¹%Iz*›Í.˜4iRvúôéRIq1ßvèPzûöíL„ñxü1"úxDQÁ €ÉV«µë–[nIøá‡ )S4¥D"AÝÝÝ´jÕª¬,ËiŽãŽ¤î‚§cìFQßlhhî[¸ñ‚žçóJII fΜÉžx«ucŒÑ…¦F{¾V’¤hSSùý~ ööR8Êõ¼®ëdmݺ•œN'Mž<ù3Q'¨Â;l6Û‰+Vdý~?õööR(¢H$BñxœôTŠ Ã Í›7“Óé¤)S¦´Ž¾ ‚ ¬¸áúëÓþžê=}:>•JQ&“¡M›6‘Óé¤iS§^|ÁH²,ko½õVü™3gÁïØ±£~Ú´‹†'¢‚-d7”––J3fÌÇóàx<Çå Üžž¸Ýîh[[Ûݺ®·~U…Œ±1ç:_(+'Mš$ æxÇã8jjj‹Å„ „o±X>ãyþ‘¡× %Pìp8ØÐé’çy€iš¨­­…¦i2cÌõðu¢(î¹ýöÛ=<Ï?ë /Ž?NCÓfÞ4M¸\.X­V¸ê<ðõ¢(î^´h‘Úðè£Ü¬Y³xI’~3o8p€Ó"pýŒ±ü@©®®€©ÃÀOEq÷’Å‹•ù÷ÜÙ¦‰ °l6ûƘXP":©(Ê[¶lɃÏf³È?~<“eùY·ÛýÕj]Ã[Âû>cì&I’þ»lÙ2åλîâî¯ôz!Š"`ZN´Ú»äÁóüã×]wÝc»víbC{Þ4M˜†-ÁÑ£Gs¥££C?|øpÆçóY–-]*Λ7™¦ £ÿ~Ó4ñ£;îÈúýþ»‰¨¹ Œ±)ªª~rúôin8ÁÇÆ0ç_72Ü?äÚÀõÝ»wC’$À±F "Às9Ž÷æÎkH’³?÷Ï›*2äþÍͰÛíGÚº¤Ûi·K˜Ç!»Ân·;fÏž=~ݺu€< aE†{ý¿[ZZÐÙÙI<Ï¿@D±6/Ùp»„Œ+GÓ¯ç]SQb›ØØØ(æàÏtδ"‘ퟹöíÛ‡×_cÆŒÙÝÛÛû¯Áí^’Aìq wŒË^zù‘2ìP±÷‡7v9°ný&ˆ¢øå=Ï@noÛ¶ /56¢¶¶¶³½½ý‡étú£K*P¤wÕej|¸Â#;𲋣ûrx©¥¯üe$I‚išè ±|ùrØívÔÖÖâš™3Q__¶ˆa  áåµk±gÏÔÕÕùÚÛÛ—ƒÁæ¡í-bE˜?¡’=¿zÉØ"Ù®B°öÁKªoÁ®÷?ÅêœÁšW6 ‘HàÇwÞ ·ª¢ÈãADÓp°­ ‡sæÌA]]— ]]]Ø·?v¾óƇÝ~ õàÁ‡£Ñè{çb¸h1.aÁ„+¸?¬^<Ö-æàÇ@V½cÐÃ>¤£~ìý´/þ‡GOPGyyy6™L6uwwï,--½Êf³]ët8®…Ã-F<‘€¢(PUªªúOž<¹³µµõ "jŽã¢JTáÞÉWð¿ûãC^U²÷¥è,…¤zAdBû‰`¤4˜I ûѰ.gQusGGÇ}DƘÀ„ &Üf·Ù®¶ˆ¢;£ëZ"•úüðáÃÍ™Læçcñ4Zªò §Õ O?ÿP¥*ZðV’« ’êE6£÷õ|ü4̤–(RDpL×;::Â,uQÀÇýŒ1‰ˆô‘ðŒH ÌÍ?8}¼å‰ç¬TD«Áª@t•CR½0õ8ôp'2ñ Ì”£_àT¯€û_Œ%}ôz'¤‡«¤ð#(só‹g\iYõûªÁ¦@HJ$Õ #©õ¥M<˜ëu#¥áDPÄ/Æ’¾€¾ÀkˆH)äùâ‚Æ@¹‡_:óJñ±gï¯rYÒF I­D&Þ =Ü #Êõº™Ôp,(ãÁÕѤ/Zàó~øè¥„.à Œ-â®(¯xza•K°ºÀË dÕ I­D:ÚTØ#Î¥™Òp¤×ЇþMú©7°vðò?jeÅüÊïN¶.òçUN¾?ç%Õ Y­D*Ô =샑 ÷'#0õ>ذ¸1’è ¤6ø€&"J8O •{øU7Nµ=²êÞ*/+°ØTHj%Dgôp'Ra_^¯›é8:zlX²&’è $78П*ü°Eüo¿7ÃöË•?«vV‚­’ê…ÅîöA׺rÕHjÈfRhë–±ôåH¼+Üà#¯^̬òµÊ‹ùgnþ–}ÑŠŸVÛYÅî¤z!X•~ø/òf2Òhí’±|­6¿ÀkD4ìtY0 ÿÜ­×:~±|~µ—XìÅT/xÉÞ·@iݹ^7S(kàÓS2š´xW ±ÀÖQf4àAƒ¸¼ˆ_6÷;Î…ËæWÛx«Ñ^Éí'HÐC§ GzòVW¢,>9eůšÂð»üí«–þ‚ ¤2pM­wÛ« ‹s`SÆ#uæÒQÞê ûOÈXù×p¬þ=ÍDdŽ&<0è,ËîÝùq¢« ²» C*t éhŒd8Wö“±òÕH¬+h°ÀÆËä¯ÇÛN¤ »«53}ûšØiÉH.m/`÷¿¦Åºü±foø'ê¿™ ˆÁ‡"ñ,>ægc]Qdb½yià >è°àÉõ‘ø­þ}9á!³¢({<.úvcÃxT¥sœÅŠ÷Û-xú -êóG[l&¢m—ûËÈEqfEEÅ»Éh·Ãã`¸ªŠ0k†Ñ´ /lŽD}=‘¿h!¢·/r~œµÕÔÔ,¨©©yÊb±”†D,D¾Îãg|Ýá-6ÑÎ˃zî8çV‚16±¾¾þÖD"Q(•JéÞ%¢÷Gñü1jŸ*¾ñ_«üÈ|DÏ€„¨IEND®B`‚ukopp-4.9/data/0000775000175000017500000000000012333076511012100 5ustar micomicoukopp-4.9/data/userguide-en.html0000644000175000017500000020205212333076511015361 0ustar micomico ukopp user guide   v.4.8

concepts

first tryout  (1-page primer for those with RTFM problems)

toolbar buttons

file menu
backup menu
verify menu

report menu

restore menu
format menu
editing backup jobs
technical notes


License and Warranty
Ukopp is a free program licensed under the GNU General Public License V3 (Free Software Foundation). Ukopp is not warranted for any purpose, but if you find a bug, I will try to fix it.


Origin and Contact

Ukopp originates from the author's web site at: http://kornelix.com/ukopp
Other web sites may offer it for download. Modifications could have been made.

If you have questions, suggestions or a bug to report:
kornelix@posteo.de



Introduction


Ukopp
is a Linux program for copying or backing-up disk files to a separate storage device, e.g. a USB drive or SD memory card. Any disk directory may also be used as a backup location. You can select files to be copied using a GUI. You can navigate through the file system and select files or directories to include or exclude at any level in the directory hierarchy. These choices can be saved in a job file to automate recurring backups. If new files appear in an included or excluded directory, they are automatically taken into account. You need to revise the job file only if you change the directories or make new exceptions within those directories.

Ukopp
copies only new and modified files: files that have not changed since the last backup are bypassed in microseconds. A typical daily backup of personal files can be done in a few seconds. Ukopp can optionally retain previous versions of backup files instead of overwriting them with newer versions. You can optionally specify the retention time and / or the number of versions to retain for each group of included files. You can see these versions in the backup directories and recover them if needed.

Ukopp has a
synchronize function, which is a simple method to keep files in two computers synchronized using a USB stick or other portable memory. Ukopp copies the newest version of a file from one device to the other.

Backups can be verified three ways: full, incremental, and compare. A
full verify reads all the backup files and reports any files having read errors. An incremental verify reads only those files that have been newly written by a preceding backup job. This is very fast and provides a high level of security. A compare verify reads all backup files and compares them with their corresponding disk files. This is normally not necessary, but provides an effective check that all hardware and software is working correctly.

You can report all files in a backup job, or all files in a backup directory. You can search for file names using wildcards. You can report the differences between backup files and their corresponding disk files: files that have been created, deleted, or modified since the backup was made. These reports are available in three levels of detail: a list of all changed files, total file and byte counts per directory, and overall totals.


For disaster recovery or file transfer, ukopp has a
file restore capability. You can select and restore backup files to their original directories or anywhere else. File owner, permissions, and modification time are are also restored, even if the backup device uses a Microsoft FAT file system (normally the initial state for USB drives).



Concepts

The files in a backup job are specified with include and exclude records. These have filespecs with optional wildcards placed almost anywhere.

Examples:

    include /home/*                 # add all user files
    include /root/*                 # add all root files
    include /shared/*/documents/*   # add shared document files
    exclude */mp3/*                 # remove files in mp3 directories
    exclude */.Trash/*              # remove trash files

The first include adds all files in all users' home directories and sub-directories. The second include adds all files under the /root directory. The third include adds all files under the /shared top directory that also have an intermediate directory named /documents. The two exclude records remove all files within all /.Trash and /mp3 directories.

The backup target is the device or directory where the source files are copied-to.
This is the directory of a mounted device or some other specified directory.
If the target is /media/sdb2 (where the device sdb2 is mounted)
and the source files are:  /home/<user>/documents/*,
the backup files will be:  /media/sdb2/home/<user>/documents/*.

GUI interface:
The above records are normally generated using a file selection dialog.
This is documented in a following section: editing backup jobs.


File Selection Logic

    loop:
        get next include/exclude record

        if EOF, done

        if include: add all matching files to backup file set

        if exclude: remove all matching files from backup file set
    loop-end


Note that excludes are effective only against prior includes. They have no effect on following includes, which are processed afterwards. See the section on editing backup jobs.

Restriction: include records must include at least the first directory name (top-level) without wildcards (the GUI file-chooser does this automatically).


Retaining multiple file versions: if this option is elected, existing backup files that need updating are renamed with a version number instead of being overwritten. If the backup file "foo.bar" is updated, it is renamed to "foo.bar (1)", and "foo.bar" becomes the newest backup. If it is updated again, "foo.bar" is renamed to "foo.bar (2)", and so forth. Newer versions have higher numbers, and the unversioned file is always the current or latest version. The section on editing job files explains how to specify old version retention policies.

Ukopp limitations

    max. 500,000 files in a backup job (compile time constant)
    max. file retention is 9999 days and 9999 versions
    must run as root user to mount backup device with [mount] button
    must run as root user or use sudo to copy non-owned files
    not useful for disk imaging (operating system backup)



Ukopp first tryout


After installing ukopp, please perform the following short exercise. This may be all you need at first. You can enhance your file security and ultimately save time if you read this whole document.
The exercise will check that ukopp functions correctly on your system and help you become familiar with ukopp usage.
  1. Choose a backup device or directory. If using a pluggable device (e.g. USB drive), plug it in.
  2. Start ukopp: click the desktop launcher or input a terminal command: $ ukopp.
  3. Select button [ target ]. The drop-down list shows disk devices and their mount points if mounted. You can choose one of these, or input your chosen backup directory.
  4. Select button [ mount ] if you need to mount the backup device. Check that the selected target device/directory mounts OK. Mount may not work unless you are root. Use the [root] button to become the root user. This works like 'sudo' - enter your own password. If this does not work on your system, restart ukopp with 'sudo' or 'su'.
  5. Select button [ edit job ].
  6. Erase the default backup job shown (select and delete, or use the [ clear ] button).
  7. Select the button [ browse ] at the bottom.
  8. Navigate through the directories and select the directories and files to be copied.
    • double-click a directory to open it and enable selection within that directory
    • select one or more directories / files, using left-mouse or Ctrl+left-mouse
    • click [ include ] to include the selected items in the backup job
    • click [ exclude ] to exclude the selected items (previously included from a higher-level directory, and you now wish to make an exception)
    • use the buttons at the top to go back up the directory hierarchy
    • click [ hidden ] to enable / disable the display of hidden files
    • for now, you can ignore the inputs for "Retain old files" (see editing backup jobs, later)
  9. Select the [ done ] button when finished selecting files.
  10. Inspect the generated include and exclude records
    • these may be edited directly if desired (e.g. erase mistakes or redundancies, change the order, or even make additions or revisions using the text editor)
    • re-enter the file chooser dialog if wanted - new choices will be appended
    • cycle between the editor and file chooser as much as needed
  11. Set a verify method: choose one of none, incremental, full, or compare. Use compare until you are confident that everything is working, then speed things up later by changing to incremental.
  12. Select button [ done ] when finished editing the job.
  13. If there are errors shown, select [ edit job ] and fix them (remember that exclude records must follow relevant include records - excludes are exceptions to prior includes).
  14. Select menu: Report > get disk files. Inspect the counts. Be sure the total byte count is within capacity. Look for zero counts, indicating possible errors. Re-edit if needed.
  15. Select button: [ run job ]. Backup and verify should run automatically. Check that the error count is zero.
  16. Save the job file if desired: menu: File > save job.
  17. Select button: [ quit ].
  18. Next steps: play with the report and restore functions.

Detailed Usage Instructions


Toolbar buttons


root
This button restarts ukopp with root privileges if the password (sudo) is OK. This is necessary to mount a backup device or copy files that you do not own. If this does not work on you system, restart ukopp using 'sudo' or 'su'.

target
The drop-down list displays all drives that are visible to ukopp, with their mount points (if mounted) and descriptions. Choose one of these to set the target device and directory for a subsequent backup. You may also type-in a directory directly. This must be a valid directory for which you have write permission, and of course there should be enough space for the backup files. If an unmounted device is chosen, it will be mounted at a new directory under /media/sdxx and this will be the target backup directory unless you change it. If the job file has a target device and directory specified, ukopp will attempt to mount this device and directory when the [mount] button is pressed.

edit job
Shortcut to the backup job editor (same as menu File > edit job)
.

run job
The current job is executed.


pause / resume
The currently running job or menu function may be paused and resumed. Use this to inspect output on the fly.


kill job
The currently running function is killed.


clear
The main window, where messages and reports are written, is cleared.


quit
Exit ukopp.




File Menu


open job

Open a previously saved backup job file for re-use (edit, run). Default location is the hidden directory /home/user/.ukopp (or /root/.ukopp).


edit job
Opens an edit dialog for the current backup job (the last job file opened). If no file has been opened, internal default data will be used as a starting point. Editing jobs is explained in a later section.


list job

List the current backup job data and diagnose any errors.


save job
Save current backup job (with changes) to the same job file that was last opened.

save job as

Save current backup job (with changes) to a selected job file. Default is the same file that was last opened, but you may select any file.


run job

The current backup job is executed. Backup and verify modes are taken from the job.




Backup menu

backup
The backup job is run without verify. You can then run whatever verify you want.


synchronize

This is a bi-directional copy. Files present on one side only (disk or backup location) are copied to the other side. Files that are present on both sides will get the newest version copied to the other side. "Newest" is based on the time of the last file update.


Assume you normally use computer A, but you need to use B while traveling. You can use a portable memory device (SD card, USB stick) to keep the computer files synchronized.
  1. A and B must have identical backup job files, naming the same set of backup files.
  2. Initial synchronization: backup A, move the memory device to B, restore to B.
  3. Work with B: create and modify some files.
  4. Run synchronize on B, move the memory device to A, run synchronize on A.
  5. The modifications done on B are now carried over to A.
  6. You can update files on both A and B in parallel, as long as you work on different files between synchronizations. Synchronize A, then B, then A. Now both will have the same set of files, and these will be the newest ones present on either A or B.


Verify menu


full

All backup files are read and checked for errors.


incremental
New backup files are read and checked for errors. "New" means any files written by an immediately preceding backup. Files not modified are not checked. The backup files are compared with the disk files as in the "compare" verify described below. Non-matching files are listed.


compare

All backup files having the same modification time and size as their corresponding files on disk are read and compared with the disk. There should be no differences. This verifies that ukopp is working correctly. Other files are read and checked, but not compared to disk.




Report menu


get disk files

The backup job include and exclude records are listed, along with the file and byte counts that are added or removed. Look for zero counts, indicating a possible error. The disk directories are read at the time this command is executed, and the list of files included in the backup job is retained in memory. This data is used to determine which backup files are now out of date and must be copied again from disk. The file list is static and is not updated by disk activity. The list of "new" files for a subsequent incremental verify is also reset.
Note that this is a report to help verify that a backup job is complete and correct. It is not necessary to run this report before running a backup job.

diffs summary
Report the total number of files in each category:
 
   new files disk files with no corresponding backup file
   modified files 
disk and backup files exist but are not identical
   deleted files backup files with no corresponding disk file
   unchanged disk and backup files exist and are identical (same modification time)
Differences between the disk and the backup files may be caused by disk updates (file additions, deletions, updates, or moves), or by changes to the backup job file itself.

diffs by directory
The above counts are reported for each directory having any differences between the disk and backup files.


diffs by file

List all different files, grouped in the first three categories above. If a file is present on both the disk and the backup location, and the backup file is newer than the disk file, then the file is flagged in a way that is easy to see. This can be normal if the backup media has files from another computer that are used to refresh the (older) files on this computer (see the synchronize function in the backup menu).


version summary

List backup files having old versions retained, with the range of versions and file ages (days) available. File age is days since the file was modified.


expired versions

List backup file versions that are expired and will be purged from the backup medium or location with the next backup run.


list disk files
All files in the backup file set are listed in alphabetic sequence. Use this to check that the correct files are being backed-up.


list backup files
All backup files are listed in alphabetic sequence. A summary of the space used for prior file versions is also provided.


find files
Enter a search pattern with optional wildcards (e.g.
/home/dir*name/file*name).
All matching disk files and backup files are listed.


save screen

The main window, where messages and reports are written, is saved as an ordinary text file.
 


Restore menu


setup restore job

Specify the copy-from location (backup device or location), the copy-to location, and the files to be restored.

The copy-from location is the topmost directory of a tree of files to be restored.
    example: /home/joeblow/documents   # backup device mount point is omitted
The copy-to location is an existing disk directory where the tree of files will be copied-to.

    example 1: /home/joeblow/documents
    example 2: /home/joeblow/documents/restored
In example 1, the restored files will go back to the same place they were when backed-up.
In example 2, they will go to a new place.


Files to be restored are specified the same way as in a backup job (see the section below on editing backup jobs).

If you need to restore multiple trees of files, you can do this in multiple runs, or you can simply begin the tree at a higher level and use the file selection dialog to specify multiple sub-trees, with included and excluded branches.

Note that files being restored from a backup media will replace the corresponding files on disk, but other files, not being restored, are not affected. Thus  include /some/directory/*  in a restore job will replace all files in 
/directory/  that match the files being restored, but other files will be left in place. If you do not want this, you must delete the files in  /directory/  before doing the restore.

list restore files
After performing the file restore setup above, use this function to list all matching files that will be restored, at the locations where they will be restored. You should check this list carefully to be sure you are restoring the correct files to the intended locations.


restore files
When you are satisfied with the restore job specification, use this menu to start the restore. You will see a running log of the activity. The file owners, permissions and modification times are also restored, even if the backup files are on a Microsoft FAT file system.



Format menu


format device

This is a convenient way to initialize a portable memory device such as a USB stick or SD card for use with ukopp. You may select the vfat (Microsoft) or ext2 (Linux) file system. You may choose from all known devices, mounted or unmounted. You may also choose a device label which will show under the device desktop icon if automatic mounting is enabled. Before format begins, you are shown which device will be formatted and given an opportunity to stop. Be sure you format the correct device, since all data on this device will be lost!


Microsoft vfat works somewhat faster than ext2 for USB devices, for reasons not clear to me. The disadvantage is that some of the strange file names typically found in Linux hidden directories are not vfat compatible and will not copy (error messages are produced and the backup job continues). Use ext2 if you must copy these files. Use vfat if you must exchange files with a Windows computer.



Editing backup jobs


The [edit job] button starts the job edit dialog. See the screenshot below.


include and exclude records

You may edit the backup job (the include and exclude records) directly in the text window. You may also use the browse button to start a file selection dialog. The dialog has the buttons [include] and [exclude]. The "show hidden" checkbox turns the display of hidden files on or off. Select one or more directories or files, using left-mouse or Ctrl+left-mouse, then press the [include] or [exclude] button. The selected directories or files will be written into the text window as include or exclude records. If you select a directory, the entry is modified to add a wildcard at the next level, e.g. selecting  /aaa/bbb/ccc  and then pressing [include] generates  /aaa/bbb/ccc/*.

The include and exclude records allow precise control of the backup file set, allowing you to quickly converge on the desired results:

    include /aaa/bbb/*             # include file tree under /aaa/bbb/
    exclude /aaa/bbb/ccc/*         # exception: exclude the /ccc/ subtree
    include /aaa/bbb/ccc/xxx.yyy   # exception: include file /ccc/xxx.yyy

Because of wildcards, newly added files within the scope of existing include or exclude records are automatically comprehended. In the above example, if a new file is added in /aaa/bbb/* then it will be automatically included in the next backup job.


Here is and example of how to include all files in a direcory but exclude all subdirectories:
    include /some/directory/*
    exclude /some/directory/*/*

old file retention policy
You may optionally enter a retention policy for old backup files. If there is no retention, a modified disk file replaces the corresponding backup file, and a deleted disk file causes the corresponding backup file to be deleted. If you wish to retain previous file versions, you must specify a retention time in days, and a retention version count. The values in the GUI dialog (days and versions) apply to each file that is selected when the [include] button is pressed. Old file versions are deleted when they are older than BOTH retain rules: if retention is D days and V versions, old file versions will be deleted only when older than D days, and not within the latest V versions. The latest version is never deleted. You can disable either of the limits by using zero (retain zero versions or zero days).

Here are some examples that will hopefully make this clear:

    retain 10 days and 3 versions: delete versions older than 10 days unless one of the 3 newest versions
    retain 10 days and 0 versions: delete all versions older than 10 days
    retain 0 days and 8 versions: retain the 8 newest versions regardless of age

If a retention policy is given, the include record in the text box has "(ddd,vvv)" appended to it, where ddd is the retention days and vvv the version count.

You may alternate between editing the text window and using the file-chooser dialog. When you are done, press [done] to accept. The include / exclude records will be validated to the extent possible. Re-edit to fix any problems. To change the sequence, cut and paste in the text window. When you are done, use the report functions "get disk files" and "list disk files" to verify that you have the correct files!


[choose target]
button
This works like the [target] button on the toolbar, described above.

verify method
Choose one of the radio buttons to determine how ukopp verifies that the backup copies are free of errors.

   none no automatic verify after backup (use the verify menu instead)
   incremental  
verify and compare all files copied by the backup job (new and modified files)
   full read all backup files to check data integrity
   compare full + compare all backup files to corresponding disk files (if present)

ukopp job edit dialog

Summary: The [ edit job ] toolbar button pops up the middle window. This can be edited directly: click anywhere in the text area and start writing. The right window is the choose files dialog, which is started with the [ browse ] button in the middle window. Choose files using the right window, and the middle window records your choices. You can navigate around the directory hierarchy and select any number of files or directories. The hidden button toggles the display of hidden files. Click one of the include or exclude buttons to get the selected files added to or removed from the backup list. Selecting a directory is an implied selection of all its contained files, thus the selection appears as /directory/* in the list of selected files. To make an exception, go down one level, select some files, and select the opposite include or exclude button. You can refine the file selections manually if desired. It is sometimes handy to use wildcards in the directories to make more general and compact selection criteria.
     example: 
exclude /home/*thunderbird*/Trash
This would exclude trashed e-mail even if the overlying directories change (they do) and even for multiple users.


You can add comments (or disable a record) by putting # in column 1.


Annotated example of a backup job file

This is an example of what one might do to backup all personal files. In this example, we avoid backing up stuff that is not important (browser cache) or stuff that can be automatically regenerated (gnome thumbnails). Two old file versions should always be retained, and all revisions up to 10 days. All files copied during this run should be read and verified. Files not copied (because they have not changed since the last backup) are not verified. The backup target or location is a USB disk that, when plugged-in, mounts at
/media/disk (which can be changed at run time if desired).
    include /home/rosi/* (10,2)    (include Rosi's personal files)
    exclude */.thumbnails/*        (omit gnome thumbnail files)
    exclude */firefox/*Cache/*     (omit the browser cache files)
    verify incremental             (verify files copied by each run)
    target /dev/sdf1               (use removable USB disk sdf1
)

The above backup job can be created using the following steps:
  • click on button [ edit job ] to start the job edit dialog
  • if there is a default job there, clear it out with the [ clear ] button
  • use the [ browse ] button to start the "choose files" dialog
  • navigate to /home, select rosi, enter the old version retention policy
    (10 days, 2 versions), and click the [ include ] button
  • click the [ hidden ] button to expose hidden files
  • now choose .thumbnails and click the [ exclude ] button
  • navigate to .mozilla, then to firefox, then to the directory xxxxx.default
  • select the folder Cache and click on the [ exclude ] button
  • click on [ done ] for the choose files dialog
  • the following text records are now in the edit dialog window:
     include /home/rosi/*
     exclude /home/rosi/.thumbnails/*
     exclude /home/rosi/.mozilla/firefox/xxxxx.default/Cache/*
  • You can edit the records directly to shorten and add wildcards, if you want.
    (e.g. replace xxxxx with * to make the job ignore changes to this name )
  • Use the menu: file > save job as   to save the job file for daily re-use
    ("xxxxx" means the random directory name that firefox generates for a user)



Technical Notes


Symlink files:
These are treated like regular files. They are copied if included in the backup job. The target file of an included symlink is NOT automatically included. A target file is included only if it's own file name is included in the backup job. Symlinks are verified by checking they are readable using function readlink(). If the target file system is vfat, symlinks will not copy and will be reported as errors.

Running ukopp as root: Normally, ukopp will only copy files owned by the user. If files belonging to root or other users are to be copied, you must run ukopp as root. Use the [root] button to become the root user (password required). See the note below about making a launcher to start ukopp as root user.


Command line arguments:

    $ ukopp -job jobfile           # load job file
    $ ukopp jobfile                # load job file
    $ ukopp -run jobfile           # load job file and run it
    $ ukopp -nogui -run jobfile    # run as batch job without window

If the jobfile name contains blanks, quotes are required, e.g. 
$ ukopp -job "my ukopp job"

The  -nogui  option can be used for a pure command line job that has no window and will not ask for any user inputs. This can be used for deferred execution (cron job). The backup location must be available at the time the job runs.

File type association:
I suggest using the extension .ukopp for job files and specifying ukopp as the "start with" program. Then you can click on a job file and launch ukopp.


Desktop launcher:
a desktop launcher may contain a command like this:

     gksu /usr/bin/ukopp -job myjob.ukopp
"gksu" will ask for the sudo password and open the job file as root.


All backups are incremental:
A backup file is considered identical to its corresponding source file if their modification times are the same. Such files are not backed up. If the modification times differ by less than 2 seconds they are considered equal. 2 seconds is the time resolution for a Microsoft VFAT file system, usually present by default on USB drives.
For Linux usage it is best to reformat the USB drive to ext4. It is theoretical possible that a backup file could have the same modification time as the corresponding source file but still have different content. This could happen if the disk file was updated, copied by ukopp, and updated again within 2 seconds. For use on a personal computer this is practically impossible. Another possiblility is that the source or backup file was copied from another computer or the modification time was manipulated.

Restoring file owner and permissions: A detachable drive may not support Linux file owner and permissions (e.g. Microsoft FAT). The ukopp backup function copies a special file to the backup location, with the data needed to restore file owner and permissions. The ukopp restore and synchronize functions use this file.


Special ukopp files:
A directory named ukopp-data is written to the backup location.
It contains the following three files:

  datetime            backup date-time
  poopfile            owner and permissions data for all files
  jobfile             a copy of the backup job file used
These are ordinary text files which you can view with an editor.

Special file types:
pipes, devices, and sockets are not copied.


Duplicate files:
If job file "include" records overlap, resulting in duplicate files in the backup set, this is reported and the backup does not proceed.


Finding disk drives: the Linux utility udevinfo is used to find block devices with the characteristics "disk". The file /etc/mtab is used to find mount points for mounted devices.

Removing detachable drives:
To remove a detachable drive, right click on its desktop or file manager icon, select "unmount" or "eject" or "safely remove", and wait for the "OK to remove" message, or the LED on the drive to stop blinking. Pulling the drive out without doing this can result in data corruption or total loss.

File system cache: The flush() function is called after each file is copied to insure the file is completely present on the backup medium and not waiting in the file system cache. This is a little slower, but insures file system integrity if the job fails to complete. The verify function uses direct I/O to read files directly from the medium instead of using the memory cache. This is not slower for large block I/O, and provides additional insurance that the data on the medium is valid.

NTFS:
this Windows file system can be used as a backup source, but not as a backup target. This is because the Linux driver for NTFS fails for a file open() function call with the attribute O_DIRECT, meaning direct I/O that bypasses memory caching.

Linux error codes: Linux error codes can be misleading. If an attempt is made to open a file that is already open and is therefore locked, the error text is "no such file or directory". I have noticed several such screwups in Linux. This will hopefully improve over time.

Funny file names: Disk drives formatted with the vfat file system (Microsoft FAT) will not accept some Linux file names. Notably, files names containing " : " or " ? " or ending with a blank will fail to copy, and this will be reported in the backup job. Unless you need Microsoft compatibility, format the drive with ext2, or avoid copying the weird file names you can find among the hidden files in your home directory.

Retention and version limits:
The retention upper limits are 9999 days and 9999 versions. Retained versions for a file could reach 9999 before ukopp stopped working. These limits are easy to increase, but performance would start to deteriorate long before this. If you reach 1000 retained versions it is time to start over (erase the backup medium).

ukopp-4.9/data/images/0000755000175000017500000000000012333076511013343 5ustar micomicoukopp-4.9/data/images/jobedit.jpeg0000644000175000017500000031411012333076511015632 0ustar micomicoÿØÿàJFIFÿáVExifMM*>F(ÿí0Photoshop 3.08BIMsharpen ÿá Êhttp://ns.adobe.com/xap/1.0/ 8 555 755 2 2 ÿÛC     ÿÛC   ÿÀ+ó"ÿÄ ÿĵ}!1AQa"q2‘¡#B±ÁRÑð$3br‚ %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚáâãäåæçèéêñòóôõö÷øùúÿÄ ÿĵw!1AQaq"2B‘¡±Á #3RðbrÑ $4á%ñ&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz‚ƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚâãäåæçèéêòóôõö÷øùúÿÚ ?ýE£4À¶€jº|—rZâ!1Š;©còÿïÕ|Õ ÐÔßͬ_øGí¿ç­ïþÜÿñÚ?°-¿ç­ïþÜÿñÚêúŒû‹ÚY£5‹ýmÿ=oð6çÿŽÑýmÿ=oð6çÿŽÑõ‡´6³FkûÛþzÞÿàmÏÿ£ûÛþzÞÿàmÏÿ£ê3hmfŒÖ/ö·üõ½ÿÀÛŸþ;Gö·üõ½ÿÀÛŸþ;GÔfÐÚͬ_ì oùë{ÿ·?üvì oùë{ÿ·?üv¨Ì=¡µš3X¿ØßóÖ÷ÿnøíØßóÖ÷ÿnøíQ˜{Ck4f±°-¿ç­ïþÜÿñÚ?°-¿ç­ïþÜÿñÚ>£0ö†ÖhÍbÿ`[Ï[ßü ¹ÿã´`[Ï[ßü ¹ÿã´}Faí ¬ÑšÅþÀ¶ÿž·¿øsÿÇhþÀ¶ÿž·¿øsÿÇhúŒÃÚY£5‹ýmÿ=oð6çÿŽÑýmÿ=oð6çÿŽÑõ‡´6³FkûÛþzÞÿàmÏÿ£ûÛþzÞÿàmÏÿ£ê3hmfŒÖ?ö%§üõ»ÿÀéøõ3þûoùë{ÿ·?üv—ÔfÐÛͬ_ì oùë{ÿ·?üvì oùë{ÿ·?üvŸÔfÐÚͬ_ì oùë{ÿ·?üvì oùë{ÿ·?üv¨Ì=¡µš3X¿ØßóÖ÷ÿnøíØßóÖ÷ÿnøíQ˜{Ck4f±°-¿ç­ïþÜÿñÚwö%§üõ»ÿÀéøõ/¨Ì=¡±š3XŸðÛÏ[ßü ¹ÿã´¿ØßóÖ÷ÿnøí?¨Ï¸{Ck4f±°-¿ç­ïþÜÿñÚ?°-¿ç­ïþÜÿñÚ>£0ö†ÖhÍcÿbZÏ[¿ü—ÿS?á¶ÿž·¿øsÿÇi}Faí ¼ÑšÅþÀ¶ÿž·¿øsÿÇiߨ–ŸóÖïÿ¥ÿãÔ}Faí ŒÑšÆþÁ¶ÿŸ‹ßü ºÿã´ßì oùë{ÿ·?üvŸÔfÐÛͬ_ì oùë{ÿ·?üvì oùë{ÿ·?üv¨Ì=¡µš3X¿ØßóÖ÷ÿnøíØßóÖ÷ÿnøíQ˜{Ck4f±°-¿ç­ïþÜÿñÚ?°-¿ç­ïþÜÿñÚ>£0ö†ÖhÍbÿ`[Ï[ßü ¹ÿã´`[Ï[ßü ¹ÿã´}Faí ¬ÑšÅþÀ¶ÿž·¿øsÿÇhþÀ¶ÿž·¿øsÿÇhúŒÃÚY£5‹ýmÿ=oð6çÿŽÑýmÿ=oð6çÿŽÑõ‡´6sKX76 §ÚKqo%âK~f%¹–_ý[+ ‘cùé\µè{1’Ž”P(®s`¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š( dxsþEí'þ½!ÿÑ5®k#ßò/i?õéþ‰ë`Œ*±ë׫gî·ó™2ÿÓ*è°=*¨Óm"³Å´KæãU¬æ½#0Å¦Ë SE$RŒÅ'j­mag§‰E½´0y½€  x¬ýIîþÖÞÖXàó¿ëb­ ©q§Û_ù^}´Sù~¢€#Ñîg»‚äÎc2¤¾WîªþComoc޼˜GJš€ QŠ( bŠ(Å¢Š1F(¢€ QŠ( bŠ(Å`ÞëW6ÓßÇ1´^UoU[›K©<Éí¢šozçìuI,¦¿Ž/(C³J¿òÖ¬zòoµ¼ó auж~ÉiÇú<\Kçt¨eѭͬͅ  ¸9"+P3%Õîá¶» ÖóI ‰üè‡üõ«úmíÅÓÝÁ9ŒËjqçEK>ƒlút–I…d=]‚ {ü¸#Š{ÐP>Ÿ¬ÜMv`”Ç„Kö=j´·²Mâ V•nbD“ÊŠ+¥nKao0”Io$ÿZ*Ym’S’>cÿU@zú¼ëgqÆèŒ±bzK[D šƒì6ÿgŠßʋʈcÉÇ6sY‹©ê—6³Þ$f<ÅmæÅT-u'‡W»Ž#‡š9|é»þæ·î,-ïùžÚ)¼¯ZSgiÿ>ñÏjŵ×n%ŒÉ#[“%¤“Faÿ–T±ëׂ)¼æ·žaj.£òke´Ø<›°‰2K3zÕeÐm£Ó%µŒy $~T²ŽôVÇW¸MÑý¶QRæÖ¡¸Õõs UŽãš(2Åÿ=kfÎÎÞÆ/*¢sûÀ)òX[Èò¼‘ÇûÚÔ Ho¤»›O7 –;‰b§Ç­\ &ãRÌe˜¡­‘e™æyqçÌój!¦Y‰%“ìñyÒ­4WLÔÝî.໚ݼ“óaïTl壘ŠŠÞIof‹Í–¶™hm…²[űQÛhöÑY iÇÚÃÉæËç÷  7¾ŸR›K™ P˶^HýЮƒHºúd7ËQšYtÛIÒ$–Þ#ê³V8Ò=‰þª€bŠ+ QŠ( bŠ(Å¢Š1F(¢€ PGPzP=WþA—_õÎZ½iÿ°×:£ªÿÈ2ëþ¹KW­?ãÖúç^n4Ò™5Q^I¸QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEÿ â4Í@C¡Y™n|¨£µƒ7Ú»~õæR‰n<ñEûéf°Fíz8¶bdé¼âs"è=Ñ|E2MRÅ¡êßË/•ú¯7þZÖ÷„¼meãêÚUÌj..¬PÏû£æE1†_ü¯Ÿ<'ð§Ç^øðçF}cÄš®­gqááy£]%´i¥Å ðùÐ "Šî¸ýô³tükÏüKðwÇ—–:-Þ›¯¦iw⦃H·SCsu©yÖ×pÅÜ8ýÌÇ÷ßòÇ¥{†GÛÿn“„ÖF=çL×mµ[[Ëøïì."󣺶¹2E,_dø×ÇV>ðÕÆ¹«ÜÜ¥…»B²Iïy–o+ÿj×ÅPü,ø•á뇚\>ŸE—Bûi×öï%ªK,°]ÜÇwåÚÊ-vÉ1òfˆ™§õ®ËÇ ¼Q¬øKâv“…µÙþ!êz´³Ûø¢|]ÙOeý¥Ö‘C™LgÉ„Â<Ÿ+ìóV¦gØr]KŽŒîI÷Ýé¿m“þzIÿkå;Dñ—‚Ç‚u[Ï ëúµÖq­iºøÓg73ê“]F<½J2×D}Oú¬â¼¯À^ ñ{ëèøÄüIÑm<#oˆÜŸeÑv[Aöµ¸o?ºõRùßÈï¿¶Éýù?ïígÃâ{ õ{=VÛûRÞÔ]ϧý§÷°Ä屋þy{Sæ™ZW1®ØË£ÐgŠñÚÃ>3·Ö4O|7µ[ßµ­Ï†®¢~‰krswõ´»†¿ë—YžÙ¡ø¢Ãĺjj^©o¨Y3Ê‚òÖç͈ˆ¿ÖÔz§‰í´kÝ>ÖîæHî/¤1[q/ïeùßûJ¾R³ø¯x_ãF“adúü~ÒoáÝ_J·Ç­ºmž ÒâòD²‰|á,2ùÞukø+á'ˆ<;àÿ…‹k¬Ë«j~'Wº–YfS¡ ^yó¹9š*ÔÈúÀß4ˆúLÚ—‡õ »í>)<¸ïÚX"¸\u†Iy˜Óh«¦ûlŸóÒOûû_kžÖ¾|øXž²þÅñ¥þž¾ÔôŹýì’ß[ˆÚ_(ËXe†¿ëˆš¾¿Ò´›_hö:M™sgcv–îòy²…Š/$~”OÅÿ¼-ðåíÅ~0Ѽ2×ÒyvË­jQÛ¡ë›ÐVå–±ý”W–·fæÚæ/:)cÉŸ¯ øßà_x¯âgõ/Þ[hϧizÍ´ºÕˆ¿¶µ–háò„‘ùì:׋èÿüi¡àK{?Å:•c¤iÖv_ÙЋ©t‹øneûT…£–(¡Ž_Ý0†lÃŽ”¸ÍìŸóÒOûýGÛdÿž’ßÚø¿á^â_øŠïVð–­h:Šë^+M_ÅZ…ÔŸb½€Ü^Ciýöfòeò¹0þçɪÖß ükqà?iÚ?…¼MáØ®¼;c¦êÖZü’ÜjºÀ»ˆÏyoûî?t%baÉž(4¹öT*±ºÖïtXµXçÔí#†ê{_3÷°Å7ú™(f«ÿm“ûòßÚð-/á¦à¿ÚNmJËÀ×-¢O¡ÚØhº½ˆA§42Ýù¡ÿ{ÿM«ÛL¿J Ì3ñÁ‘x¹¼+'t5ñ@o!´WÕ£7àúüß7õ®°ßIŸõ’ßÚù'Æ_ ¼u©øÓÆú‚Ä÷¾¹ñ®“©ÜèYÄ/oí¢‚Ólö÷_ôÆX(&õ®{Cø;ãŸÇ?ð”Oã[mnîÓR†îãA´YbÕkÌÛ¤û^fWü±1CÇYŸdIâ»mzÛG}R8õIàšöÞÌIûׂ.%—ñó¢«ÿmŸ÷ö¾Lðï€u“â/…Z÷ˆ~\‹]2ÏXÓá·Ó"=„²Ïhm.¥ŠY|ØN!›þ{ÅÓþ|B‡KÕ’Ë@×ôß k–þ#Õîu6ßÍ j|ïùë“ &$qZŠÙ¿m“ûòßÚ>Ý'üô“þþ×Ǿëq'ƒtíNÖ­ü-o¡xsMñ1‘µÈRS,wr‰nc„Í1¶ŠI€æh£1CšÔÈô¶Éÿ=$ÿ¿´}¶Oùé'ýý¬ÿ7éGš}«#SCí²ÏI?ïím“þzIÿk?Í>Ôy§Ú€4>Û'üô“þþÑöÙ?礟÷ö³üÓíGš}¨Cí²~OûûGÛdþüŸ÷ö³üÓíGš}¨Cí²~OûûGÛdþüŸ÷ö³üï¥wÒ€4>Û'÷äÿ¿´}¶OïÉÿk?Í>Ôy§Ú€4>Û'÷äÿ¿´}¶OïÉÿk?Í>Ôy§Ú€4>Û'÷äÿ¿´}¶OïÉÿk?ÎúQæý(Cí²~OûûGÛdþüŸ÷ö³üߥiö  õ —û ×ï$ÿU/üµ®ºÛþ=áÿ®uÂêÿ Ý×:î­¿ãÞúç^n4TÉh¢ŠòMŠ( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( 5çºNŸ<šU”†ù òaçì£Óþ»W¡â´_ùi¿õéò¯[c21¥MÇújqÿN£ÿRÿeMÿ?©ÿ€£ÿTÚ…äzmŒ—óuÎøÄ÷Z¬VÚ„Ñ—¹ûŸçüÿ©®Šøºxz§Sí0ÂÔ©Nu)›ŸÙSÏêà(ÿãÔeMÿ?©ÿ€£ÿVˆéEuÆwöTßóú¿ø ?øõÙSÏêÿà(ÿãÕ£TeÕR¼É"“Ê4 þË›þSÿGÿ¨#ðò[\\ÏZE<ä›—‚Ày’×ÎýõM¢ø–×\kµ€H$¶âS5iJ Lï칿çõðñê?²¦ÿŸÔÿÀQÿÇ«FŠÎþË›þWÿGÿ£û*oùý_üüz¦›RŽ,¿¼Ì}9¬|KÐ|wqq‘4²˜.fµv1~äÉúêAìê{?h^—Áö²j°ê²C`Ú­¬?gƒS“J{uô†oJ·ý•7üþ§þþ=Z{sKL ïì©¿çõ?ðñê?²¦ÿŸÔÿÀQÿÇ«FŠÇ´ðòéÑ’ÜÛÜÊÖ,y¶óK`2ƒþ˜þú§TÃþ_WÿGÿ¦ÿÂAi“òËùl|©qýuõ­B0pzÐdfÿeÍÿ?©ÿ€£ÿQý—7üþ§þþ=Z#¥ßÙsÏêà(ÿãÔeÍÿ?©ÿ€£ÿUÉeò¢ó*+mZ £ÏÜÍ$^w“/jƒû.oùýOüüz칿çõ?ðñêÑ¢€3¿²æÿŸÔÿÀQÿǨþË›þWÿGÿ­ geMÿ?©ÿ€£ÿQý—7üþ§þþ=\ÿÄωÚ7Âÿ ÜëÖ ¶P¬íœÏ7üñ‚ùk/½q ?iï üX×n4}2úOíXbóFiäM,?ôË÷’ù¿­gí¡ÏÈuCŠ©Fu©ÃÜêŸÙsÏêà(ÿãÔeÍÿ?©ÿ€£ÿUÈ¥Žh¡’?ßE/ëRÖ‡!ý—7üþ§þþ=Gö\ßóúŸø ?øõhÑJöÔfwöTßóúŸø ?øõÙSÏêà(ÿãÕÀø[ƾ ñ?ÄÉâ·û2xI"•‘1ûÑ gÉýL¼ÿ×W§gð¨£Z~™S„áñ™ÿÙsÏêà(ÿãÔeÍÿ?«ÿ€£ÿVˆ¢´ ÈÔ,&û ×úr«ÿŸQÿÇ«º¶æÞúç\Ž¡ÿõÊZë­ãÚúç^n4TÉh¢ŠòMŠ( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( 5Åh¿òÓëÒå]©®+Eÿ6›ÿ^*õ°F<³âߎa>(²ðú\GD>Õ{0“˜Øÿªÿ>ðמZø®Ú o\X™,n¢×òÜ]Iïaò­9÷¼Eû‘ùW¸ø“á…|U«Í©jZsO},~T³GuåVwü(Ð2çÿ¥¯™Ì2lˈJsõø,Ë.£BêB~áÕø?Äöþ3ðÕŽ«À–/ÞEÿ<¤ÿ–Õ±YÞðæ›á .3JµZC'š­8¯³ ¦©ÓöŸòÕÜJžÏà9ŸˆÞ >ð}—7¥68¼ÙÖרük¶Õ!’ ´/iÞWIeÒ$̵ô2±R ’èE?Ï“þz?ýôk¨å>vøQñiYŒ·dè’‰n êvKÿ,‡µYøgñ!¼.›áÈ|;âZòÎàZi×–Þ’/üµ‡JúÿíÿÏGÿ¾!¸”‚ ŽGq¸Ö~Æüç¡<Ï<Ô~À:„vPAïM t¢´<ð¢Š(€ø“ñ<1ö6SDšŠEç\]Oþ§O‹þzKï\'ÃÏ øÃÆ~¿¹þÛ—Dðý뉬—T°û]æ¢Äþú{‚x(O’bÿõW¡ëß|7âÛkÚ’ßÜK‰›Kisi;Eþ¤ËLŠîØ1ebÌO,zšóaB¿Ö½¼ê{ŸÈtNpää‡àï Úø;BM: ¥¿c'Ú§ºœæ{©ç¹>µ¶(¢½#œ*¸Dö³E¾L’Çå¡ÿ–U5ó½Å­Öñ êæ?[Ø\#ÑÏ“Ö_zÐÖlm|GâKibú3ßKsËúèâ¯Rñ'ÃíÄ‹r×–¢;‰ü}B?}I¡ü>Ðô)<øà·^i»û]Øýñ˜÷Íz_ZèeÈni–¥ØÛY´Ò\Im’$›þZÕ¾ÔPzW¨ ÈñOîtëkœJ|«»Ys›ÿ-b­q@àäpk 8;X–îûïïm&ˆ}š\È?µ'2ÿä"+¼ëK¸óÉ篽%@:Q@éE¤rÃçG,gþZWá=Bß[ð¶•2Àú»É§Ú1d—É‚/ÜÏ_ùeô®Ú˜`HRHã‹ýL1v È‹NŠ{K(¢º¹ûd¸ÿ^F3VJ…Q@Á@|-¬êº^®i–ïsmáû›¹ïc‹ýt1M¤^oþAš¾eý—4]SÅÿü1¬Y"ÿexP¶Ô/o1û¨ãŽL?ë­~¨kÞ´ñ…g’æ#ú©­&0ÍÒ²ôχúm–¡k©}³W»žÛ÷±5íáš(O¶kË­—Æ¥¬}–q}\QS(=Ûÿ±áÛ{‹o èÐ\Gå]Áa Éý6òy­(¯Lø&%rÚ¾³¨×V®òEeò¤—ÖºšËÔ|9eª\ý¦A$rÿÏhe¯8Âⱘ_g„©É2èNçû¯„ôè4¹oÕ8ó#ŠÞªn…m¥ [«¨ÿ¦²Õúèʰµ0xJt*uçí'Î(¢±¼]á]/Ç~ÔônÝ®ôíF?.[u“Êð¯?øgðOQð‰e×üYã[ÇúżØh·7éi§Øda[ÊÿYu×3÷ÍzfG§jñáwÿ\¥®º×þ= ÿ®uÈêñáwÿ\¥®º×þ= ÿ®uæãv2Z(¢¼“p¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(;þçíxtï‹Ø£ó~ͧ‰ò zzâ´¸£—C³G‹ÎŠKXL‘{ù5èà>Ù‰óþ‡ñâFµ¡|5·Yü#e¯øÛI¸ñ_ÝÚÝG§X[G2ù¾w4¿¾êL='â²ojŸj¾>$ÑôI°Ó<kâíOOÕ®$ûEâ\I4dµ›öI}ßσŠ÷ýcá¿„|EáûUðƪhZ~Žqg ‚Û<‘Rjÿ¼%âÝ"ïTðƪ^i‰·M’âÂkHÿé†z~îÁ¯ø‹Ä>ø›ý¡®[Üè:è¶³Òfµ“íöÞe…¤¹ó|܈q0ãÊõú¯ãN«{¢üñΣ¥ÜËg¨Yhw÷6ÓCÿ,¥Ž)|ªÚ¶ðw‡­|Owâh4¼Gw—6± ¿š,ç¯Ôš×xÒá]#‘ùrG/b+9ûð4‡¹3æ?|~ñw¿‡~•/´¿kë jXh ’XG Kç}¬ù?ë×bcÏúšÞñÇ/hÖŸµÈítIôêvÚ›¦Ì²Ap×71Z§žóÎ1Eú_A z+|øjÚM–žÞðäš}„²Íiiý›‘™þ¸Ž¾õ±âOiúï†$xàë÷6ºM•µ¾™$¾¹A5ÆŸ$šh–Qþo•åyÙé>Õ¿àÙ›Àþðe׆µ-Lñ,º‘Õ.…þ— DÓ¼²b†!ˆuwÞðÝ¿ˆ ñ>Ó#ñ­·Ø-õ(lÇŸ¯üñµiê÷†ÏL»¸ïRË_&ÛþÒ¿í¼/ÿ ¡kàÙôø¼1câémíí®¢–h.$òþÇþ´âAÿ=Žë}s$QË‘¼bX¤9¬ðïÂgMþÏ>Ò?³œ:g“öH|Ÿ²?çŽ(5<–O!‹ÇCu®ŒÞ1 M9<ïíU¹h³öÂÕñ.|œ B|ß8×%¬þÕþ)µð…5›; õ][ú†§"L²˜mï Ôlí"ˆ~÷ ûYü«èÔð'†#ñ‚ø¥|7¤¯Š‚”m`ZB/H'$úã=«2?ƒžŠûR¾Oø}oõ }2éðƒxD†A»×÷±ž´DÈñX¾$|FÕþ9è~ —^дkÝTÔôÍR{}6Il5Tw1'Îó"—ý/þ{ƾ›ldíÙë\Ö¹ðÛÂ%žIõo é:›I~š„¯=˜2µÔqyF_úìc>Vk¥fg%˜îcÉ>¦€Ó5x¿á)±Ð5==ïõ½!môx®`¹:|sKæ“,ÜÇ/•“?sÀúRÚoÆ:†.õ VËÃW×2ørÏÅVM¥<ÒÛÚZÏw2ÃsÏúÏÞ“çÓºWÑÖÞÐ-/!»‹EÓຊò[Øg€ne>\²úk5gi | iš¶¥xGD±°Ö•½¾Ÿ Áv˜Æf©Åyˆ~6xçTø±©x#Â'ÃVóA­\éöú†©m,È!ƒK³¾òæòÚo+è=x§þÎ_üKñÅþ ׿Ôí,<;q¥èºŠøvX¤šæÕ®¬–isy¿ê¼âå—>د]ðïÃøPZºNöW•â{+1†Y#òæÀôʤӾxOGÕtÍONðΑ§êz}¯öuÕ¥˜†hm?ç€=…ljgN°»¹yæÚ)dò¡¯•ü3ûP|J×|ž&ºðŽa§ë±égDԮıYY½ÝÜVÆ+™<ÝÓy"_7΋ÉÏ ¯¬Áèk’°øGà:×X¶³ð~…¦´sªE†ïþ»úÖF§ÍÚŠüU¢êÚ¥ž¥ª¥ö³qã~Þ{­6{¨­£htQ,>Lvÿ¦3 ±ë]}ß|Q§þÇ^(Öµ»]Z—HÓ.RïLZtŸ¾ŸÞÊ&óŒ¾äÃ^Ñ¢ü1ð†í­-ô é:TK5¨·´Ú ycòæ”x«œñ/À ë^_ i0[ø?ÃÓ_Gy¨éú%¬*5â—Íò›ê«S#‘ÿ…ïâ'ñôeìôeð¥Ï‹î|žžqÕác9»'ˆ¼¼ ù8„‰¼ã\·ìñÅ–ú/Ão x†m/WƒZðÕÖ©isk,¢úÔ[H£ý'ÍÉ—Îóï¸É×5ô"øÂñøÆO§‡´ˆüS *ÚºZB/ ç;|þ¸ö¨¼!ðÛÂ_e»—ÂþÒ4 nˆ36•i ÍŽ™Årîo"Öiç”^mx'ÃÞ.ÔìþÝx•ü>±xêÆûQÙ¶òB-‘-ÐÀ¢Y¿{(ÏS_@É›—'jæµ?†^×<5cáÝCÃ=€¶Zlö`ÁlA¬O<ûPxóÄZn™âI¬O@11ýî9ü+S#Âõ?xçÆß<mc¨è:Wˆt_ø‹G¼º‘neÑï|›!/œ! qûÜß*PG=ý÷àÿ¤ø¡ð·ÃÞ+žÒ5kEw³†_6!#H#ýÜ¿óË÷_ʨüð«¡Øh·Þ Ðî´m5¤:~˜út&Þ“™p½&ºÈm㵂mâH- ‹ÊKtÊŠ(¨ʾ-xóÅZ޼#០ÜxvÊMjÛP¾šóÄÒÜF†Ö8dò¿u4?óÚ¼ÖÚ×V½·ðF©¥éÍe©Zè×zæ™ö $}>-JçìÐÉý¡æùDöÿUûî¤Ãž=£ÇÿÕG¡ÿ®5§«ü*ðn³¬éÚ¶¡áM÷V°D[;Ë›Y­#ýYôòGDÔñëßÚÆ3i¶°hZ«xŠ}[ÄÚLv '”$6 0³ö×Ɇ±uŸÚSÅòé~Ò<9gkâOêSj«¨0Ñn¬â³šÔÃæÚIg,þh—÷Ñu›§ï«Üï¾øQÖµ-NãÁzÚ¾£ ^]›8Ló‰O•0'½:„^Ô<;eá鼡\hv“‹ˆ4¶Ó¡û4 âP½;šÈÈÝðÍþ¡¬xkHÔ5m1ô}VòÆn´©&ó¾Ë7üñϵy¿‰þ#ø›Lø³q¢YÿeèþOêQOi,×wªÒÍ•y¿¹¹ô–½Z ³!Š8ã·‚/)"‡þYEéP.‰c­>ª¶–ë©MئºX±1Œ7Ê­Cì2øGãçÆ?øAàÛ};PÓî¡´–xâ†iO ÛùÞuä°ÃûÑå|êÙÖ¿iËû=Ä·ÚUÆ®Áeàý3ÄZuâÅ$Q_¼÷SE+y2Kæù?¹¯XO‚Ÿ—OÔ¬Gô³Ô%󯢎Å|™æõÊ *ÊÚÎÊÖ +H"[Û[Eäà `øPüQñ߈t?x;¾m"ËTñ4—`jZÚJöö±C›0Xb M)ÿ®½x¼ßüKà/ø¿IЬã×5Í{ǧM†W†I­­ÀÒa–_*#4<þçýO_Mø§Á¾ñÆštÿèvîž’ù‘Á©À'Ëî PÔ¾x/Z±º±¼ðž‰wg}å}ª ìá"o*?*,HqéÚ²5«¦êrø3B“SÒ¢Ž泇ϳòÿÕ{WI¦i6:“Ûéö¶ö¬ò\m"òpòKçM-jd[ Q@ü¨¬Jš‡üx]ÿ×)k®µÿh?ër:‡üx]ÿ×)k®µÿh?ëy¸Ñ@–Š(¯$Ü(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢ŠC^Uoñ;ᦓ¢Z[ĺ5›ÚÂc¸2^yb9 Ž7ýûó¡ïÇ›^ªkçm7ö_´ñ6½â¿kw‹g6»k¤*m"Pòéïhb•n¥‹Ëó¤–+S÷åŠàõ¯[a3Òtü7Önì`²×ô‹ûIâKh ¾ó|ß0M4CþLÄ×/j©âˆ^ðwÄ Áúìñiž±i%Õœ—WTRŸ:(LC÷œÊ|áþx®kŸ²_†<â½^Óõ­yåÑ'K¿²Í$FÛ¬^<¿ºåö„ÿêñÖº_Š_4ŠzÕ¶©ªÞßÙÜÁ6Ñý™£-ý­àì嵤5ë™?~]-Ñ‹ÅZ1[c)º+{Ÿ,G–\óÐE„Ÿ@kkÃ^.ðŒµk½+BÕô½[T´Œ4öÖ—~d‘GÒ¼oOý…üo¤j–K¬ë&¡ucqy3tš²¬Ð¼s:Ù|讦Ši$£‘^ðÿöwð×Âÿëž+Ñ•w«½Ì’¤¶V»ÐÍ4rÌ>Ô"÷F”ÒÇL€ -Çÿ |5â Tñ•§ëöÂò{k›Ï.X¡Äß½ëÿL¦ü­R‹âÇ‹ä¬>)Ðæmad¸°j܈ú˜ùííúÔþ+øáï·^îkµ>3Óí,/<§÷1Ûy¾L‘qÇúÒ{ö®GIý<#¢øŽ×Z]g]žùuµ]DÌö§ûZö;¦ºŠyÿqò0–Rq”9é@¯Ãx'ã/…cñ…\_éŸi’Ø1ÄRH¤1ƒÓ§ùæ³!øÉð’}?S¼/ÐŒtñÙ^Ê5|‰e›ÉH}å_?¥t_ >[ü/ðãèz~¥w}¦›«›˜bºHÙüÙd–HóâIò¯)ø{û"i>‰/µÍsQÔõˆuH¯ãû3ĶÖé©=ü0 ò͘™q×Ú—Ú±±ø§à;ÿjzrÞéÿb³´Óå‡Ûâ(Íu'•F1'˜¸ýÏPó±L‰þ½ñ=¶‘¡C‰î§Ó/oÄzf¡Lÿf¸ò|¸â’AæfA??t»ñQøÇöu¶ñ¶¿ã[ýKÅ:¸ƒÅ:\:Lö1¥¨ŽÚ8¼ç…áo+ÍG,Ï(%ºž†¥Ñ>Ûø_ÅÕôßj±ÛøgK}"ßN–i’X¤hd¸yÉóL²ù X†#8æ˜h¿>k^ð~¹w©Zè6þ*·Z]®±t-¦•HÏ ÉïUÇǃ-ôßð›xÈ´—ì×ûOˆ¥o_ÜOÿ~«—ÿ†(ðkÚø^ÞêöûUOÀúeÔí,.ÌVO “Éýì§½½=s\?ƒ¿a[†Ô%ñž¥ ì‘Íkm ¼ñÙÚZÃs BâîÐÅ2âíºÄ¤`~ðÐѾñ‚<_ªjf©iú­þ˜v]ZÛ]yžAô5ÑÂ1¦Ï¡ÿ¿’pŸþèÿ üa®xƒKÔ/üÍVíåÓ#Xíì׉|¨£½õ—©æ½V€1áÓ?çÐÿßÉ?ÆøFtÏùô?÷òOñ­ª(þ3þ}?ò$”Â1¦Ï¡ÿ¿’mVn§¨=!XãË,ž\qô_þ3þ}?ò$”Â1¦ϧþD’™»Wï¨Øìé?øõµoúXàºOþ=@ÿ„cLÿŸOü‰%ðŒiŸóéÿ‘$¦nÕ¿è!aÿ‚é?øõµoúXàºOþ=@ÿ„cLÿŸOü‰%ðŒéŸóèïäŸãLÝ«ÐBÃÿÒñê7jßô°ÿÁtŸüz€ÿÆ™ÿ>‡þþIþ4Â1¦ϧþD“üi›µoúXàºOþ=Fí[þ‚ø.“ÿPÿáÓ?çÏÿ"IGü#güùÿäI)›µoúXàºOþ=Fí[þ‚ø.“ÿPÿáÓ?çÐÿßÉ?ÆøF4ÏùôÿÈ’3v­ÿA üIÿǨݫÐBÃÿÒñêü#güúäI?ÆøF4ÏùôÿÈ’S7jßô°ÿÁtŸüzÚ·ý,?ð]'ÿ ÿÂ1¦ÏŸþD’øF4ÏùôÿÈ’S7jßô°ÿÁtŸüzÚ·ý,?ð]'ÿ ÿÂ1¦ϧþD“ühÿ„cL?òéÿ‘$ÿfí[þ‚ø.“ÿQ»Vÿ …‡þ ¤ÿãÔÿøF4ÏùôÿÈ’ðŒiŸóçÿ‘$¦nÕ¿è!aÿ‚é?øõµoúXàºOþ=@ÿ„cLÿŸOü‰%ðŒiŸóéÿ‘$ÿfí[þ‚ø.“ÿQ»Vÿ …‡þ ¤ÿãÔÿøF4ÏùóÿÈ’QÿÆ™ÿ>ŸùOñ¦nÕ¿è!aÿ‚é?øõµoúXàºOþ=@ÿ„cLÿŸOü‰%ðŒiŸóçÿ‘$¦nÕ¿è!aÿ‚é?øõµoúXàºOþ=@ÿ„gLÿŸCÿ$ÿ?áÓ?çÓÿ"ILÝ«ÐBÃÿÒñê7jßô°ÿÁtŸüz€ÿÆ™ÿ>ŸùJ?áÓ?çÓÿ"ILÝ«ÐBÃÿÒñê7jßô°ÿÁtŸüz€ÿÆ™ÿ>ŸùJ?áÓ?çÐÿßÉ?Æ™»Vÿ …‡þ ¤ÿãÔnÕ¿è!aÿ‚é?øõ?þ3þ}ýü“ühÿ„cLÿŸOü‰%3v­ÿA üIÿǨݫÐBÃÿÒñêü#güúäI+‡ñ6‘asâ˜4éf¸±Ò–ÂæþâHn%„„gÍÏý6Ív»µoúXàºOþ=Xw^º½Ö¡q{i6ëi-|¹¬³‘Êb?ó×þ™PˆÛxçÂ^+ðLj5oëÝΑ‚ðÞÜ]ƒ =&òeÿ[ ßóÚ¾–Œd€¸Åx£Àrø¢ÂêÚæöÚ&¹·û9‘4ïÞŦ|ÚèÌZ¦TëL©'>Oþ=^n*„ê|¦­flÕ?çú×ÿòñê6jŸóýkÿ‚ùøõp}V sšflÕ?çú×ÿòÿñê6jŸóýkÿ‚ùøõU¨æŸçEflÕ?çú×ÿòÿñêa:•¢—y ºDÿ–qD"ÇþE§õZÎk *åŽXâxÿÕIÒ¦®#P¢Š(QEQEQEQEQEQEQEQEQEQEQEQEQEQE‡¥TðÇü‹7ýyÃÿ¢Å[¯•/GŽüuñOØk×Û6z¡§_ÛG&§mžòC5Î¥4SCkG<ítŸV†”½Ð‰ïC¥-|Muâ/ÚWCÓl4ë+;íV{[³Zܾ‰®¥†³.ÿçŒ^\·½æ!Þ±SƵ$:zGru!ÞÚy59¼0³\Áû‰³Š(¿{ûß(“äûaL¼è¯‘¼+­xŸMñwÄfÏBñ.“ªÍk“§XêÖׯÂòþI£mO5ŒqÇæÊs GT9öªÒOñƒÁ_æÐ¿´9tèî­ôQ}w$#N.¼ß*Y¦˜Ý œõ °¨¯‘~ø—â…®ðßL¶Ò5íM³ðäpIi‡ÝeÖclkÏ2#ö{MƒHp× Úøi¯|Uñ׆ ð·Ž Õmnµ}NÝ^þ{ì˸- †)/Áòx´àßòÐLsÇ4õ…bk?ñÿ£ÿ×Ä¿ú&ZùOÆZw‰4†¿|5o£ø²îö5ƑҮ5ˆoìæŽ!2ÍÃÊó<âç–G§=÷ÃÝcÇšïÅýBëY›R‹Àw zÚ=¶££­’ ?É$<^h';ÿ]Þ/j÷08£QYbŒQE£Q@(ÅPŠ1EbŒQE£Q@(ÅP¥­\ZêÍnn ³ˆEÒX¨Öu“l“Gn%[ˆŒBiÌ_ºŠ­ÜiO& ×p^›9¦Ê8Š£¸Ðêi$û\°‰ FAåÿ­­@†çY–Ûí>P–o.èÂ"òø«Ýü¶z_Ÿbé­:]J.sra–I|âç•I§¾£aä=ÁÿÏ_*²¶›¬É-¥Ô­åÞ}œàKkÇ›RoA5¼3Ç„Í'•äõ´É¼6· sö‰åžYLGÎ1óK‡VÃeÁ†Xäóc—Ê­@®5'Vš)¥$Ž/*ÌEûÑW†µJ"òŸ›£iP>‚$¸3´òý¤˜¼¹¼ºA {öŸ>_/Ìó>WIhú~¿¢ð¢Å$>hÌ^t_ëEI¬]Iaÿ[,QUM F{%´{©s_–qßœ>Õé`?åáƒ8‹¿Ú ïGº³/áåÔôŸìÛ ™olîÌ·o-Å¥Ôâ(aò±&>Ì9ó¹ó½«oOø­ê ð<·—U®®×1\Ùé7¦þÚVÆHvÝ40“'÷xär:Rh?¾]9´Ùíí­®låÔmÚ; BÞ#?›?A„O*OO¼:ù£6ì¼O¦IªøOL³ð”vzN¯5Ìñ›Û3kqms~o™öcç®zýyöÌÏ6ñí=wá©®4íGMA4u‚÷º‹¬fkK©Úõ\Í’éæääÃ^ð«â®¥ñ/W¾´½ðÌz~l²¬WòÞÇ,“K ÆC[ŽaïÜõ5›â/ŒŸôÏ M{m£&¯çéðë°XE¤·úDM ³Ã7ú®¸†L¢¶¾üVðG¼eªXhok/‰b€½ëCmó”…’ÎÆeòå;sôÖˆ„Œß‹¿õÝ_Â6Љ4ïî̱çý"oÝÚÆ}0Òž¿Ã\Ö§ûLêZ\·ö×°yô¹þÉkmóËö©¡ì‡ì¸˜+33y^O¡Æk´ø‘ñ Løi©åè1ßÉ5”ÚΩå»–19qÿ-Xâ(ýp¼ð*ßÆï‡°]-ÍÍ­ÂÉcç$óM¢ÍæÙ9žX›ˆÿuçK  ùiôêÄ蟵oˆµ[›[5ø{huY"–vH|C„EþLŠ%šs7›Ç•Šè­>&êÖëãIluˆ¼Of—1éšoR6£$ÓDÐ'–øbÌ?½êq7\qRçö•øE>›juWHmæ}ÑA¨é  KåIÆ$óôæµ—ÅšG†õÍ©øCFÓgðêÇ©G¨hÒ¤ÆV—φ%”˜"òîˆãËgúá‚r(ºõ§‰|GáûijºRM¾¯&°bk¹cŽbY£û!ò¿ãÒ^›Ö.¹­¹¾9\ÚéFæ=.ÒþÞÓÖúíÔ’j>]ÅÁ–[˵‡Êýçúžæ.½:ÓßãOƒ5+­{G»ÑÞ}^ÂÚS{g&“,°4ÑE,’D²˜±/>v8ç÷¿JÖ‹âƒ,¡³º›LKsa¥Gª5Õ®š^2ÞHŒ™2ˆñ!d>þÙà/Fø­¬x®K *ÒßN²×ç֥Ķm}f–°Å ÔÒÃ)Š#()2Ü K/ ®ßÀã ×Y>-†Æ/›û`xÌqˆ³61éÜ×Kà/ÂÁ×üHµ=iצÐiEå€ÿ–±ûs@OÄOk–ºÅ«?SM _ Y«ØÜ<H‚Ñn|Â%àîÜbÁô¬í#â·ˆ/¾3Ç£´ñ6‚× höд_* uö¿ùëþ´˜ruÇ5sļ?m¨xœOà‹ûÙ,µ/Ã÷­5€’;ó!„ÆÑgýh‹í~žµa¾3|5±ðÓjËnÿe•—ú..öà~NwÊ—v:r{Ðëß/-~ øZîÛW[¿ëZÅjúLÖ²ý¦âY%ùÎzbÇ“ƒ¿½v? 5ë¿xnâkí†æßU¿²EŒIäݢlí¯<ðïÇŸêž$¶Ž×E’ kK;4³ÕF(ò<éLQAŸ+1aã§ïG¥]Óþ>ü<ðß…l¯|˽'Gžâc[Û Ä—#ÎW"Nq/Ÿ^M{]s¾(–H®t­’ù?éÿDË\ޝñNK}?Y½³ÒÄzN»o£Î'–Kgo‘ûÐ<¬ÿËÐ>žôíOâ.‹¬ø¶ëD¶i¥ºÐ¥2_;E¶ÿuÿ=(síןóòß÷î·]ÿÏÉÿ¿u‘áiþ1ðæ™¯éS›­/T€_ZÈå¬'‘Zxâ€$ûußüü·ýû£í×óòß÷î°ô?i~%Ôõû=>s,ú%ävW±ù_êåÃ)ÿÈSVÆ+ $ûußüü·ýû£í×óòß÷î³õRÃÃÚUÞ§©\Åg§[GæËw4¾TQÅô«(èË+™ø úÐÿn»ÿŸ–ÿ¿t}ºïþ~[þýÔt`PŸn»ÿŸ–ÿ¿t}ºïþ~[þýÖež±e©Ï¨Ce{mysa7“sRqÃ÷ß½õ«øÿ¯@}ºïþ~[þýÑöë¿ùùoû÷\“üMðÌ^×|ZÚ‚ F’î;Ë¿'˜þÊ{ÿ¢k¦Ša$K$CÌVÌAèh·]ÿÏËߺ>Ýwÿ?-ÿ~ꆱªZø{J»Ôµc´´µŠY¥–^±EAMÒ585ý*ÃT²s%•í¬7vòºE0Î(Gí×óòß÷î·]ÿÏËߺãµOоÐõë­&óRDÔ-®4ëi"Ë{¹|«AÿmE\ñ—ÄOü<‡Å>)Ñ|:—_ºŠMZú+S/ýý —íןóòß÷î·]ÿÏÉÿ¿uÍê>7Ð4ÏÝx²ãV³o ÛØI¨IªÇ!–#jyó|Ø«zÚawj’Æ»VxüÔ_CZ7Û®ÿçå¿ïÝn»ÿŸ–ÿ¿uƒ®øËFðÖ¥¥Xj7fÖïWy’ÛýlÃçMÿ¢¬€è~Ûwÿ?'þýÑöÛ¿ùù?÷î³´mVÏÄ:=ž£c™uâ›ÿ Ã?¬YYA}5·h£šY¢‡ô†jÔ ß¶ÝÿÏÉÿ¿tŸm»ÿŸ“ÿ~ëÅ~/Ò¼¢>±­\}–Åf†”w3MäÃÿ‘f4í ÅZg‰­õIôë³u¨O§\çþYË ÄÐÖ@mýºóþ~[þýÑöÛ¿ùù?÷î¹/üXðGÄ É­|-ãx’xbó¦GÔbº’8»sµ­à¿ hºî-õþ™ýÙ¾º<ê &;©cÏþC¨ÿn»ÿŸ“ÿ~èûußüüŸû÷Y¶v¾ñF©áË}/O—UÒ­àžêÛìã÷I7äÿè©kkþ ÐOÿÀqþ_í×óòß÷î·]ÿÏËߺ±ÿ†?è§ÿà8ÿ ?áðÇý4ÿüá@þÝwÿ?-ÿ~èûußüü·ýûªòxÁVúÂi2éšLZ¤ñ½äv¢5ó$<ΞãšÐÿ„Ãþ`þð  ÿn»ÿŸ–ÿ¿t}ºïþ~[þýÕø@|1ÿ@ ?ÿÇøUgþ ðî—u©êšv—§éÖ±y“]KŠ(ã  ¾Ýwÿ?-ÿ~èûußüüŸû÷Y>$ðþ“¢K¢ÞéVYK-ôP´P ‘ÉÖ´J“í×óòß÷î·]ÿÏËߺŠ8¬€“í×óòß÷î·]ÿÏËߺŠ8  >Ýwÿ?-ÿ~èûußüü·ýû¨ø£Š“í×óòß÷úïÊ—ý$ÿߪw¿ê¥¤ö{DãH±ÿ¯h•^*މÿ ‹úö‡ùUáÒ¾mît ( t¢  ¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š( r7öz6¡ðæÆMvq“ogkw%ɘÇå´^\‘È%àŒ9®¸×%©èw¾!øe™cwo¦Ëw¥Ç]]Úý¦(Ç•ÿ<¼Èÿ¥zض`Ì8üà]WSµie–±-Š%Ô5¤»’ÖXæýÑ2æ_-ĬH=Çý2ŠÅ|&¡áM.×XÔ5;¸åšëHº†òêèy±ÇûÄk¬˜î³û©O¯ÿ å«klº®¼²^--¤½Ó,~Ë#%½¥Í¨ò³$ž^EÛ:r;ÖþŸà NÎo‰5Í 5=îWŒYh¦Ú+‹3•$BrBÉÓ÷£ Ï*õÌÎ_ømðÞ¼¹Ô.®–Ö{%¹:ÍÁÔA,B(Ï›Ÿ(E$¸Š.=ºWqà½Ãz6¡¨.xødY&Ò“P–X­¹”?ÙIÄ>fâz¼w¯(ÙVçûk)|W§É i‹£À÷z˜‘YÇi-®$ç÷²ùR‘æ“Ðc8®çáÁXþkÚ¦¦—ñêk¥¼’EtgŒK7&Y®¤ˆ!b"Š!‘@Háð_†$ñþ+¼»x‚÷eüé¶[ZÄFž\»‰|ß4 ÿ­ˆÉ8ª÷Þøcª_-åÌÖoý§$×D¦ªDwÄLdv I‰|©e”ƒÿ,³Æ8«¾3øXž2¸ñlšåì0®³dš=‰H†ÖÓýd€gþZI)$ãþyCÔŠãµÙr GSÔî¹½¦¥rgžìµy!ÿJ–xc‚O4y š>o'Œ‘@›àφV¯o©iš¬K*ÚÃ}§ëͳ3Ëç<Á.[Íiy]ò1Ú¢Âÿ¼3¯]XI­]iÏm©Câ;«+ýNQk-ÄòLÑ<ÓÉó#2ˆ²0ÑDzžR/ÙÎÂâƒ]‰’;ymä³šÊæ;Y"’if Amuß–<àý9®«ÄŸâñ¶™$¶¥nš”úÆ­u'ÙVë24^TiÎ?ÕEä¨õÀí@äð/ÃÝSS×$ŽF‚þ8Eî¡ym¨M•¿jľh#þzÏô¥°ðGÃÙÖÂÂÞü]¥Î’¶1Zÿm4ƒQ±‹ÌÌaó–#,¼’q“Ïsv¿®ÚÚÿL“ÄštºtúN“¥-ºè‹æ«iòI,RMþ’VhåÜuå徦´5ƒ:®£âñëÚTšÌQC3G’mckËq(„Ç/›!Š d"Hϛߑšè<àÏ ÛksjÚ4÷—wZ|×6rÉu$²³\æ8¦É.L’l†(÷dýß­nkz÷†oSȺ×ì û. ÄZ–ë<_¼òŽIf/J‹Ã>>м=¢išªµ®’<»Ö6âIoXDw9ýÔ†Y§äžÄ×–êßµ§°Ðt -oÃíg§\݈n.4F7)¶w0/úOïåýï2+<œšïè×ü9¤ø»J¸ÒõÍ"Ç^Ó.dÓu+Hná—þØÍF‘£iþÒ¡Ó4:ÓGÒíFØlì-ⶆè!‹ ¬€ù»Ã~O xÃâoü$žñ¶¿}ªMe{·£-ÄQßõ¤2dÃ7•¾t÷#÷¼~^×ñ»Æ×?¾ø“]³E¸Õb‹ËÓã^“]Ì|˜£ÞlÑWkïUµ .ÓWˆEygo}–)‚]EæÄ%Š_2¾¾i5¬ýð‡¸|wñ§àޝáÿ 7ƒCñg‰´›oÇ¢øxøxÜÉkMç ¹®¢ˆàyþd'Λ÷XWA?Ãïk_´×öÖ¥¯éSÝÛ=„Öeô–sØ ¡–).ã»û- ó<Þ$„ÌzçWÕ½yä÷ÉëA Œ­4_k:§„|lÚ¦©â½?ÀðÝÍÅÖ—`&¹Ô5<ô{~<Þ¹8®“Zð×¢ñ¯Å{Yx‚îëVÑõ±×5E¹±þÌ»)Ùmm<Ù¾Ë,Xºšˆ:Mšúv}:ÆmB B[Kw¾¶¬’Åûëq)ÌÂ)¾µs§µdj|?sðÒiô¿ßi¾ñì>—Sð”òÙê‘_Í«L–·Kuå‚|éL[‡ú¯Zô½7Ã^5ºø¬x~ÓO×-añ‰¾Å¤C©´²jšF‡u,1Í,Í)ó#ò—íR7<˜y¯¥{QÚµ2X´½8y¯™Ší­y¨ø)¬xÇv×úÝ·‹¶jÞ.ñjlºÕý¬m¦›i~ǻʛ÷Qy±CäŽÂ¾Ÿm:Äê‘jfÎÞMF(d€]ËïR)9–7þÙCùUÚÈŠŸÁŸuÝKá¼¾!¿ñFxoG¯,t«Ûç´Õ"šSv.>Íw†mžNf»†hlc›ox©¾-ØjŽƒâÉ|Ui¬ø†îó\®¥ÓÎkk¸´ñhIò¢Ά 8_^vbµã!¤üUoü&Žm'Ä×m·‡…Åù¶Ôîå½ÃÄÆ+ˈæû,^Qýt9ÎkGÄEÿ>k>³½î¡âŸ>«ic¥X_Ý-ö™2ÚYZNmO› 2 ’o7þ{LM}y4>t~\¼ÅéPXiÖºU…¥……¥½…•´"k[H|˜¢‡Ä0Ž‚€>;ø¥àŸx»U/®xCÅRê׃ÂòèvšeÕÕÖ¤<2Å&¡ÁˆùHˆ`&”ݺs»àŸøIøÓ<º^…â­&ìøóTÔuRì]E¤Ï¡ÍÁÇÊšYf0Ü7Ô×ÕÔb—8að—ÃZ„þ'ñ¿5ý9­5]OT:e„w6û .×"ÝzqæIçMÁêðúVgÅ;mwRøY¢Úé0ê÷úl~"óõËFŠöm4_Íæyxýö8BDÞõìUÎZÙkº25­”VÖ«q,‘I=ìÑÍûÙ|ïùã5fj|íâχz¶¡¯ø›ÆÐ…wh¶Z-ž§«]iÚºýÕËÅäSƒ4*%í~l0b˜d’~¬ó|Sÿ>?þ gÿäJ<ßÿÐ?GÿÁŒßü‰@uñ7@˜þÐ^ñ=ÿ‡u~ÆÇH»Ó£¹ÑRå¼›é'µ–&–8¿åŽ!—™¿uÒ¼+IѾ,ëzWÄT—GñŒúµ…ˆºÒ¬mõH ÿí=ÓÃiu,ò´£ì¦QçÚ˜¡ Œr>ºó|Sÿ@ýÿ3ò%oŠè£ÿàÆoþD ˜¿i(®ü¯ê°ÁŠÆŸ ¦“oá Rº’ÚÊaþ—ößcÍ?¹ælô¬ÍOá¯ÄŸØ|XµÖn5Û½zúÃ^´¿ì»Ó¦ê‹,˜¶]ÍxmOîü‘†eô¯¤õÿkxŠÛÄžðe߈lÆ-õ›ŸÞÞÛHgûkxÍâ¢rl4|ÿØJþD¬€ÅЮôÉ|àÕÒ´­[D±þÒŒ¥ž¿Ô7q¹ÉûWï»×oé\ìv:Þ¥`úŒvZ}µ´æè.&—ÌÏý²†º!Ú´˜@0(À¢ŠÌÔ0(À¢Š0(À¢Š0*9ÕKRTrÿª–“{DÿEý{CüªðéTtOùXÿ×´?ʯ•óosD¥”TQEQEQEQEQEQEQEQEQEQEQEQEQEQE%qž%°¸ÔþIekc{©ÝÜiKpX]ýšYd0à~ó΋ÿ½ZìëŽñ>­?‡~O©[ÝÁaw’;‰à’hÕü®¦(ÎO>•éà÷™ƒ<á<ñ+Qû©¥Þ꽎ÊÒÚ85+ô’Þ Z]C, RI ¸”ÚËÓ·±­]?Áº¥¥÷‚u”ðÞ«úUÕÌ7_Úz¼Z† ¢[sš“Í3f?3Ì÷ò³÷r“âÇŽ£ŽÆûKÓ-u­ ÙY”´7ê[—³º–Oßy½<衘xóOZÓÓ¼W¨ê:·€u‹ŸC©[ÞÏscw.™cqec$’[þê) ’i±'˜8ç¹Ï¿²þÑ™Âëÿ>*ø—·:EùÕ®"‹IƒJhmõ¨ã–öàZ\Àמg›Ì^aŠOÞ‚dþ(A®ûáƒ<] øŸU›Ä·z”°ˆ¤D“í¶ iË[ýž/0É‘B|“û¨‡¯špÃϼCñãÇš—„nRÎ+-.ít˜|ûÅÒ®$hu±ÜÉqió¿ÖãŠ8³ž¿òÛŒz/Âø·Ä¾(Ô´ïÁ¥ÙYÚÇ0ŠÅn#™#…æcû£æ¨2ü½2;RÄñKáΫñ8xµ~Ïå$:göV޳I°I4¹7} f(¿ ‡z嵇Vþö×MÔ5W¶óæMèëŽVƵÌÓ-Ñó|ÙZxšâ'Íò|œæk¨øµ¯ø“R,_ Þ\Ú'‡t™IlpeÔ&HÿíŒ8—ôÞ/J浌¿ô›»‹§émk,¶Ð"h÷%uaö¹âi¢&oÝ,Ç ²ñ7úî1ÅH¿ ¾,iS[[É­xŽãMGa ´Ö–æä]¥òî Ksb˜¿u÷A0ËÔu¾0økâÿG¨kzuÅö—«_êS‚m/„ñÙ[Ç4VpîTHÒMõ¿SOøÕñz-"¿²ðÝ£È&˜êšeí­«2M4BÐ,ÇÍCæõéë]GŠþ øîØßëþ–ÒêÒâøé¶pj^D µŠ2àysDD³L|®{D+IŸ†<}ë¶ãOÖ#Ô/t &ÚMVM^ÞH§¸µóEØ…¼Ã,R;Wå IótÕÛ+Æúg‰¬õm7Lñ<:U•¼çÝkqÜËù¿i³–#,‚Y¥>IŽLÊp3CTl>+xŽEñû,¦Ö Ð4[ijŽÂ墳žY%7žd^neòp%Ú1. OœØ€r½øU¼M HŸ^šå5}zynRÞd$Û,¥H]›bÏP{×—Üx‡Åž Ñu]SYñ-¼²]øÖ 8Ò[ˆ|»/¶E‚!ö£ådçësKíâo üK³¸¹¹kOms©D·VÚ~¶|ËÈåÔ!x£¶"aöU†ÓÍŠYA„žGï¿×ÔžÓ¾*hÚÆ§sy±ªÝénä<Ú‘{iã:xò­(óM4DÌqþ¦\ÍÏ;§ÅOˆ6“;y=½ÍÝÒÁ}.•râÂÚ-Gì¢Iq7ïO’â_ùcô®wáŸÆÝi:‘®i¯gtaó¯|Iy§Üd·û$“ªósçpçÝ)ý=WFø%¥é×^¼¸¹¹“VÐ’Yw,¿º¹¸™&žAŽKK#7ã^§^áoêWv¿ µÙn®®_Rðôö×¶‘´‹o.£äÃ,QùRõŸ,ã'ž bxÇþ1ñ±ºÑ®uêºÝõ˜KË,â³´[H$Ôà×tߴޱâ/ƒ?>$xj-:MLÙÿñ½†\Ük ·M7ïyò¦™¢ÿ·y½k^S#èšJù³Iø¹ñ/Eø„Úw‰.ü#©hÖ~0´ð„ñézUͥܢâÐKÑ™.æÞÿ©í^{­þП|uð—ľ m2ÏÂÚ¶­qg«Û˜|ÛcÔbˆÃ(sM0ù¾q1CäÐÚ´WÊ>:ñ—¾ü\ñ¿‰­µŸkI¤xgCŸWµ:eÄ1ÞGöûÈ€µÿK" ?ë¯õ­3öñ[x–Ö[˜´=CHÔu=wO·ðýŠH5‹A§Ç4‚I˜ÊbÌÞHéÿˆk >– jù3á·••ã?ßøoྷâÈb‰õ};ÃÒj¾TÃ÷2M —úÖS÷ !ï¸âƒ_/¯Ç_ˆ:f³aáMF jÒèÏmâÓ®SKµŠú ɶÜÁö¿7ÎÍ „;÷Þt9¥ÿ†€ø‰â "ßC Á}Ÿˆå¾»ºÓ®níµ¦\Cû7•,>P›Í=µ©œ}óéú+æO~Ñ_¾'|G·³Ñü#jÙ Ã)›ÍŠY¥>lF!ûè?LÖF§Óc¥ãß|oyà¿ø&[8b$ZëW›¦y:ÚØI.—69Î?{ëë^goûCøþÆßIѵY¼(uXøzöËZŽÊæÚÃOTy£o´A$ÒùÆ#3ÃçyÕ©‘õn(Åx±ñsÆþÖüpòÝxWÒ|á‹}jö×NÒ.Dú´ó ²c¶?kýÈýϤßZåtOŽuMÃë{¤øwI¼ñ«§XØj²Z,öæ+‹y¦›m¥¦£+È"1ßâÏŸþ¤u ©è=+äý/ö˜øƒ¡xbï^ñ%–­ ¼;â=RÖÇB´¹³’­"ãÈ–æK7› ¹äÿË¿¥|bøÅ{cá‹;/ú>¥¯ø‚ÓK°Õîíxg‰¬nîæ"Ò×R”(Âýùÿ[þ¨u ¨h¯”¾|oñ/ˆÈ.îæó~hð þO”1?}@GQ_6Mñ¯âêÉá+[ß \x‚?Zøa¼JšeĺdðϧMvH³ûg˜%ˆÅäó5s¶?´ÇÄ}3ÀWúî´ž¸¹ºð¦¯¯ÙEceu´ºÓfòD2ù³~ú)}sô ­GJ+æËÏÚÅ:x›D»Ôt-WĶ—=¾©èZCËoy5÷œaµ²Þ$ùûüª×Âß¾7ø¡©ø*‡¢›½ óXÖ`—O–y­õiäÚ˜®ÿr8ÿ¦µ©‘ôMG/ú©kÁÿfOŒ¿>1ÕüCá«=3ÂÚ†œš•…Ź‡Íµ”ÉŸ&@.æšQÿMŒPý+ÝåÿU-'°ú'ü‚,ëÚåW‡J£¢È"Çþ½¡þUxt¯–{(J((¨((¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢ŠNÿ…s—ZÔúÃí[H>×qe¤­ÄQgýafº>ÿ…rš¬º*ü)kö‹¨h±iqËuEæù‘ù^ëÒÀËÃrwÿ´Ž…4+©éwÆÒ=>Þî}V‹ËA%œ×_ê|Ï7îÚÉÆ?Ö¶#ñïˆ'ñ'ƒmî4ø´›Mqn£žÆà¬÷YŽßÍGŽx¥1`óž>‡¸«¥ë¿ ,¯4ý#Ò4ɾÏms“@bÉšX°qåÝ ˆÁí-SѵO?…4­ÃIq¦^Íqw¦N¶k ½½Ìqy˜ ))1ô1ÆGÓ·¶fbk?µ )áÓ{¢xvúûT} -jÞÚgˆlŠ[9n€”ù¼q?•uß ~2GñQº³ƒ@Ö--áI@Õ®,Þ+‰b˜Ã*E!çýbž½®SÄÚçÁxNóP¹Òô;­>æÖ-FK{M9d{˜Ì2Í cŸ(JF}ON•Ýx/Tð-O‡mtû¼>eÚÊc"1§ú¡ø FOÅ¿‰š§ƒdÕ—B±ƒPŸFÑäÖ.Äĸ°ŽÞ?ç¦ÙŽé½bÞ~ÒÖ:ZjK?†õHMœ[êˆe‡u”²Í,0(oï¼É"#÷YÆk[â‰<%ðÊ=dêÚDºŒzœÚ®­åÆ’ƒ¼pÂZo6O¹(wéÏz©wâσóß[^Ýb=ëý®E–k?Þ/ïey¤¯î³/Ÿ3ç¾hNý­­õG‚Úø¢MVE–°ZÙ‹©VÚ9¼‡—0™ýï­_ürjº¼ðè7:¾•èÒàk'Š9%"i.ß÷‡TºQŽ|Á.sÅGmã‚ý½ìIáë«Xß1Ê,Æ‚cGûÙ=¾cWuÿ|9ðl÷Z»o†—6Ãy}Ú£—išç2yÂe?ôÎoÈÚüuW²Õ5t›±dšV‘¨Ù[îˆËpÚ„’Ç ë0š¢>qÍ]—âôvþ(¶ðö£ Ý[M,–¶“M$ðɼ÷+)Ž7LÉþ«’zϲŸáQ›ÄwqhúL6vú5¤š…÷ØÔG5”ÂQ^ãtúQ·ánµ§è©e§.«y§KXM¼–·‚ <ãä™Ä'ã#¤¼rh±øyâkxvnHã´²Ô§–m7>e©?¹“ë*/ü ¼òË㦹c³.·áÉ$ÿ‰¥åžŸea<[ÒÞÒ2If”ËŒý?­z7‡¯4_Å¥žŸfÑ禮°–hFÅx÷@ÒÃéÖHøç¨®2ãÇ? <[¢]›¡¦júU­ÐººÝ¦É40Í'œ|ïõ]ü©¿}íב@¶øá|òkŽþžKHu+-3Kxï"ó/ÞæeŒ·#ÊânõŸûDéšÍÓ[ÙèÚ„×±èòk»hyµ!Ïúßùí‹ÿ­VŸRðw‰Æ¿…’'²¹¶±¾–âÒ;Ÿ·'— Öò/•æ“ÍèG#ŠYu¯„olкøyí…°œÄlÔ+È᱌y~V¦(Ö•ñsþûÝxvßzø’Ina7gÓbû÷8÷Ýý6½ðã^ñ'ˆô9.¼O êBâHÒÑn<Ðчáÿ*ÄUðo‡uó}¥ZGq«ê—çJ¸k2‚f˜·Hø¤|äÖ÷ï‰:?Äím_Eû@´Šâ[Vû\^Qó##GÙ»EÖ.?ÔÕáfÇÛ/8¿øÍcóù{ÿ}EÿÆk#S"ãÁ–k¥ÜÚhqÛøRöæìÒÿF²ˆMqÿ«‹ýWH{zU_ |4ðï…¼kàø´»[Ýt’ B8¦óÞIŒ²I,ê³,Ù?S]ÙÿËÝïýõÿ¥û ÿŸËßûê/þ3Z™ÃZ,·ÜÉ£i’Ê÷I¨I9±ˆŸµ!%&çþZÅŸõÕF/†þ†ëVº_øv ­ejwK¥Ú‰õÇH¦ýÏï€÷­²ùü½ÿ¾¢ÿã4}ŒÏåïýõÿ¬Lm?á¿„´Í1´ËO xzÏO{qnöQi‘E ¶ÂO0Eäù_ê|ÙsÇ­^±ðŸ‡ôï_kÖ~Ò,õÛÈÄWš´60Ãw6?ç¼Ã÷Çñ«c_ùû½ÿ¾¢ÿã4¿dóù{ÿ}EÿÆkS#œµøIà[ÔÖÓÀÞµ:«Ô–-"Ö/··™æfoÜþøù¿¯=y®±‰bX’Ìy$õ5Wìkÿ?w¿÷Ô_üf±¯üýÞÿßQñš J3xcI¸¸–I´õc$žl‘—”Å'¼±ª”ûÕûûu{K›kÛt½³¹M’ÚϱÿÏ*O±ùü½ÿ¾¢ÿã4}ŒÏåïýõÿ¬€ËÕ<á}nÖòßPð΋¨ÙÝÛÇowî— ÐOmú¨Èèa‡ÐÔÐøK@,Ñ41ÖÕì"H¬bÞÑÿÖCê¡íнö5?ò÷{ÿ}EÿÆi~È?çò÷þú‹ÿŒÖ¦FJx ÂÉ«XëKáM-WN‹É²Ô²¡[EÿÑn­n,ãÓg·—K†X'·OõvÄwŠùâjY5-55û}µyN­5¤·±Úù±y²Cÿ[þ§þ›V‡Ùãîçó‹ÿŒÐ~‡á-±ˆô]LÐáGnM³ŠÐyqãÉ‹ñå2\zvªZ?Ãoøuq£øCÃú>.~Þ?³t‹X?Ò¿ç¯ÿ¬ÿ¦µ££j>$Ò Õ4½b]CNd0ÏÑùDåÏJMoS°ðÞ›%Þ§¬Ë§Û)Š6Icë7î¢ÿ–5©È|%¡[Ì>Ò¢{qp±y|_¹ιǧšy—þ{j®ðãÁÞŠt ø{Hˆ\›Øÿ³4È¡ò®îŒ£Ê‹‰?yþ¸VÛYaˆ7w ƒŒù“ìƒþ/憎øÍdha7õΛsÿŽ%Öžû>_ì‹_:ÓxÄ‚#å~çÎã>´Í;áO‚4]>{ 3ÁÓtÛ§K™ì¬ô{X-®$O¹<ÀCÖ.€ÕùuM6-vßD“Y–-V{Io#´ÇæI gÊsÿM«Gìƒþ~îsõ‹ÿŒÖ¦e¢KI$“’OZJËÑuéPjš^±.¡§N²¼3G,^IþçþxÕ¿±ùü½üâÿã5‘©-Ŭ…´¶óÛGwŸëaš.µGNÐ4ý2fžÚIš/%g’æYbÿž1Ôj³ö1ÿ?—¿÷Ô_üf²ùü½ÿ¾¢ÿã4Úm…õí¥ÝŽÍý¨‘í/2{q)òæòü±V)øqàÿøH®ü@|% zæ)#ºÕJ¶RÄ”²ˆ¼ÓÐqíí[ßdóù{ÿ}EÿÆj®‰¥K­Û½ñ×u;h Ì±Ç +jò¥ò‡ü±ÿ¦"€¤x[Bðî÷Ò´M3KgŽ;g’ÂÂ(¿uú¨¿uÿ,aϵ»zUQáŒø¨õÊßÿ‘iá¸ÿ¡Xü­ÿùµ2,Õ+ýËR¸óîat¹ÿUö»ifµ—þÿCRÂ#qÿC&±ù[ò-ðˆOÿC±ù[ò-dÐ[Áeo ½¥´VvŒCon0zڰϼ2úÍî®|3¡K«j1}–öÿû*uuüòšlfhz~Bµ¿á¸ÿ¡“Xü­¿ùøDn?èdÖ?+oþE  ÝÀ¾ðþ›¦éÚg†4-6ÇO—í6–zT0Åi?üô†/ùc-cxËá'††ÝtHѾÉlö–ÿ`³ŽØÛÂòù’E•Ò4tõÏÔèÂ#qŸùõÊÛÿ‘hÿ„Fãþ†=cò·ÿäZÔ ÍÁÞðþ£}©è¾ÑtCSù¯µ ;N†®ÿë´°ÿ®­‰ÕKQÂ#pædÖ?+oþE£þú5ÊÛÿ‘k :=Ä¢Çþ½¡þUtb¹hü5mQ§Š5Ÿ-?uîìùìMGþ†ÍkþýØÿò-y_Q›êiÎuY£5Êÿbj?ô6k_÷îÇÿ‘hþÄÔèlÖ¿ïÝÿ"ÒúŒû‡´:¬Ñšå±5ú5¯û÷cÿÈ´bj?ô6k_÷îÇÿ‘húŒû‡´:¬Ñšå±5ú5¯û÷cÿÈ´bj?ô6k_÷îÇÿ‘húŒû‡´:¬Ñšå±5ú5¯û÷cÿÈ´bj?ô6k_÷îÇÿ‘húŒû‡´:¬Ñšå±5ú5¯û÷cÿÈ´bj?ô6k_÷îÇÿ‘húŒû‡´:¬Ñšå±5ú5¯û÷cÿÈ´bj?ô6k_÷îÇÿ‘húŒû‡9ÕfŒ×+ý‰¨ÿÐÙ­ß»þE£ûQÿ¡³Zÿ¿v?ü‹GÔgÜ=¡ÕfŒ×+ý‰¨ÿÐÙ­ß»þE£ûQÿ¡³Zÿ¿v?ü‹GÔgÜ=¡ÕfŒ×*t[è¿ælÖïÝÿ"Òb_ÐÛ¬ÿß»þE£ê3îÐêóFk•þÄÔèlÖ¿ïÝÿ"Ñý‰¨ÿÐÙ­ß»þE£ê3îÐê³Fk•þÄÔèlÖ¿ïÝÿ"Ñý‰¨ÿÐÙ­ß»þE£ê3îÐê³Fk•þÄÔèlÖ¿ïÝÿ"Ñý‰¨ÿÐÙ­ß»þE£ê3îÐê³Fk•þÄÔèlÖ¿ïÝÿ"Ñý‰¨ÿÐÙ­ß»þE£ê3îÐêE-r¿Øšý š×ýû±ÿäZ"–ïCÔ쬮5N+éeŠ)§X¼ÈäòüÑŸ,Gû¼E/nN;VSÂN˜ùΨt¢Š+ˆØNÿ…r×Z:ë¿-ôó©]h¶óé±$·¶‚68ü®ÖÇ$¡ÿ§¿á\oˆ´ ¯ü(—IµÓì5)o4•¶[MQü»R C‰3 £ÿ!LW¥€ûf ôøá­[C·‚úÿR×ìå··}Ü‘ƒÊûFfEïìçá­C¿–ëÄ~ ²±‹NR] í·ZéñA$BܳñŠCÏ2tçƒ]Ãï„:ÃÝN÷RДˆo„ÄÆðZíQ,¾l‡ÍŽ,™by–FþµäšìÑâmk@}.òëAÔ~Ï¥A¡ZÁw4íØlî`ŠòPcæ_ßG'“؃‰ºÜü.ø?uðûÅšÞ¥|é¨Iv®ŸÚ‰y žó̉͜<~XÿPŠ) ²¹Ç•ÊÕøW¶óxš_ë"Ù¼El¶É#ÍOoo ™Qg3Kžy'Òªj¿³§‡5‹ýJâ]CX5¯µÞÛG4~LòùÒK?»'÷RJJú`g8§øóá…÷Ä÷$€Ûµø#£ý¢úÊ?ëó*izu¡²2[fÚYe–ÒU?fó¾g˜rIç·®ê¿bÔï¢Þ&Öf×<˜É¸ºû;Å5Ìk ·º–(ã‹æˆÉÒ#8Zä¬~ø†; rÀC¡Z[ÞèzN˜÷¶—2‰.¤±Y×Ãå Å/™åæŸÜŒSÇÁÍz×Äzf¿¦é>Òî¬m¢’ÞÇOšOø—˜–\YÚæ(±¾l~iÌ`íÿUÏ ™ èZ,:f—¦hº„±iÞo±Ïmk,{Xòn=p%àcœcázÀ_ xÇH6z‹ÞúÎþàéÚU¬v³yPÚÅ<&Ô -F$Š9%äEçEåeë^ëá? ßxCEðþ‘m5­Ð„ÊÚ­ÌÀù³K&e–hñݦbN}}«Ì<]ðO]ñ]Œ6íoáíJÖÏ_¿ÕmƧ$¬dŽê9²ÕŸ*Xe›#Ï”0bÏ @uºÇÁZ†þÔ5Qgw%¤ÓZ„¶–šÜGR…’s²«ëÅcIð/Âß<=qªC¨N$Óô94xçÖçìëb ¥#ÿR÷˜õ' àsz/ìË}¡:ëw–Úé’öÒâçX»ºwŸUX®â™’{sÞ#ˆEóI6@ãÊéV?á5(ü=©hw‘èZ¼—ÚGödZíñ‘¯4¸¼Ÿ,Âbàð%‹½0‰ÛxOᾑð¬xi§ÖÞI,,WF´f»žO6êãÕæ°Äúƒêk¿Ò5Í?]¶{.îÞúÚ7uf·“?¼¦:ó¯|.¼ÐüOl“‹hü3¥O¨]i¶–´,—sD`ü˜Œ‹ÇïÏ¡®çÁþ Ñ<¤Ë¦hVQé¶RO%Ë@æÈr:é+™ñoúÍ+þ¾þ‰šºjã>#ÝɦéI{úÛa,ŸùZ`¢¸8|m«Oñø[욘ýª|!¨‹›?ê…É>_N|ßõG°Ïš_‰<_ªÙhÓ öÊ]CN¸¼0Þi/Ù¥ŽQ˜eýÿ?»ôÁ¬¬ks½¢¸ ë¾ ñG‡ïîí´íBÎêÎ_NÕ¼?qc3M·ýL~lÞ\£Ž±~ç&³SÇzßö‡õŸìMgìš•ÏÙ®mÏ…oÆ›1‡8–Û>o—Ç•æÅû¯ÀÐdzPéEy¡ñCÅÚo…´ýfx…®/tµÔ£Ó­<9q-ÏšdÜm%H¦Û€L¼u®’x‡Nñ\ž¹Ó5D™Œkg{‡..¬çó:°š9¼¨„_ô×Êö¨Å«j:6­¥ÙËys­i0ZÛ¡y&–ÊSåÄýv®.ßÄþ#¹øaaã+; nþ×WvúD:d’ß´RÊ<¼Ççc+óõÒƒ[èb¼ï]ñ‰t4Ôô=JæÎËS[»°Ð®n‹Âð‰MÜPÅ7›,yýÎ"÷ô¤²ñŒn‘æx[R…ï|©—Fr!2]ùL&"oÝíÿ\qÿ,¸¬€ôZ+Ìlþ ø–GÒ£½ðæ½§¶¥ciuj…®.FePL34SbÖX3ÞùU¿áíkZñ þ'ŽÆk9!ÐîšÁ®…ŒžUÜ‚1$±Ä<Þdù#èh°\ëÇJ+Ít_ø£RHe¼ðö¯¤Û4BG{¯\Lc?d‚lb›Íÿ–ÆúëÕ“ñ^×ü{¯Yè:ãKiuåÉ¥\ø^öÊé¢óš3,q\˜ŒŸ¹oî±.p=«S#Ó¨=+ÎuoëZ~‘¥kI¥kWúEûJ’4^½ûU·—œ mãçË›+Æ´ôo_Ö¼A¦h3[ǧjSèé­^ÁqbtÅoÝC‘6 ¿ë¹~æ€9ÚÂkø>˜­àAãÝ âM‘y¢(ÅÉÇ=¾µà°ê¿4_iú–¬x«UÕ|E¡ëWZŒ7Vßk}6{mF£’Ò##›ì²Íˆxó ××]yÈhŽI|þd?óËІÝ]ø’íÔ¶Œ±¬2M§ë&ñïà­nËK×o£š[·¿ûE¤Æ1˜¢>iŠj|MÕ¼[â[W‹Ä,Ó~I®ZÁsªèÖrÁ5¤#Oùú¡,0ý«ýnb×½ø;Æž2ñ¥ž©u…©i‰ikáuâÆynÆOÙPM?ïã÷Ç1dôÆMW?üD<+¤xˆhºëØÜ0û\Mák߷鹇wÏi»Î”ç÷DÄ%úšÐÌùRÎ÷⇾ü1Ñì–{@ó¾•öÏÞÝ<=&½Œs\b#­}5àÍS]ñmζºv¡ome¦jSi‹s%”›.^6Ä­ó¸°0ƒþÉ®œøgÄg?ñ8Óy9?èò}×Ph|yãyîÍÂj? õŸø–GðV·e¤ëº„sKv÷ÂâÒo/&8ºÅ ¿O¯|PÕü]â}SW‹_ñf—ð²mvÒÞëWÑìæ‚kX?³å "Wû_“æþäW×ÏáÏÈImgObqœØÊsŽŸòÚ”ø{ÄåÃoO.}†\üA™ð•÷Ä/|9øk£ØkÚ§´£_Ïi«OÙÚ'T˜Æn„V’‰È‡÷¾L¦/8Ÿ\×ß %TŒN¨“…!Ö?õK.zS×Þ&W.5­<9êÂÆ\ŸüQÿÂ)âú éŸø/“ÿP:QGü"ž ÿ ®™ÿ‚ù?øõðŠxƒþ‚ºgþ äÿãÕ‘­Â«x._ø§Çý}ßÿé\Õgþ?ÐWLÿÁ|Ÿüz³¼Ím¢H“J“I¡æy_õ÷5GEæû <ßaUüßzO7Þ€,ù¾Â7ØU7Þ7Þ€,y¾Â7ØU67Þ€,y¾Â7ØU7Þ7Þ€,y¾Â7ØU7Þ7Þ€,y¾Â7ØU7Þ7Þ€>TýŸŸÇ?´-Þ£ñ'SøŸ®ø~ÎÇÄ·v6¾Ñc¶K(­m¦ÂÇr’ƒ!–\NAç¥y¯ìÛû\ø‹áŸÂ-/ˆ:,Ú–›q k^"Ó5¤ÔÍÝåù±›÷ÑÏÜÄNké7ý•>Â|Þ.´Òµ /T¸½Šúâ +Sº‚ÆâëLö±*CSé_³Ã}"ÃÖqè’ÜZè:f£¥ZZÝÜùн½éÍßš?å¶k_²›Eûhë^Ðü_7Ä/Å£kšG‡lüQcc¦j‰|—°O7‘ Fl~îQ4±v÷öu_ÚïÆ>ð×Ä‹moá®ßø> ¶ÑôE¯mî-ï‰òçóc‡8‡ÍýÖQÅwÞý–>x;Ã!Ñ-´)5?^µ:„šÅì×ÓIiÛ²Ï1>\0Ž‚ö^ð'‡|!¬ø{J!Òÿµ®­o/õ[O]¦©q$_êÕ®ÁóŒQvÉ /ñÏíÃá x´ CñwˆüSow|¯£jwZrCkþ·l‘Z™Œ§þ¸Õ¯Ú§ã'ˆgýlüuáùu¿êúœšt­ ²y:…¿›/•,\t¯@¹ý•~ÜxkÚE®™¨hË Ms>ŸªéºµÕ¦§—'÷² |Òf=y®›Åß|'ãχÖ> ×m®îôK3lÊw+ÜHm¿Õy²ÿËj'Ï~Ön4/ˆฟâÝÁÖ¼G +ñQŠÂæ/+O<ý–€–#øw®/á¿ím¬ü&ýŸ>iVúU׌üSâ1«Ý}£S¹»?º¶»›'̆)¥?õ¯´¼Kà=ÅÞ.ðÏŠ5H'ŸUðÜó^i¬’ùPA$ÑyGͯ7“öBøj4[-6ËMÕ´˜ôûûBÂûHÕ®í.줹ÿ]´ÐœÅo'§PGEý®u}oâwÃïËá;? é¾(Ó-/š_Íes$ÒËåMihDM,?óȘ«7ö›_øçöøUà}i§Óe°¿ÕnôØüE>Š—/D35ÌK)1þ«é^©qû7x3Sñ'‡uJ]T—A[Qea¯ÝÍh“ZŒC3ÂIÊ9ýñ» ÿ‡z¯ñ Jñµå¼íâ]6ÂãN´¸is“ps/î¾ P‰h¿¶&±«|HÓ`ŸÁñYü>Õ¼[7ƒ,5ó¨ÿ¦K¨DLAͯA‘OãŠíÿfï*øå¥jÞ!Õ¼7¥øÃîæÓK{]DÝÝNñÌb—ÏO'ôíùU­'öiøq¢|KZ莚ó\˨DŸl”ÙA{7[¡lr%#8 ë~|?оø]”õIÌNδy¾Â©ÁVÐGon‚.F?ͬ€³æû <ßaUüßz<ßz±æû <ßaUüßz<ßz±æû <ßaUüßz<ßzŸÍëÀ¬ÍRLø‡Âßö›ÿInªß›Y—ÒÿÅEáoûÍÿ¤·U¾;1ÒŠJ+採;þÈx’KȾ\ýƒíŸÚH ntÛ2o3ÊÀò£þ•×÷ü+’ñºþøI.©o5¬YéH滓ˈ½,Û1g› øªÏ§Þè†úVÎÎÞ=S·µH¢ÒëÍ–_Ýy£÷Ékǧ}]MÖgÔüâ cñ±-´·V××ZÍ”6÷±¬¶ÜŽ(¢_˜aƒúfÏû@jÒYÉ‘o­é¦ØÎn¬¯ —sKqiu7É”"Î-—¾˜zt­7Çúöµà ÉüA¦?U’êÚâßGºK›)˜Zù°âY"™8Îu⽩}£3ÎõÛŸ‹úç„/4éãñ¹þÈŠÊäÙÙÚnõgr&òÏ“…†iŒ'ÍÿÑè? SÇÑx£S_êw2ÛC±¦žútQÁó“É’‘ÌÃÊÈ“?ÅŽõÃëÿµ.¥qáK»*×I²Ô×AŠöYî¯IŠÆí¬î§0cÊù§HWÖ»¿„5¯ø–ÿL¹Ðc°±µŠl^6£lÓ¼‘ÌÑ:Ù_̄șÌ` àœã*?H­ñ_G×|/‹¢ÒF£mý—£Éc§˜‡—ç^ÜýéãÿžžT^Pã÷¬:×3«KñŽÞöâÚÊûZ¹0Í,ZmÓYØyw­ÜÞt·Ÿ¹Ê7”bòDF7N¿ã޵í5|H¾º†ðþ-ÝÔ¯›ºòn-cÆ{/›!÷hk—Õ?i-cE»¿´“Ãú`º²¸6²Ä5 Aº—ísBM®aÌ¢ˆK)ÇüµÇä $øëi5¬Z‰uˆ-¶Êòj#D´ºçó¥Xã1EP‹lž¹ WGâý#â&°—¾"ðÕî«¢ßjŒÖð,1ÂòEckË1ÜÃ(ýìždÙûÐÖ.ûUx¢ámm[ÁZTš‹‰nƹ6¦8ç1`Iså.cô®£Ç¼EáË«íWJ±²Õt©5£ÚZ_J`5¼3Ku6ÿ(ã$yXéû­i (ÙÜøÿf¼Ñÿn¾¯&¢3XÄʳFó@[·—åI/—/Kÿ-A:\ºÔü¤ø’ÒXgñ-ÿ‡m­ÒFûFk›«p%ûA—ˆ9’sû¯*(ü¾œƒÈªúoÇ«™­µ»âúd÷0è.¥š—‡÷/vóš_Ýù¢8¸n˜1 àZçã­ÕŸŒ´ýxtIìÜ[ý¢ëOÔÌŒÂO8´Ðþìy‘EäâBHÆ{t £Âpkð•ŒW¶—3ø“[’kÛâ™ •̱™ ?<Åòá •âËuñßJðþ ’jZõÕäÅ%½ôš~œJKäJLF(¡Çúï&<ú ×¶xKƳ\øF×Z×'ŽrYnôÍ6LG*Ûù&hàÿ®¢̇ÐçÒ¼ŸCý©üA>yuªxRÉ &ÉV-6öid–{¨¥6‘ÜÿËY£†,ÿÓa@WÞñ¾­u®BÚŸˆ­ç¼×t[ûYm¾Ëiû/Ú<œÂsåH³~êlôÏ5>âŠ:Ìz†½e«jZU¦ŒË-¥½µ§ŸsªE2L_òÆ\¦?é¶OúšÆÔþ,øÇéãxfÖ4ëßXA-ìP-Ý´ºU­¼sEQäÇö˜äĽ%ÏCÏv×?®?á%—ÃqhÓêÆw†×eç‹¡p鑘ù_Nhš:Â[Û;Þ\ø–ýo´c-Æ¥mo$žEíÔ¹y˜Œô-+cÿ×^ª:×Ïþ×<}|Ÿ ÷ˆmžÛ[µ“WÕ¤ƒÊN¾BÉåEþ‹Ä9— Û/Oßs»e¨øûÅzMž½áÙ4±c¨ëFeMF/õzRþíXc©”,“{yÀU=@öjá>,ȳ7ýq»ÿÒIkµÀÉ®/âÏü‹S׿ý$–¤éž<´¼ñýÿ…àH¾Õab—“–T›,ØÿTcýâð?x|c4Ï üeðgŽL‰¡kÖz‹¤Þ9‹ †9|©%Ϧk"ø¹~1·‹$}ûô¯ì¥¶Y%ûHŒLdߟ/öé\¶‹û6‹o èZ%þ±çYXøgTðíÌ–ñbI~Û,24±q÷9ü(û tš·ÆÝ2óá߈|WàÏìÿáÚKp..¥´†XãˆË˜åËœùc =>•×Áã-*tðÜ7W gyâ<Ë+IŽe—yÒ¦=£þ_Jç4¿ø›Vøi«ø?Åš†–Ð\iŸÙp\hÑË‹ˆÆd5=ø¬'øgâËí7áõÔšŽŠ¾(ð“K·“ µºŽ[G·8çÍñ¶N>œõx÷ö™ðO¼ ÚújÖº‹M¥É©é–˶ƀ=12Ÿ/žþ½ºÉ~-øRÏÄö¾¹×--õû…£°vä´™òãã¿øWŽÜ~ËÞ"ÓþMáÍ#Ä:bÝj> ‹Â:¥íí¬›YQæ60ý=ÏÁõ†õÇìñ=¿Å ÝvÝ´­CIÕu+]VîßS{Æ’  †(T¤i/•!yqƒ×îòD²(Fz òˆ?t¯ØxÒd°–ôxV;I5 ™ã†Ü™WtbBÖ˜JxÇÌ9ɯNÔ¤º†ÆæK(ÕÒ'î¢i<±#ûšñÑðJëQø)}á »ëx¼Q­Í©¬_æÆ÷’ÏÓ°DyŒÇ5ô Á¾5ø!<;&º|Ilt¤º:sJ¨y»˜Dxó<ßöqTu¿Ú/ᾃ“%"Vµ–N™ö˜}F¬wàÆ­wâKÄÚ6³hºÀñL^"°Kèåû:lÓF$R`ç%|Üc¦Gá£~Í·úSiN5ød¹·– ›’Öîç]WûF_,gˆÉ>^=:СOñŸÁWÚE¥Ç‰l#ºÕa†êÐ?ÖE7ú™Ëó;f¯ëÞ>Óü5â AhÞKÝIn¤ÛvðǾ[‰yá2a_\Ì?Ñ¿ek¯®š‘Ï£ëVRišUŽ©«öՉ峷ò|Ô…&Ä þª^3ëéš5+ÿøãÄšÊE§«Ÿì½=7y†ÓO„a㬒™¦ÿF:Š%ýÀàÿ¾ø‡$‘xoÄÖÚÌÑÚÉxa´Y7ù1íÉçóÖ/Ïòµ£üVÐ.¾Kã+ëË-+@†K…{¹n3qÇ+D Àêvÿ«õ8ë\7ÃÏ€ºÏíBËU¶Õìõ+ë+Y-âŠhäŽ'“û3M³óÀó4Üý&=éšwÁËð¼èž§¥Úê6:ðñ6}j&–1sý ú†ÛˆÜÅ”Ü|¾ sÁ öÿã_‚´¿Yk:ôvöWwc…¤†c4’äþïÊÇ™ŸÂ­x³âá?GâdY´¼{XmSI1Í%ùšUŠ(â笯N}k“ñ/€£áýSÅ7šÖ§\ðõ¶‰,·W7R+Åç n ×LX“çqô©‡ÂOÜ|9ðg‡&Ö줟û-%·¶º»³‹Q·Ž×ÊÍ–"%Ž@q/æ€;Éþ0x6Πé¼Ic„Údšº]—ýÔ–‘‘æL=¹§ÝüJ𮜺´·Zµ½ºi/j—Ë(ÿw¹Ç“‘޲y£ó¯“öRÕo¾x+·¾%´|!=Ʊ§\ÚÙƒ»VûL’[K(sͼ"C¾,4õ#­wÀŒÿh¿ j6·úV›¦Çqâû<‰e¨^[Kæié3É’iæ±{œP¬j^8±Ò|k¢x_ìÓÏ©êpÜÝ¢À™Xa„¯™,‡·Í4#²ÖgËþø¡¨øŽÇD’áåЯ •çŸncÙ/•½ý¦­Uð„ï­üUãk© š†³8µa*Á¦ÂX[¦Ûó&”W®›Fðf‰á‹›©´½.ÏOžèüïk‹»Š< vÄ1ÀPIEPEPbŒRÑ@xŽ·y§}¾ÚÐÛíŠîõÑ^ %–YeÔ.ÿuô¯l¯ÐøñŹïý©uÿ¥Z\þÓñ'ý§ÿÁl”iø“þSÿà¶JßÔŒ³jZ£y·f8'Ž(¡ŠêXGú¯jñÿˆ?¼[ ÜAiàE‡[Ôe¹Ž)mõ¼ß;ޱJæ­^ŒèÃÑyrS=ÃRÕõ–2ÎÖâ?õ‘]Z˦¬j7:¾›m-ÍÅÆŸQÿÓ kBý¿â¼Ô Œ¤Z‡OQçMU|H1¤Çÿ_vŸú6² Á©ø“ò Ÿÿ²Rÿiø“þSÿà¶Jíµÿ /R{oÝŒ$C2ÿÓO;ÿŒÖ•ñk¿[hRÙÂÞ`óa“þZ„§úÖÆF$^#Ôæò»ÛÞ<ƒ&;;Ye“Z?´üIÿ@©ÿð[%[øsÿ#V½ÿ]nôá=\øgñ‡Ãßôûùôë»xn,ï.ì®,â3,fžX|Ãé“ùÅdÿiø“þSÿà¶J?´üIÿ@©ÿð[%v‡ÇžëG_Òÿ²•ü¹5·Gä'°—¥?Kñ¦…®\Km¦kš~£y~aŠÊò9eòýý(ˆþÓñ'ý§ÿÁl”®ëqËÚíÅ„RIån¬dŽ*èu‹Ð<;.°Ú”º¤7"Íÿ°,æÕf2ÿÏ?*ÔK&•gêÞ3Ò|ð®ËÄ: —Ú>§5œö—Q ˆL ÔX#Ì Zˆ×ByV-·?sçÚéòË7þUçüúÏÿ‚»ªÇð¤ÞNkþôŸú6¯ê:;™§Ýß\Êb´¶Ì’_ZÈÔ³ÿ *ó?ñí?þ ®¨ÿ…‘x:ÚÏý®«+Ã>/Ó|aá­7^Ñæk/T€^ÚÈÐË –Ó÷2ô­/µŸS@ŠmÒ7ŽçJ–)"ób˜ hÙ­ÿÏÆŸÿ|ËMð`Á~ýiÿ¢a­zÊÙ­ÿÏÆŸÿ|ËIåëgþ[éß÷̵­E 2¶kóñ§ÿß2ѳ[ÿŸ?þù–µh  ­šßüüiÿ÷Ì´lÖÿçãOÿ¾e­Z)•³[ÿŸ?þù–šßüüiÿ÷̵«EdìÖÿçãOÿ¾e¥Ù­ŸùxÓÿï™kVŠ@elÖÿçãOÿ¾e£f·ÿ?ýó-jÑL ­šßüüiÿ÷Ì´ÖÏü¼iÿ÷̵«E 2¶kóñ§ÿß2Òyzßüüiß÷̵­Edùzßüüiß÷Ì´yzßüüiß÷̵­Edùzßüüiß÷Ì´yzßüüiß÷̵­Edùzßüüiß÷Ì´yzßüüiß÷̵­Edùzßüüiÿ÷̵ ­ÕÜž-ÐíîÚÝ¥ŽôÉûŸ{K±[ë Èû£ÿ×Ô_úI¨TÖþˆ:Q@éE|Ѱÿ æåÕF‹ðÍu- ÿÙô¨¥kaÿ-1Jé;þÆëvº,ÿ ÔxŽóì^M.3}'Ûd³ý×”3ûØäËü1øW¥€ÿ—† Ê޾Ó.í`¿Šm6eo}-ñ·ÿE‡Î‚Yyž¾\2öôõ«>>º¹×<1d<4t½7ZûIQ&Úâ b‹ÍÞÖÞYžþoõRÃÃ? çMС›O¹Ôfµ·žßN¿Ô庻x£†Q1K!’HÄrÍÁ÷ãÒ®…7ë»ÏéZMõΰ¦{™tFËZº¾†9c‹Çö¯8“û¯ùe’:ñÞ½³2¿ˆh iž{ë&÷Vºm.=r+­0^ŠYÖOÊ=ò+oá÷Æ? üBñöŸ¤H©ÛÂd¹hU )ü™có;ùRñü«×<ðWÞ¿½Ôî­ítk‹H÷Éÿ É “(†(Ú3åÉ ’í†/”†<ï|aá-wS‡Ã:Š5ìiæ^i0j²H¶ÆRdmL¦8wç#lkÔö $c|BøÃVÕÅŽ‚š¤ñi³kZ—–ÞVB‘;¿t|ÙfÁ®@‡ØU[ŸÚÂv’M-Åž§Ù‰K×k }‚C4°¬Rc¼’C0ïøf¯xþoøDk³ø›íRÿoÛ›@Éæ°K[h•~o/T@¾ý5œ÷cnçÊí€h:?Ú·ÀZÀš€½´šWvK-FÕ#DG/—ž“¯5±âŒZÃûíFÉôK‘¢é“示E¾ò.Z)gž/(BÅIL?{õÎ~Ÿ¢|1Ûjšn»¦A³ 6 í7ÅAæÏ!óE¨–;Ÿ›y“p‹œç8cGž´ÝxÅQøÇE}QmR;G¹”YÈÍ70ÆÞX”ýk“âÏ…üo¤èZœ:õÉ]KK–Ñ/c’Ø ¼“ʵ»óÒ3ý+¶Ñ"ðö·ah4Åhlü7s-… G’8áxÃ…Ä€ ×ü|ŸÃ^ø-¥x+MÓÅv7ösiÈu oJ¾uÔ\[b¹ýØ/æýß^8ÅwÚŸÅÍKþЖ=SžÚ=F=,Ý[X~êææI¼ƒ‘æ2C“Ó“øË¢|hð¶»m¥Åsq<“› KC†QiöÏ+þý`ýkñö“𮩦©â&Òç‡X°»Õ"¶×nsi3\Åä4Ѭ¸´ó•ûÏÝžG'¥]›áÿÂHô™/%kK]>;3jdṏ¤µå‰†%6²ÿ®âRs޲ݗīOé~šÃE¸»#i£ŠÖù _èQç͸ly Æ~\ùkçEÏ5µðÃÅsx¿Âþtž¼ð°¶¸–Î=:ñl±ÄL`¦:8¬m/x`hºÆ“y=ôÑ®‡¥\Gw6¥Q!%c™qþ«<œ~äzWWàßh~>ÒQÐõÔ,ḒէŒpdˆâJéë„ø±ÿ"ÌßõÆïÿI%®î¸O‹ò,Íÿ\nÿô’ZÊÐüY¬?Æ›ÿ _Z­½„zWÖÒE~Ó&có#0.Lùë.Epþ ý¨¯u}S_ðµ¾au j:í±²ÔšúS”«¢T0Ãåýáê3ÇcŽâ/†þ_‰Ÿð‘%æ¢|^Ö¾k@Þ#½cö]ØÇÙMÇ•åy¹ÿ–~_µhé <% Å§Ãe£BcÓ¬gÓm–I¤›m´îXO™!ßæ9lôëŽ)}8¯x—ÆZ×Àë·öÐø'PƒD—UÓ'Ñ5ƒ{:‘’Gæ™lñ áe}0zñM>çán•=²^KâÈÌ¿2ì1ˉ¹ó_òÓ&b2â)±“4ï«æß‰ßüU?Ãÿ‰¾(ðíËÛizEݦ—£“5¾•yc&•i „"ËU½¶CýÔSˆåèúkæwë[š€¬<;/ˆå2É{q¯_Ë{}5Ç%‹F#Hÿëœq*¨ß…)t*øEûGx‹âv³ka{à» [ËžÚDñ ]$Òý–ÎëÊ8µ/•{ïyÎÍ_Ѿ0ê:wÁ‹O]ém{®ÞkÒèVúöMxuYm#OµbýÈŸ(b!Ð×e¦üð_‡ö ;M›Nxâ$¶ºÌRÇÙá¶Ç˜%Ïú›H¯HG~k3Ãß|oà]WBÒmn5_ êWRK47šåíü~œd‘â–Y¤1¸—qýÖ˜|À­âO‰ž.ÐoñÚíÛÛé|;#6—=ß–f¶¥AsŒˆÇk1Áé^°|'¤É⨼FйÖ#¶{¹óä(!r$+åçgPqøÖ4Ÿ ü1.·­êocpnõ«?°ê[/î’+ˆ¶yy’/7Ëó<¼7n;ÔKá)|GŽèߴ޳ះMâ I>¿…¡ßÛ[é—myý¤u £µ‹”‡÷rùÙ>Z‰r9ñ]—û@x¯X´Ð­×áü¶:æ­â 4!mªÝÝØZ¨[)®ÍÔR\ØG4±æ#ú‘Î?³MýŸü¡øz÷B³ðöí>úÖ+ ḽ¸¸o³Ä»bŽ9e”¼qFF@àUëO‡žðÄš¼ŽöÛPkÝ:MKWºº¸k³ ±“æM)’cä™vIxî5‘18Ýãεãk_ZøgÂÖ7ºîµ¦Þj’ÚêZË[ZÙÇm2ÛJ>аJe>l¼b.œšÊ¿øÍâ |Vñ”wšd·…-/<;e6ýDÇ=„·Ø‡÷QyGÌýì°õ1¾•èW_ü{¤iš`Òå‚'íÂK NêÚæÛío›J%ýéäóÍi]ü&ð욃Ühгê/g%Ùºù¦Ðÿ£$ãÊÀàwõ©.ê:¿Æðî“å?D²7šìÂ1!g—þ=m‡pøóf$r<´òÛŠ ¼]ãßkÞ(‹Æ‡ÃööWžNœÑLÒ‹…ò¢$’8ÿ–‡¯\‘ÿ,«¬ð—ƒlü%q¬K ’\Ýë”úåÜØó]™³gþyÇX‡²Žœ×]@Q@Q@Q@x~™7“âûi=uK¯ý+ÔkÜ+Ãtý>âûT¿’ÌÄ·V·w71yÜÅ'üL5Ññ„ͤøšÂ×XÒ´m^å£0Oª¿—Pýî*†‰á¿øKGuÓõ»-[Y¹’Ö69!Ü K"Š!ÀÕ²òøøm'GØLÿñšS?‰H ézAþ¢gÿŒ×4èB¤ùæiÏ:pä-~×ãÍXÿÔ.ÐÿäY©¾+çHoúïkÿ£b¤Ñôëøõ+ÍOR[e¸žÞ+hâ‚O7ýP”íj·®éÒjÚ\–é/•/ü²­rþ>Ôµ»MskáËÿYÍg q‹…L2E,ÂoõÓCÞX*Èð…®µ}ãÍ?Qºð–¡áË;,¯s|ö„“åKÿS,¿óÖºóqâ¦$ÿeé@žp5)¿øÍ'âŸúéø2›ÿŒÖ¦EoÜù^ ×_þzIuÿ¥sWƒk_²¿ˆµ_Ãgd4Zº,PÔà.Ymõ—ì¾l‘~ön 9ú}+ݬ|=«hl.-…•ÝÄâAt“ÉäƒæÍæŸÞù?ôÚ®yÞ)?ó Òÿðe7ÿ¬O µø ¯ë—w7÷~ð÷…´ëïxzýü)i43YćíDY“ÎìÄ#œÕm3öaÕ´Ë=:ÛD‹Ið¥ÄÑxÆÊïSÑÄVòÅo$¦Ä$~ô ?õ˯}ó¼Sÿ@½/ÿSñš<ïŸù…éø2›ÿŒÖ¢±óß‚>üEømðûTÂZö‹u%Òô‹Ûˆu«'Qc˜<öqÇ0¬¿¼Ä^oüöì!ûF‘¦ üÑôdÑ?áŽÎk8Fö¸ï0.¢ÞXÿÖË(ýé=É眚Üó¼Sÿ@½/ÿSñš¯ya¯ë" [ë{ 5–)¤’ÞøËþª_7þxÑÎG)ò×íàíSâOÁðÞ‘g>¡=Æ¥jÓØÚÜùrKmöøüÌûy~oÿ^¼[Àÿ³Ÿ‹¼ãt¿Ò´Kí>ÊkßXJN®)´É­±¦CûÙ{I_qKð³^²- …Ö—-¨“÷Kuq,Sü…50|;ñ?gÑð2çÿ‘k#Cㇿ¼e ë~¸ñ?ƒ¯|C-—†ô?HÔ!ÖbŽ? ÞB² ]¡ó—»9Oõ?A[²¯Â|=ñáÔu"÷ÃvqèOe¬Ëu¯h¯ˆµ_7÷w¨žoîÿu_[ÿ»ñG÷ô_ü ¹ÿäZXþø ¬mÿÛåÏÿ"Ö¦\§ x;x{þÁŸú&ÕªúVœšV™g§Û¿™mgo°”÷òâòjÅdjQEQEQEQEQEQEQEQEQEQEQEë Èû£ÿ×Ô_úI¨VçzÃò>èÿõõþ’j¿†¢”P:Q_4l'¹CO½Õ¾Ecc}™-Æ”±Ë›_6(Á‹ù^d}>£ð®Ÿ¿á\Ž»§Þêÿ f°±·¸»¿¸Ò–(â¶½6r C8«ú×¥€ûf â¿áž¬5½)!Õõ…»Ymìí¥¸Ó­~Ì'Š [˜WËdcºo˾xßÿ„VûûGÂ1j>(Ó$×´™¥–M8Çö›S—,B/4âM„~ðp9ýÕq­ðÏâïk©[kwš6¬–vvËæj’ÍmoåÚ]C$’CŸ*Sæ´2ôäãñÜÒ<¬Ø]ø"õtk˜®4›«‘w£¬¶¥v±Klcó#žiIÙæmýÞ}Àí/´fsW²øµÐïc›ÅP˜ÓK-´÷XòíôÑi-¨Y?{ûɼ©0e'ôÊ»¯‡Ÿí>k7W¶7Ö÷VR%À·imßíq fó¥o4Æc2Ø©¯'×þüQñ„§°»¾º“þ%Vú^Ôñè/nÖæ¼y8&3$±Iåõ‡–éŠôo„üKá_êWÚö©©ê1¥Ž=ú—›bÀÎZ?&×þXjÿÅë%ºÚ¤zOö^æM³sȳ\ýÜÇ“åEÇ8I{åu„_EíÌzn¿©­˜’_ìË™üEx^Â/¶M4‚Oßbí¦ŠXâˆÍŸ$B=³ hÙSL´Ô#¾¶Õ£žQm5”j–ROk42Í,Ò¯“Ñ0–9?¡Ít¿ÁK/éÒZßê¡uyµÍ^êxcYæŒÃçê¢ò@õòEyÞð'â^œö±Iâ¿Í¥B%W·³ñdò\‰¼é¼¹ŒÓuÊ0þæº|%ñg-¯õH//´]kRÔæyŦ±6Ÿ*YÇ ÑYÂ&‡¨/ÖcZH ßð¥&™/t¹o¨"¹Ë?xýl5؉¾¶žãÃúE˜¿“\i ÅŨ”Ýyy“0’O+Í\{Ö­ËàïiÞ+Óõ}:=aôë+häŠÂMq˜ã‰-^9fÛq,Ù÷óäÅž€= CðœZw‡4ŸéÂgií/Ç–$–äyD>æÿ–rù’ xëéƒ^[{û-[[i÷V6^0¸²ƒTµN–#˜ ybÀXe‹þz™q^£àQðw‡´/È‚úòâygÖ¯DÂh„\u®O]ý–ïÃÚFâ8cµh&hmîìòEܶ‚Õ‹IæcÊò„cÉò±Àõ∼ñ;¹®GöäÑÝjP›‹[Þ!¹ŽMF9£D1Í‹E‚ÔK ²ûî=E/ƒ<+ñGBÖµ;¹†¥¨\XÊþY¹×&{{ÄþÏU¬0Ëû¦Q4Ñ“1Ç0ÍÜУx_áïögеïõ›mjO:òHÒ8Äq½ä͉ŽÌɃPˆF@’lòq^‹¦hº~‹lÖú}”QÞÑÛF"®Bø¡i×~¿ºY§Õü>²îVSþ‘<Ÿ¼ši;ÈL’JßV5êtW ñcþE™¿ëßþ’K]ÝpŸ?äY›þ¸Ýÿé$´çúWŒ¬¬ÿi»í5u©%‚ãDŽ9-^c$1ßù±æ8‡ü³ýÖ8÷ö®á¶¡ñOÑ4iã½Õõ½wWð†³tðë-ûµÔmîWìÛ¿ç—úâ1ì+ßô¿ˆ:F«ã CÂvZ…Ö§§Ú$÷‘Á}’ÚäãÄ$ó#öÏçOðÏÅ xÒk›oø³EñðF&tÍF;—HÏCû£GÙÉ|>ƒÅß|sa¤øÄ^(ñö,wQëË ÞÉk ò’^Y2bc8À©ßâ?‘aðk]Óï.âð¸ólõ`KòËýžþTrDA~¸ç½w÷Ç/ÅàÍ{Ä~½°ñÂh±K5ݶ‰¨ÛI$qÄ3.I“Þº‹i×0è®ú„O¬ÃæXÚÍ2,³þïÍ>Pÿ–˜NN:Í|kã­WÆ'ø>'ÔüMno>‰llí`ýæ«©ÊòÇqçî°[Ë6ù‡ÒYý«Ñ®õ¯?Ç›‹KÏÉáÛh5´­9¢ºh.´ï*2ùQD!–V›ÎŒŸ4ù#ð¯Wñ×íà¯x=üA?ˆ4»È'±’óM·¶Ôí¼íYPuµÌŸ½Bk¥—âG†-¼Ccáûé–þ!¼dƒJ¸½.äÿ¶YÏé@íÔV6òÜÜIĆG–Oùg_+xÓÃ÷¿~xÿÇbŸSñ”–gJkt{]+¸…šD}|Û¼vkƒÇõUÕ´WVïo:$ñH›9GúÊóo|ið÷€,üJÓCst¾·³’õ-ãÌp ©¼¸£ü8cǜЖx‡Wñ7‡dÔôKïø’/ ÙøÒ;kÍl×ðé’iÞjåü¼ˆ>Ùû¯79ÇzæÚûâ?ˆcð‰½×üY¦Á¶Þ ÖŸ¹’îÚmkìÑË7Yâc_K7Å/†dñ!ñŽˆ¾I|†ÖSˆZ,¿ÜóOîÿZ¯ªükð€¶-ªxãÃzRêQ}ªÅ®õ{x¾×?ÖÄ Ÿ¼^œŠù¢?|K¿¼ð̺߉o|5u6‹¡¶œ’[\on$·ðÍ vÒ fó—äæÕì×-7Ä_Šþ»é>†m;I 3¿V’/ô™Áìbˆˆ´³ú×{?Äï ÚêZN—qâ}+Ýb8åÓ­¦Ô#^Æý *Ne±Óu/è¾ÖômE[{­TÝOp T#C$מɜ3¹”zæ‰O›þê^*ÒuH_Åž%ñ5æ‹.“'Û§ÕŸ)mÿ­2çÍÿT?{æÜÞgéÐã­ øß[½ý¦Õu;«Ë?K©}hMFßH“T˜ý¢#äŸ2_².p{žµíÚÆx­&mÇÕ’W¼‘´ýZÞ*ÿ–ÍåÈuïïÍOá߉Ú¿à•ñö¦Ÿ€Sý¦ú•¼¶†$”ÇæùÑÈbÁö=xë@5ãrÚÞÇÁ`ñ‡‹#øwq.¡öÝrÝç’ùîcAöxÌžY”Ç´÷1.Oz²/uïŠ^økàCZÿO·Ö¼eñ,r%ª‹yb'És6Ä*ÝK€+ÖçøÇàK? Ûøšox~ÝIåAªË«Û‹Ydÿžk7™å“ì Kâèž Ðäñ%Ê c›Éd³‹Íšå¥—Çãï%ý~´zãÅ6)âvðþ.N§5”—ê«m'’cR#ÿZG–HàŸJùKáþ±ñD¹Òu+ÍOÅþ!y­D×Z\ò|×ô›©ˆ‹÷_ëLÖ°ù^™÷¯³žB‘–î+Ïìþ;|9ÕÖOìÿˆ>¾0Ã-Ä«o­[J#Š2<ÙNácîOJÈŸü¾'øá¯‰>¹¼Õu. [ÜØÊ—·r²ÞÈ&óí¾×,päq›õÆH­M;ÃVþ,ðÀkMQ× ŠÓQêbYEÕœƒI›|$ËÌ_ó˳Š÷?üSðß‚leºÔ5¹’8-îcÓíÍ»™f˜A•Wó%qéÔsXþ$øùá¿ø>ø°ÞøVÞ{¸ì!²ÖAu,¯"Çû¸†|ÁûÀxÏ5¨+âÿˆ÷üý•{âµäµ¼Oì²ß4:¿­u´QEQEW‹h:•¶¬ê¢{ˆâ2Ü\˜ÿðay^Ó^1á\ÅãáÎ?}}ÿ¥znÿÂE¥ÏâRÿÂE¥ÿÏâWœKûaøfm PÔaÒ5Èbû>©ý™q©YÉmm©ÜX Œ°G'üõŬÇ´uè~øÇámVó@Ò¤ñ“iâ}WM‡S‡Ã’jHuŽH|Î!ÿY'­?þ-+þ~ÒøH´¯ùûJü.Ÿµ™?á6Ð<½ãVí‹mºaé‹“æâ.F?yÜSÆo™4XÇ´&¼»ô„½¾ué›aææQÏX« øHt¯ùûŽøHt¯ùûŽ» $`~´}¾Oï7ç@ü$ZWüýÇGü$:WüýÇ]‡Ûäþó~t}¾Oï7ç@ü$:WüýÇGü$:WüýÇ]‡Ûäþó~t}¾Oï7ç@ü$:WüýÇGü$ZWüý¥vo“ûÍùÑöù?¼ßqÿð‘i_ó÷Z°Õ¬õ™ ¸ŽQúÊé¾ß'÷›ó®jiŒþ.Õ $Ÿìû>í¥ÝT'Òˆí‰Íðé_óö•⯌Á„Ú¿âK¹m´¡ŸhòA˜#2ùQÃ$Õùþ6øRÃUÖíoµë]2N´Ôï5KÛ¤ŠÅ-®¼ï&A1ýÙÿSøþ4gþ-+þ~ã£þ-+þ~ã¨u__t=?J¾Ô~ xcO°Õ”6—sy­ÛÇêð3žoïG#šžËão‚5MÿÄ6>6ðõׇôæ1ßê–ÚųÙÚŸúk/›ûºOøH´¯ùûJ?á"Ò¿çí+{þ-Ó¼Y£Yêº&¯m­éWC÷Z†ŸsÌý$Š´¾ß'÷›ó­@ãÿá"Ò¿çî:?á"Ò¿çî:ì>ß'÷›ó£íòy¿:È?þ-+þ~ã£þ-+þ~ã¨×ãwƒßÂÖ¾!&±:5Ý÷öu½Ð”ü÷F_'ɲù¼yU×5ü˪„bB­¬²u÷‹üMr¿ð‘i_ó÷ð‘i_ó÷gZ|rÓoþ$^x7MÓ5íRïO™!¾Õ,ôé$Óí&’!2G$ÿvËϸõªÚGíàKË? ¦¥ãDÖuÔ…ì´{nÉ®.<ù<¨Œ{e>né‘çµmÂE¥ÏÜtÂE¥ÏÜu{DøŸáÏkZ¾¢øŸJÕu}$âþÆÎþ9nmý6þYVWÄ_‹º7Â/†·>6ñMÄðhú|½ãÁšKIŽŸŒ‚€'ÿ„‹JÿŸ¸èÿ„‹JÿŸ¸ê­ïÇ Z|Bðσ¤&ÖüG§ËªØ¬Y’µŠ?3ÍÏ¿oZÑÒ>1ø3ĺÅÖ—ãP´Ò5;‹MR)VËþºŸùe@ÂE¥ÏÚQÿ •ÿ?q×á¯ÚkÃÞ2×µ]îÏSÓ ×­485‹VÙío$žÏíY„™OœFqäÅ™xn5Ðb×;ë¨ãŠÚÎK±æñ8ý+¬ø]ñvoˆÚ¥å²hWöÖ$¾^£qo,M4R˜eŒÑûÀÇIzhûA#âΫâ/ŸAá[«È‡ô™ O°´ É©Ì2ˆD\Ÿ& ²cÖxjç5‰4«Ë«Hôáqqo4–ök…qÿEûd±M1ÆDB|™@Éó²pq]¯Å¯‰ú߄Ʈ¾·³¾“EÑ%ÕîÖèäfòí!ÿZ1æíœäÿÏ/zÅÔ?iDÓ¢Ô<ÿ \'öMÂÁª'ÛbÌ2Ëu4‡üµÌžxÆzTÄZüOøÕ͕ݖ•޲Ìúþ};yÒèX¢—ý`f'ßÅt~*ñ?Žn"¾ñ„5¥ö¥-…•´¶2^Çö;XçÝ'•y§ù3Þ(àïRhŸµlú•ÄIà]V]fHe¼:}›¢-£›È’PÑÇÏïxÆ+[Æ?¯¼)«k6š'öÞ• ãi–æ)ŒrM<´—rdäyQŸ.Ž|Ñ6xÅ\€ËÓ¾"øÅÓÄý‚âM_ûD!mäÃis+ËöÌÏcúß$~÷µY¸øâ]Ͳ\Ü-ç†à‚)®ï›E¹·óm‚ÊnÄf?./Ý{ý1¯mñºU²Õu)´´[ôRÓàŠoÞ<—òÍ•$8– 2iÉñ‚âËŶZöˆÖÒ¼¶¶—wQ_FÑÛÍr%ò€™?Ôã8ã=)£à‹NÛÃúKøŒ^WÄ7]½£Ç$‚ÄÈd‡'ýPŠ8Âc§™Èë^k.«ã¯øFêH5k»i‘s,QÇý£q:ÊHŸ²Kæö´ýâõ+ÿ¶Fš6­²ÙÚ\è—-ܾSþõåýYó:ÅÔÇZv«ûDê—ž ñ.§¡øq[}2ûQÓ%Ôî± ëk?“/ÿ<¾•½â/Šº¿n.­µë¯Ï@þÓ¹kÏ?›F8ó/îù“¿®sêÏé.ñ®©ªx'P/©ÛÚMc¨Çy§Ï¤¢¹»ŽhD;Žs¡e1IèG\Ö6ñCâšè:=毧ÙÊÏÝZéMëÜ*˜1äæŽÁ÷2~&»›Œ~!m"Þ ´ÝB]WP‡T¤ûWÙ­lmÌ€ÇÔ—0Åÿm‡‰©ü{×4ôðö·ÿ´Rhz†‘s¬KÛãûRÚÆm”v'÷ÿêÇç@ž!ñ^Ÿiâí*ãU¸»ñ£®¶— ‹3ìßgI2c˜8ÿ]*&—=tj÷‹¼'®x~óX½ºñ<Ú–€úY¶°Ó%Œy–מּlÆoSWuŒwðOs ·†ŒÊÚ¼ú5“Íbåà‰¥•‰¸Ð[…¼p¿ßÄri^_ $iI2ë&ûË<Ï(Úyyÿ¦~f=ëðÿìã«Yx_AÑ®õ+8¡²ð®³áëË­:Gó·ÓC*Ëcí°çê:×¢é^0Ônþ1Þxjâ a³ƒDŽþÔyÑœâ²Ö‹à¿k? õOøŽßÃúRI¤ Ö}I&F_ Åæ¼Rň„y€zö¬YüãÛ­;ᮥ-¿‡Ç‰|#$±Éký­rm®#’ÊKs(—ì¾i“‘'”cÇ›ÜØñ7ŒüY­|ñGˆ“OÔ>ÜYiêÚt‹sgu3Ÿ&Iz†V<¸Ï~z稷øö+¯†ú]õ»Í}⸵ÌgdQjN}*³¯üñx¯Qñ“&‰%ì~/‹Ä¶v:„ò¥»Æºh³’71Cû©zÊóXZ_ìïÚG¡ ï49¤³’ ›•UuA2ëGQ™"_ú¢‡Êük±—ö¼ˆ¶›ÿmßü%©âðòhfö0F³žoÚÙåù<ôÎF:Ö ïíW;ÿb/Á7ú¾ÛàêEö;“¨gù8?ë|ùèÊi¿²Žµ§É£Ã™ât]MÔV_êÚtPK§ÆJ[Ûþêê,ƒ"‰<®O>Þ»¢ø#PÖ¼[ãêÑnoÐèš,R6L6 ËÈqÞi‹1Çü³ŠJâÏí¡Ïy¥Çe¡Þ^Á5Ž}vDñ™ák±˜a†³Éÿ\¸¯AñÄË_‰þÒ-¡ž×N±›X×§u9† Á XãÌšPÇ'ŒA'|Q/ï„O;ø_ðÄÿ5m7Sô+ɬm^sÉ–_ìÍ6Ï>g•Ÿõ–2™Ãùö_¼sªü"›Ãü^±ÕmEÇ™mGH‹þZœsœŒÿ ÿi[¿ŠŒVÁsè7—{][}§PŠâ9%û%ו˜½#½‡Ÿj¿£üd’Ëá x¢þÎëRÖ.õ»ÛJ‰á-ßö„¶‘[¤ÜE Ç›œ`gÚ€â¯xó_¿ð‹cÓ¼,¾&Ñ~ß šL÷×"Å¡ºEBë?Ù¼ß7÷`«7ã„>êz%ïÃo^IöŸ øHY–í°«©ãÉÌ]q/ à¼X(kÚÞÒ[ÁÍqâíkírC£G©Ä¨!·Eó%ûIüè@ã<öÇ5µßŒ§]ø}àýgÁñ$ºÏŒå‚ ÛPˆ€¥Ç™3J?º†)¥Ç'ÝÝCâÓœ!î¼¼zóŸÆ¼;Àÿ³Þ¡àMG÷ڤú4Ö:ZC-òZ‰d–d‡M»³sþ¯2ñuQСúPcaÝÓÌ|à¦.s_AÙi¾Ôì¼gàKí–¢ÇAÐ.t‹”S(’ie‡1ó/ý\ãÒ¹»OÚSM½±šåt[¨ž/ êZô±K$`©²¹û,¶§ßÌ Ï·áWu_ŒZ•–¹àËKOÁªÅâ…·’áÕíq+ÇæM)‹ÊÊŠ><Á/$ã³ßúó “á]GEø©ãoI4W6º¶™¦Û[YG'ïí×›ùý¦:§ð³ÀòØøKP›Å6ð\x‡ÅêZÌc7‰oî!„EüŸ{ã­Bçâå§…4¨­æ´°²}K^ºš)K@%ÌVqEÛÌ’A3HâÇqT~üKñOµÚëÞ}:ëʳÅ.é¢ò¢Ç_ÞŸ½åKÀÌ#ª w~ð~‰à­-¬4->>ÔÈ]¡ƒ¦þ+~Š(¢Š(¢Š(¯Ðÿuã ý6¿ÿÒ½F½Â¼7H¶·ŸÅÒ-Üò[ZÃ5ô²I¾Wü½ê9 1ð—ì­¦i¿ ï4ÍRµøÂâß[ŠßR“Uºº³²ÒÝðÚ͘¡—Ê—œB:Mïœ;O€?oÓíúV·ü Z_­ïþÍþ4Â¥zÞÿàlßã@?iöý(ûO·éZßði^·¿ø7øÑÿ•ë{ÿ³dý§Ûô£í>ߥkÂ¥zÞÿàlßãGü ZW­ïþÍþ4“öŸj§§KæøXÿ¯KOýw]ü ZWý>ÿàlßãX‘i6ÚGŠu+{wqYÚKû×ó?å¥ß­djp<ðeçÄø"ÎÚâÎßû3Sѵ‡7_ꥊÖH¥–¼¶OÙ¯Åþ¼ñÊø/ÅÖ:V¬ú\:%‘¼šÎk{8.¦–ëNóà†S G͛ʖÞÚ'ø—¨iÒ%§Û/ID_¹ž*»Ä+éLYÖ/¡óç±ò«Ó§…u?wí pÏä2ôÙ“ÄM½ºÐ‹ÅI‰î®¯BI©ÜÃ26XŒ²¬_òÖbrj׋?f={]²ÕZßTÓ„¯ÿä¶Ñ}¦êÖ+‹,L$óf†!4"LŸßCéZ_ð–j¿ô¹ÿ¿´ÂYªÿÐRçþþ×Oö]s›ûFéß¼¿< k¢­µ¦›sç\ß\­–©u¨ÂÓK/œOÚ®¿}1÷®ÏíìׂKâ=z{K‰5 Ò—C1;ým%ߊ5›9fŠ}Bö9¢ãÊó¨þË®^¢{çÚ}¨ûOµ|ÿÿ n«ÿAKŸûûIÿ n«ÿAKŸûûGö=pþÑ z%§ÁOY|Q»ø„‹âù`Ø×ååòt7"3û¡)òÚ»Ÿ´âõ¤Çü»Iÿ£¢¯ÿ„³Uε.s×ýmð–ê¿ô¹ÿ¿´eWí¿‰þx·[ø»¦ø›KÞ¶†úÞkvÂþëûOPµ‡‰m&¶1eþë3J|œW;áƒ?ü){ðËN»›Âº×‡<¥¤1Û6­w©}$’»ý’_8Ç"|“æçØU¿øKu_ú \ÿßÚ_øKu\cûRçõÖŸöEpþÑ Iû?~Ï÷_u¶¸ÔãÓ¯†cy§éZÔZõüóIÍל|ËÙ!ç“äË^ñwÁ_|c¡ÙOejãRÒ®%m@f?.¨¦šûõ¯:ÿ„·UÏü….ïí:ëSËåŨ_M/¬F—öUpþÑ C§þËCÃڵƗâ£x5çî®,´Ÿ²]Ãe§Zz%»5‡ì½â}sÃf¯¨xfÆK/ ÛxSIEIf¶¼H.¢—;„ÅNa¸Ä yÓ×Mÿ ½0˜Çy©‹‰yÿWAñº,EéÔo~ÊeòD¾wüµ¥ý—\?´( Ñ~x×Qñ‹ø—Ä á­'P¹ñ­‰.lôkÛ©¡ŽÚ ?ìA–Ö"e÷Àþ•›¬þÌÚïˆ>øÁ—Zõ•¤š.‡«iW·¶Ï!íV’A°ƒ1‰;ù_Òµá&×¼Ï(j¾pÿ–9éRÚëž!»µ’[}Fúh£—ÊÕe×äö3ŸÑÿgh©¡ëöW†lüc¤j–úŒ¶÷¾$Õu[=AÎK0$šò#5±ÄÌ!ò¢“ô9¹ð?ÂZ—Ão†úw‡õ†±—TŠæöâa¦4j ·RÍåCæÇ¾Çá^~5¿Ÿ³ ½HÊ?åŽjøJ5Ÿ&Y¿´/|˜úsKû.¸þ½Dúí>´ûWÏçÄúÏ•æj_yR{ÒÂ[ªÿÐRçþþÑý•\?´h@ý§Ú´ûWÏßð–ê¿ô¹ÿ¿´Â[ªÿÐRçþþÑý\?´h@ý§Ú¸I'ö'‰É,RýŠOç y¯ü%º¯ý.ïíiXj×Z¿†|\n§’ïËÒ—ýwýv¢ytðþýCO¯B§îé™>$×58¤oø›jè)wÿÇ«¿øC#Oeà÷w’W{ûŸÞË&OüÅ»W™ø£ýc}Oó¯Jø=͇„?ì!sÿ¹ZéÍèS§…^Î¥J•Ÿ´=èt¢Š+ó#ê®/^»ðÕŸÂèÿá+†Ê}ìm¡žÚúÓí1LqÄ~O>iÏü³ÁúWhzW%àÛ/|>Ót}W|-'Æq,rÄbš)~¢Hü+ÕÀý³ œæ‰âï…VVúd:lz–ßϵ¶‡O¾Lq¿y³Ë/(­Ös/÷¹ïPéWÞ k èúO‚­m4kù®®¬MiÉiu^gš-æŽ91ð%Œuãé >èÝAq%þ«dÍ[í$ó|È qE~\žaÈÀº‡û´çðî‘aâ i7þ$Ö®uø&šóLžìÂÓK–c–"|‘—åqĽóÖ½ƒ3’ñW‰~ è¾¾¿>е;[¨ ÖÞÊ×AŽY.Ï’óÃpbò¸ýДù²àGÜŒœõ>ñ€5¯êmá›;Ë µýÌ•-Â'”»L¢1ç³G“å€ÅbßþÏÞÑ4=Bk­S]³±ŠÌ[½×Ú£Ýga Ä-â">"K/OÞò9®³Àÿôo\Ï}£Os›t­*YI ¸†0ù¯ D%$çþZÈÔŒ¯ˆ¾2ð¯Ãkmd^èM¨­ýŒúΫŒIçCoPî”KÃû˜»ð=«&ïÇîuooltÉ.­žà›‹½¼ÛI^Yb—Ìfˆ}–I¥Š^%Á”©ë[ž/Ѽ'§Kâi|Qªù'ÄPyLeo- ¶µ‹ýTXè›4¿Yš«ßþϾÖoµ+‰¥ÔuŸµ]ÛÅÛ£kšj>¥ÌUoÆMAêàM4ñ’DX–Rz‰²O$RÇû:xRë}BÎMBÃP·Iaûb‹{’byåšHvM ±àË+æü\s~O†žÖ<Ï^^Ms¨[Í.­p"—Ë›ý(Í™&:çSþª€1ì/>ñ.¡†´[htK;‹½P訟l°ºYDPçËÝ,ebÛå óˆñÆ*Õ¿ü*ËÛÍ/EþÁ³²½“OûŠ?‡¦±šÚ Ä¿¸2yQý˜ÊCä“?7ó-¿Á]ûGP³]c[š/ì­:É,¦k*Ñ-¤›ì’Ã(ˆJ%ŠA,€ù§œv©õ‚öú•äwÓø‡Z›W6ñ¼¸[gó.‘d÷r$q K“þY˜âöâ€: jW­¡ÔâÓ!i—ÓA§M$Q’†=Й"þà?¼ŽÙ®ïÄ?eÒï/nt *{K›ÿ<Êž’hﮡ|ø‡“›¯/:<õàó]=®©àíxE´×“I‹I¸þͶ´1Ä.^(v_Ìæ]¾`ÿ¶˜=k Uø7 ÛéVzM׈uk[î¾Ç¤[´ö«ö’"kH³ÌoÿËO2Räb€"×~*|5²ð߈µô¶ÖcÒìîæº´°Ñ¥˜ÝGçbïÊýÞ&wúâ8æ\u§kZŸ€5mGPÕ|Aá{}X=íö³¨èa.`¶'Ê1¥‹“Œ+Ÿ¥ii? |¬è—vºiytñ¥£±Šäþë̽/sÏO.hˆœŽiŸ¾éZ˜×|O­xŽòÂÐéÙ¼r aim"_5q›‘$qËþ·¨íÒ€3|©ü>ð~¢·„,$–ßP¼—HÝöU´–'If’äË ¾_”">yy`倯j”^?øS¢\XLJ­,ôv –ûl~–i—0ocûdŠQ4gÎ9ˆùC“Åu>/ƒ%k½OX»ñ¨ËsÜÜ HÌ—™¦ëû˜þ¯á^_ƒú ¦“›yªk–"ÒM ÓÏ’?ô[iüœÃ—ã÷0Þç§Z]Æ®l5=/VÓcÔíRû=Å£x~[¸d¿yòH–Py ïšç/üwàoC¬Ùx9­òig¾¹²ÓeŽCi/”D¦*oõ¼rz×Ogá øêÂö-'QÔm%±×.®d¼µ>MÕ­ñ‰¢›l|f9ˆÎ?å¡®w[ø}á_†u¬¶×·ú]Ñû%ÅÉy1C/9ÿžÆ€:è~i£âü%ƒ]ÖdÖ^ÛìÆÕ®£6ÿfÏú¯/ËÿW¿õ柤üð¦ƒk¦Ã“Ii§i—zLpÝIæDö³È²J²g®J޾¦¼ûJø…¢ÚŠÿÃóxïLÔon4 z{Ib&´¹ŘaÄ"\ã2yRK/'8Ç|7ø‘ãã¢h·÷ZõÇŠõ}sÂήšuÕ­¬q Ë;¤yb(a—Ÿ?>Ç­d>Ñîz?ÃÁ¾Ô4{­KQ¿Ðï Mo­ê&hàƒo•åGžƒ÷ç­W‹àæ„4i_ÚËÿ`Íæéš‘¿?k„yf=‚^èc“ËÇõæ¼ÂûŠ_³ÏŽíâ~©?‡å{‹k«[V[¯²È<“ P /“¶PeÏ®Œºn™£|Õ4ÏXGà›ã6«j-4B–=2SRMŸÝÈ$‹ ôÇ¥/æ¨Õ?f\øVÓBd¿²Òí´tÐäK}Aâk‹%}â_¬€|ýOBk¥O„úJxÁüCg{¬é×r¼S][ØêrÃkw"D#O6¸Ž: äŠù/â'Å_ø³àBøÖm OáàÔ!¬mOü$wó<±ÜÛ0òr|˜üäAäÍûîzqèº×ÄŸÅñèè â­ Ãz}¶¡ckc¢jwëš…£Åy¢€ØI,ó,ÑÜc0òÉ­úR†{ˈ­®¾Çu$~\w+)ýq\<_ ¼=cð¾×À·ÒI6ƒ Q¤“O/—$ò ¯,’ƒÌ’K™Iã,Iï^‡4Â÷½|ñ+Ÿüø…ãÛ‰|¸|Iqdš¸Mé—Ô?g“Ë#ƒ+™fëÎø½*÷-kàLJ'ëš…¤’&‰à[9£Œû»ZX²Ì}L0¿öðhDÛÐ~x_Ã2FúOÛl.-âòc¹†äù±²ÚÚõÿ®Vp~U_Gø#á»o^ø\Þjú¶“sz/3u¨–Þä\yÞlRǃ‚cæäwôæ¼?à‡ÄÿÜêŸüD¹»Ò.4©æîöÒÂÕmHÒ´Ë¿?Ìò½næ9ÿUõ«ÞøÃ-ÏìÝ5÷‡^;{UÏü*Ñü/â[ JÄy6Ú>’º&•¦¦ >-À¹\¸„dkÉ=?Kð8´ø»:x'Q–ûíž=X´ùYâ÷6¡Ì?eýàóÛˆå‡ÍhCâ=wâ—…~x?XYtÝÄ:tω„Q›i­í#\Ê„g1™§1DG]¯7¥{;øVÜø¢ßZû^ &†ÍìÒÖ;¹«mÞcÄ8ó8Ǚׯ8øIû=XøK‚kÝFêÿ^†MDÃ$7Ò½½—Û.¥™žÚ9IòdòåãÓ޾yã  ›Ø¢ñöRê1Xõ¯/Íüð+å|Xñìèú–»âíkÅ6w6É4ºTZm‡œÆM"î𼘆e3[Eå˜ÎsšÌ~Ñ~èW†µÍî÷UÖô½nYn.íõ+ŒfI ’IÊHü¹ $ñ³ü[ð"-eüh°ú“ÞÞkAɵšrç8ýöH¯ðWÆOêÞñýŒ_´9Æáí;U±ñî£m=½¤“I4sG4öÖqE0òD2ˆ²zô­H|CyñfËá Ì!Ö`»_ßÚ®¿,z}Üαiן¾µ’8>Ë$'§œ!çšÐYÔ¿gOêZ –’¶—ö–Úuæ–ZÂí¢{‹[—q¯œ¸–Aæœç¦yЇá“ŒSÄÖz¦µa¨¥®žˆü“k "Ä‘“´ž\u?Zðgøÿâ=Ášv¯®xš+gºðF½4W[[Æo5‹+‘esúß,I˜GF ¥Ô¾7xªËǺ¶š —]»ûL—÷úÖ§6£wu* Ì͈ãOúçaTz/Zë?ýzòmîOˆ¿5KÄžEð÷ƒÕ´Ë.Lyú”‘þý¼Q£þ»J±ðáÏŠ|¬ø¢óľ+—ÄjW¦kaÿÑyQ.?ò1ž­EPEPEP_=Ýøi|q/ˆü7çS«Ùêö sÔÄ%ŸQ‹Í¯¡+Æü!ÿ% ×{ßý,ÔhåýoQø£á/€ZŸÁûŸ„ÅíÄPOöî•’ÚM›æ‰Š/õ¤úw¯]øUâ |rýõ+]GS²K{#TƒJ‡í7"äbI|¤Áóy•ãø×ÐwÞ3Ð´Ûæ±¹Õ-ãºòÇÌýâV…†Ÿk 6VÖФÍç7Ù£DÇþZõúÖ•1*´9 DaÈ|qªÞøJÖÏKÓµ_ xfßÄ Æ££øNûN¿Ôl¾Æ –]>ÒhnÇ—/“™2 UÍR_ˆÖ¾-øs§¥×‹/¤ÓWGY5˜tÍ^(µxe¹&í®ÓÏû§•|Ár&œö¯³è¬Ë>QºðÇ‹4ÿO¨]ŸÈnü]t|A †£.¥ý–—w@²Œ6a\˜-6æã=²uëgÆ^ ñg…­¼wk%°ð™Ò–yu(šê?´º]Eu­þ“ˆ[÷¦`LYÎxÉû{Rž´ C¨¬û[Ûká/ÙçŽQžTžSýÉ;Ö´(QEQU"½ŠieŽ9c’XÎ%O3ý][ ¸OþG-SþÁöŸú2î»zâ5?ùµOûÚè˺h”ü{áá­ë2êWzUŽ£Ì_fšüEç˜qÿ,Œµgâˆt¯ìh ²×í5+¹u X£Š+¨åÿ–±U­CÀ~ o&³«2]Ü@$1Kkçy^moXüðð1ÏivÊeâ9a¶†» ßëÐá~Û“Ùû2¶‘{£GàkÓqw§Éw-´²þúKhå¶›þY`ÞËõ5%ο¦ËângРò-baš(¾ÆÏû¯õ²ÿßïõÕjoƒ5œB[nê¿ç¬©57ü) óà‘}©âëÄWÓj~ø‰Z/:Sêk’œ$ÆtÎ…sŒƒÆ1ÃãÏùz¸ÓîMÞ&óbÍû©|ŸÞý*w6°xgEŒzC]Ÿí »¹dòŒqŸõCþü‰Gý¶®ßþÏKÿ Õ÷ýñ:÷à‘svŸkñô×mÄ34G÷QRúÞþ~ýV¹ÎÉâ6¼›T·‹X²ÒîÊÚ¤2jqüö‘­ËÞjÊÕÿ°…ÏóÕk—9ÿuCËÿŽ{ÐéE¥ùiõ‚óÿxþOÂ[îG4s•A·ŸÊ1ËäÊ;Å&ß,CÞ½ô®'ÄO¬Ÿ„±\øs+®[YZÞ[ZgoÚL^\¿e'°—Q?ôÔ×­ûf à-¿fók¨Xµ­Ýž‰ ¦•&› ¶žd“ìâO;7K,™>t^oî‰è ¾§FðªóD·ð‡Øl¼5£K£]Ë,–Z5³ÛÙM¶þTŽª?å§¡=ºšæ|?¥üWéwZ¦±{”Ö©4¶ñØÀÑ™¤ó¥–9%óv#-C÷Rñõ«zNƒâÍ)~I«&·®ê¶‡í!¸si,pNúsÃ)=|â?Ôñƒ/\â½s3’¿ý•/µK«ý ûìÚd:-‚^Z³DÁis7}ûÿ3އ<Ž+¾øMðJ‡~,Õµ¼ÛÝÜêumLÌÿm¸NeòçOõ_»'ËvºW{áßü.Ó/¥ðýô¿ð’ø§Å“@«{åK5Ìrf$“Ï’ Mÿ\aÅzÆ]3V½øCâ+D‡PÕ5™´é-m’ÜB$¸,ã&aåsïG÷Ãû†o~OãÙ|m¡5´CQÒ×FÓbÀGnÿ½•äÿËY~B=!\g5Âj²¢jwkks¢ÛéóM#ÚbÈùúT_kšo"ÌŒy+(˜Ç0GnÕ½¯X|O»×uÝSF’þÑ.',´ÙÞÛʆ¦E²AßÌû^O$÷íZk øÂîÿB6wúÛi7Í2ë[dÔ ¶¦Õ&òAÌ` ¿Õ`{ô 7°ý‹¬´™m"Š=êÂ$™WKh^ÒÝ%’ydûPÿËdˆÃ=0#Šì|mû:¯Ä}d×ßO¸Ôouyõ;µ¹¶Y¢lÆb¶Nz˜¡Ú¹ÿ®¸ë\Cøcãäº,R'ˆ5é5­„­A§[nºýÏÈ@–_6/õÜf/ƺ#ÿµ E†§â ZÕ$Ôgk‘ikivðù7~HŠO2c'&Ó¬]¾¸$ëÙÿSƒMÖôÿ3CŽÖÿBÒ4y$KiK§°ôïE/™åÏîxïVn>ê6ž(´ñ6‘ká«í:{[{;V„ÛË›ÑJ?ÕA/š|Ð>œÕ¤|BÓ¿á%»S¯M­jún”|èïmšÚÒão—x`‡ó!—ÊÎ%Š>‘¸?òÓ“Èôö¯O¢€<þÙìu¦Ö´«2ßT—Q“P’Cnñ›¯øšÇw LG'ʈy#úvàö"Û¥¸†û@Äöw6òß0ƒ+Kj!99ÿWí_^Ñ@;hß²üvš«êë&a¨4ö“Y KÙ¦ˆ®¦–Xmxùb–9ˆÇééSŸ³–«á;;(-†æ’×]²ÖSÌ·`-¥Ž(–aî¿reÛÛ±¯¥( ñ·Á{ïè¾&Ó *ú×RÕãÖmšõ„2ˆâ–&ŠX¦Á‹ÓÒëÞ¿Ò-u½cTž+ÝJkOìÛf·‹ÊŽ+X­%Ú¾_H¼ÙLÝ Â;W·× ñ`Å37ýq»ÿÒIhNÏŶz—‰çÐm¦2ÝY[%ÅÈŠxȈ“Œù€ô=*î‘â­ÄË›«YêRÇþ±-.#—Ëü«Íí4O§íÞ!>è’èãH:‡Û­ÿxÆ_7ËÌïÓ5ÂxCör×´ èlØès'…µúöÅÿz—73$J8ýî0Çžù¥ö@ö|XÓ‡¿±´Õ,¥²¼†)ínÇ,3r$ßó®ÅŸ¼1ðïOÖ¡v•Ö¶’]Yéöä}š)¥òa x8F9ã¥w:éÓ¬în´—fÞ 7É!öá:ŸÁ½gÅß¼A¦_ÛÀÞ-ñuÌ:Æ­ÛåRS,,-·zE)öJöy|_¢Å£Ë«É­X.•"Kóu‘ý´éM¾ñLJôÖ·[ÍM³yÓÎŒ\^G‘?½É¯Ö¾ jöšÖ§ªéz¨éÑxÎ/[xp”Kk»oìï²H~aåÇ(”™³ùd;šæ¬¿eUâðÑÔ¬t ã§µ´†q$vqhÝÍkaÄ1[ úmüK¥Û^X[KªÚEq|?Ña’ê0×Ë!ÖJÉ}oÃ~Õ4½ymÞµus5´E´Nã3O)Ç×&CÜû×ÍÚgìÇâ-;û%u]=|B%Ñt; $¶Õcµ[ ,Ó,‘K7•»÷ DzƒÇzö[ ê¾&ñ‡üMªZ5•ÃÛËáÝÞç–ªI’àúm7?õ΢@w:ðþ¤’µ—ˆ4˵Š4¦Þú9qv‘Ûô¦è>3°ñ‡—[ŽSšÍ+¬óKCræîÏÜã9ôüëÀ~ ~Ïú×ÂM^ÇV·Ð´u{[ !ž×Lò¢’çþ%Zl>V@Æ>Óe7=¸5vËá‹u¯‚xN÷C³Ón­|Gý¹ÍÜ3Zj0j›Ï±K°òöÃÐŽQ@ìÞ8ðêè°êç]Ó—3yqßý¶?%½„™Áüë?ĺ¯‡ücâ½Ua´ó!‚Úæña2M*y›a‚dýìÄÿMO©¯.ñ§ÃÝ{_›Áž!Àz3É¥5÷Ú¸5‘sâ:ÞÃí_m†dxeš1Ÿë„_ë6sÎ=U–ï[Cjš]¼š ±i$ÔMçïVãpÙ‹6äù™ôâ¼7Â?|]¢ë>²’ÆÆ'C‡Ä¦¢·™7 }qæÛ~è8vÎOZÎ_¸TOjðÇÄ#ÄþÒõ»{õ··¾Ó ÕR+ÉR9"¶wG$ƒÿlwÏ·Ìr|#ñƒâÃiá½Ä:¤ž×íÞ;™ü«K3uªC,pÃ$±+—ʃ…^+Ý<9à­gþŠu‡º¾Ñü£ÛY麅صBh¼»‹Ö炟Yç© F…ãŸêÒ/ôõÕm¯n,-±¹^Y¦óóÐËäûýkº®OøYá½#Ǻ‡ŒmlŠkú‚Æ’Ý4Òî`¦OSÏúÂ<¾Ÿ»_Jï袊(¢Š(¢Š( x‡‡âšãÅ÷1ÚÉåÜÔ„SÏ)Mæ£^ÞkƼ-*[øô; ù׿ê“ü¾j=ë)«èq.©â=J»Ð_LŠ]9äv~!¾)“ùÃÅæùÙÎM;Ä—×Öß²—Å+«;‹¨íRÓT:<ñ8†o'ìÜñÐù¾oë^§­xÁ~ ÔMö¡áÉå¹áåŠÖá>ÒãÎñ/ýµÍtðêðG 6—["ŽÞ-:_('`1AÅqàðpÂC’ó«í'Î|lß¼eð¼xÒÒÆ |!(“DôíOTŸS[ iÌÞv«óþæ(ÏúÔÅ,9”k¼ð÷Å]_ø6ÃÅ^=Ñ|;§^Z\jkúaµždÇwåÅh%aå™D8ó¼œdŸÜ9H6±fzÛ޷˳þAó}ÓÿlÿJ`Ôôñ§Ù.‚§ö\ØOü‡^‰ò8øïñ*îÓâ6¤ºÞ‹¦É£ÙkR6„ÒÛ5َ݇-$Šß`ÇËæ™Ï—ûÞ8­ø—^±ø‹¢h~#ø}¦ézˆí±â··´µ¹Üir¦&²“æãþXŠú¬jÖ/+ÉöK­ìB³>l¶:õt‡T²š6G³¹8!Äš|Üç¯ü³æ€>Lo‰úïÂM;Åz–Œ¿l±»ñf¿¤}‰#ŽI¿µ®–9´¹?礇ÊäŸøø‡= zŠü[ÃO‹î¼câCc¤Â9}gy©ÞÜ}šÆkíö˜i|¡!Û1ÿõW}â? h~(Ö´½KPVŸû2tl–Þd·šE?$¯ÏÞóÇaùWO.¯e4enmï&SÕN™Çþ‹ ”þ|Iñ†æø[³â;ÖÒ|A,Æ26¶}RsuªÌmn."ºÌÂÛÉh³å`ÅÓ(迾*^é¾3ÕÆ¡¡=Öi¨5LJ¤’9î´i¢¼1C!¶‹÷¾JÄ30˜î$~çƒÏ×V³iÄ­ir%ç}‚l¯ãåÒ.©fŽÒ­µ×˜yi³æ?×÷tâ³=Ôþ6øµw‹ÓÇqI}§ã\D…~Ñ‹E3œp8¯n+èšÃW²Š5ŽÞÞòFÝ:dÇþC©?·!ÿžwßø7ÿ  Šâ5?ùµOûÚè˺ßþ܇þyßà ßün¹›»•¹ñn¡"‡SýŸiþµ<¿ùiw@=/…´ïøŠk¹íîâÎÚÐEÄ™ˆ~ãþxÕÆðõމªh÷ cci9Õ-bŽhm!†oøûŠ»ø_á_ØiWÚÌvâþM> Êš”Ö³K?é”Õ§¤üðÆ‰©Ûj–ÞÛÈdŒÜêWF/ûe4ÜW7±ýà:|xñV¡ªx‹YÐ5C¤Å§êºé>±ÕVK¨¦–s,¼žyýÏüðÐé¿1_AáOøPñå½ÿö޳jž%µkkH`ÑX%ýÀëaýñ‡‰«—»øÁãÍgÚ泦ø¹­KѦÔáXtÛYb¿-«^ZÄOõ0Ãþ¦¾ÅP\í¶Ë ?Èx)ÃŒ©µZ"ž_—’‡®1ŸÊ€>Zñ—ÄßxN+í uøfK?ÿdMâ‹åµµdµ}?í@J|Ÿ*)Œßºâ¯©|Møac¨j·>'²ø{Úf­=ž›kÍ–¦fºš)Ï›Ôþêùc_X?‡Ë££e‰Æÿ¬áQŸ ›I„dz`>_Zù*ÏÅšŸ…|âVÓþ ‹}VÓÅzˆ¾Ó/çÓâžÎÛí³álüØp%›÷$ùÕô÷Ã}Z?è­n×- í„Wp½äf)¤<èkfO ‰‡[iCýñ4g÷•¥¢è’ ôa:3KI÷  }(­ ·üôŽìÿž‘Ðn+ÈÿhkÈ­tßǪßÜé>¹×Vß_½µ»šÔ-¸‚m¹š"<¨¼ï'Œ×·`·üôŽƒ¡$h]\«“­~{ø“Åþ :^™+ë÷‘[Xé3K¤Ow«Mm|ö­«M¥ÍŠÿËýçØâŠ~™ƒþ{WÑÚNŒŸ¼Oñ?Ä_lÔ4›b#ð®—{¥ÏWf+sæ]O¡žSý°îíáù‘ã‘”ü°þî²<%àh6ú™•³·i%–O6IIL²Ë!î|Ù ?ZøÿÇ~ ñ­Ïì±à©ì¬¼E>‹ƒíu C[Ò5H–åïG•ÒK430e”ÅÜôkz««ü@ñό֛ῠéhiµÏ•sÅÇ•wx¢X¿å¡cgÝš¾˜ÿ„uÊlß;|¾:ÈðïÃ;šdö žUÕåÍôÿiýé’Y§3Lšù fÓôý¶Þ1ñ¤ö~ ¸]vñÛKñÒÃeº#c`÷ñKûï";çþ{k¼ðl^%—Ç_µêºÏöƩỘµM.i|«TŸìbfAÐMœÿ®¯¥bð¼PÂ#Ž+aÿ3ËÙûº—û a “Ì_5~î{ýhÄ>éói¾-ØÉ­êÚÊÃu¦\£j÷†c› ¿ºŒÿË(‡oƽv´°$]Çz|— Ï\Qý‚ßóÒ:΢´°[þzGGö ÏHè2¸?Šò×?ì'þŽŠ½?û¿ç¬•y·Æ ±Ò5òçÌÑ\ähkJÄ2©ð+âõõ?ν+àïüxø?þÂ?ÏU¯5ñGúÆúŸç^•ðwþ<|ÿa Ÿçª×Ðç_î¨ðòÿãžô:Q@éE~Z}`‡¥pž8ÔuŸü Õu-kx5ËÉyl÷1ù± c¶Èþ_çšîÍy^—ñ‚M*ÏÄ^øsP´±ðý‰Kõ½±XîÚXAÙ™8ÛújFsÖ½lÛ0fø÷<_¼5áûytÍnÓSŠÜMye4^SI$3MçÂ|âe‡ñû 9ÿ\kQý¡TQ~2š‹á·Žï|}¤]Þ]è7¾k{û»5Žê{iYü™å‹þXË(÷|óÖ¹ß|UÓô;Ÿ[B¼¸OÝYÇ o$’É(Är£|¸zõ—ÊýF@<¿Æ¿¼]àOÇ£Ëuc¬Ýh0ÏÍ›¢¿Õä‡JûOÛž0q ±”·Nñ~ÔxkãÞ¯©ê>±¹±Òe“Älf±¼°¼Žh¥‚?Òæ&)%‹÷Q˜å©9'é^§¨øÒ×CðDž$¹[™--í~Ù$pùrÈS¯ÃõÍq·_<=6­a£k‹˜õ{É´Ènn² L'1y¾o”fýÖLŽ9 Ö§ñVðí–®ßËo‘âZO±Átc3A§ñºÁ1K$²‚%l‰L^i á5Ú7ÄÐøîÖ7ñ†´M5.|›»k·ýÅ«cQɺ”~ö9³i¶s^Ó |Rºox2þãFŽ)5Kû&hìeÿG³"IcÈõæ,}MYøñi|âÝKL´Ó.¬ôÛ{E¼‚ö놺ÍÌÑnŒgý_îsøÐýoÄ>'ñè½mgW‹LM×P¾Ð/#µò­ï.Æô‰`ƒqK‘,§ýtUkâ·µmUš-c+¡iòëÞpÌrnV†Ò×#§™&é?íˆéÞ!ý ô?øªï@¸Óu­RÚX …m$µ”ÝM,°Eå¡ó¿uÍÔ?ë¼®½ê­Ç­;Qñ6“§è^»yõ;Õ´Ô.åÁöYÂÝŸ)€ËI(û$£8ò}%Ý&ñ.—”¦ÎÖVÓ6I–âí.â´ŠQ1y’ËX¢Þ¥ñÆ}káî«©xfþçN×õM7Ãëz'aûû›…‹÷³Ü$9û,Dó+¹ÇZô?‹´ï†þ‹YÕtÛíRÝ.cXಆ9$Š@ ùƒÌ#zŠæ¿á§<>¶—7O¥ëÛ[yÐÈòC y^t¶ëÖˆÿ‹ýOý64åþø÷â[ÿ\éWž&ѵ[{Ëå¶µK«4áõ íŒÖ8È‘b‡Ê”ù¾wú£ƒÎkÙ~ ¾©©h:®¡©xQ×í.µ9Æ™-úÚ¤ÂÖ7>1ñ³Iiö쇎Çì"ßw˜#â×ÍÆé¯ãT´ÙûÂÚ6Ÿ¦YÆo/­ôýïCX¯$¼Û[¹“ wÉ%G§N”¾Èž%ø—¯jß¼Oâ­#FÔüw§é2ê–SxŽÒÞá.£I/ú˜®øé‚&1HãÓ§ƒâ=½¥ÇÃí+P¶¸}GÅp3$±'î¡’+o´?˜xÆyŽsQh aÒ¼+©xv÷ÄZç‰4{Ûo² miá& o$ÅäÇåÅòy‡ßšÊ_‚ÑÂö_ð”ø–]OÃsy¶Ï™iö´C ˆÄßèÞVØÿV$ã¯Z`q^9ý«í“á•î³á/k:œž:úHðÁ$:dNòÇ —X˜–>d2ñ—î“ÈÁ;:¿íYàÍâ~¹iF±ö»=>ya¸´ ÝÄqbÉ7œÄùѪŠ@3Ïz}÷ì«á™|%káÛMkÄzfŸýŠž»[+›q&£b#¬34‘9–_õ^_‘Ó§Xß ’ÏÅÓëšWŠuýngŽâÿE³šßìwó¬1DA$>hÄqD?u$y @“_?üNøã¬èÞø‘­è6ÏcáYìì`¹u2½ÅãËÚÐDü²I¢Ö]Þ˜¯nÔc¸šÆæ;9c‚ëg<Çú×Á |"·øyu{>œ¨¦êî'ònngy²ÌO¬²åûÇÖ€3_ãöžˆcñ øuåÐ?á iöÓrÖÿj$·Ÿäù~Oïw z~‡ûWè–©¢­„¼O¬ÜjAHŽÒ+4{yMߨü‰|먈—Î_zéõŸ:Nµwªj ªjú^©{«.»¡`ð¬ÖwKh-7E¾"1åq‰DúqU,g_ X.•öyõE}8Á"3Ü!yÞ+ó|¯1ÙÉ’~XñŸ@h˜Õ¿m?‡º}Þ‘i,·Cíö6òÊfµŒÙEv3ød”O)#þxÃ/ZôüB—Kñõ‡‡,ìâ¼öZÖ³8'ýÕ XzÉ,¹#Ú ½«'Dø¦x7û(h>&ñ&‰¥…®Ÿ5½•Õ¸]M-“l^w™ "B:˜ŒYÉ+sBðz5Ï‹¯µ Ö¿Ô¼Oz^âh@‹É"Ãc9ù@óûÉXÑ/îÆ|3ý¦´Ï‰—ðiöñ6•s< w Z¬6‘I†ÞëÉíG÷¾Uäã‚yâ¯è¿-ìþÅãn×QyfÕ.4¨ôÕ†µ=ÙÔ ¤v›bšX¼Á(ò³æã®&ðÏìý ø6{yôWYµ¹·‡É·»g¶}ƒìVv‚A˜±Ÿ&ÊõózEýžôý;ÀwÞ½ñWˆµË5!«Z]]Ëg Õ…ßÚM×™Û[D ó›™D‡>ÜPÝoã9ÐãÑmn¼â9|E«=ÊÛøz&²ûhŽ(÷Ë)oµù;3€?{É 5Ï‹ðÜ|>ð®»á@5[Ͻ­¾‹ovLbF”g”r@†4²¿¹#­.µðNzÃJ{¯ø•5í!®>Íâ4{Xï¶H›%Œÿ£y^_CÄ|©¼7ðŸNð·‰!×ü=©è‘Ý[Ûj¶ZµÌ‘\KæË¢X$ŒÆeãËàÄ=9Ññ—ÁÛkz.¡ªëZµÍ–—qmyo¥“lmÞîß÷ÎÃæ†œÇ,cŽhÃ?5½Wâwü3¨ø+PÓ4ŸˆeÔ÷–%£y<ÙÒL€6ÂcÌyÀýàœsÞý±¼âû=zþËí“¡\ø‰‚Ëkv×|`%Ž(f’H¹1þêo(ò8à×£†–CÆúï‰áÕ58N³o¾£¦þëì—~\2G?ºóxõrœqU¼)ð¢è÷Zvã?\iíf–z}µÜ¶Åt¸Ô¾Iû6ò@ÌÞiÍx£ö€ÔôÏ‚·4Ÿ_ÞÉk’I¯ôò²Dúã4WRÄböYZ_úe]Gо"Íá†÷>&½ðõ¬"8tKdžK¹.¥a0²EûÉ c‰Hç“Ö²´ÙóÃúo¼]ᇻÔu Í%ί(·ŠââsQ¾LQć¨â 2µhÍðÂçRÕüú¾»}®ZøzYïKêI–öñ†-ä“ÉŽ(‡” Ø!Ö.àäǵ¯ø—ÀÚÏ…§Ô®õ}‘_^iDµ¥”¢ ¤#œfóÿ,„‡µ{EBʬê쎟óè*j(¢Š+ç«ïÉá)_Ðæ_üÔ#Öîôë=fÖ/êz¶“­ÞCqnòj"{m!Š8å-åí±[d‘ÿ=qÖºô?…ßt‹~­>–—6ói·×6]Åå˜äŠibçþýïëÍ~ø#]𠾯¤j7¶:–öû«Ý4ÚÂñ\/Ú'–yn|³Ì¼cü+Ò¨¢Š(¢Š(¢Š(®#Sÿ‘ËTÿ°}§þŒ»®Þ¸OþG-SþÁöŸú2î€>lñ‚añg‰’{¹T‚ËOˆZF8—÷óÚº}kÝWÑ|»›èQ¯­m|©u;¹bò¥–(¿ÕM1­‡øm£ø’ÚÇTOxƒA»–Âî#Ò^ÔA/”=%†_çV´O…ºN“¬Xê7¾,ñ6½-•ÌwQ[j-o䉣‹ˆíbí\\“ç™ñ'Å @ñÿ‰t Yoî5ÝEµyækh–â Oš!õýÕSð§í¯¤1ÞøÆÛAÓ4»­?_š+­:y¥’i“ùùÞ±W­Í øVïP—R›Lµ–ñîmõnÌó-°ÄsÛ*ü)à§µ†ÙôK!…/A4=ãþ>‡ý¶®Ð<«Ãß´/Šõ¸WBmÊׯ3xŽ ØßÁuijì¾Ød– }Ÿ*ºÖ=‡í;ã]nò-/Hð\ZƧcϨÇaÔ‘MåêZ~ï1bøó›ýw­w¾'ø7á¯ÜøoÃvÚF…kusËý»N–ýg–2 D: ã<ËV¼7ðCá÷‡<5á&m. jmd6W×Pí˜<’ù²ŸÜ€<³.?r(‡_u¯øÛĺˆlíôK«/6k­æ/gçyBìÊu4Gþ™W§W) ø_ÂÕõ[IÒá°Õo›}åÒ,²ÌÇýnIï[ÚÖcÿù €4*Þ‘ÿ!úV'ö½§÷ÿòhé:µ¡Ô{ÿË:ëéJÎþÕ´þÿþC£ûVÓûçþýЂ÷¯ý¥g×4ƒž'ñ‡ÿ°…ÏóÕkès¯÷TxyñÏz( t¢¿->°C\-ïëÚEµåî,—w–vQÍ$WÓFÎbÄ°È J›ùÄòI#©ó?餲·9ëôœðÀ øQ-„rê·wö÷ÎÚ”ú¥Ò\ÌÒyyY¤IA™—î¥$` õj+Ö39­Â:W†[S:}´–«}q%ÔÈ—:™ýdˆ„þï9ÿ–xÖDŸ 4»×.Êj°Üë y4:íôEÌcŒboÝÓ÷x®«RÔ-4{ ‹ÛÛ˜ílíãó%ši<¸ã×ô«ËÌqÉ£Æø‘3÷è:×úM–…‡…¼:DvâÊ+5P!(Ð`cÊÚü ðm¬ZXƒEHWGŠÖ5‚êTˆ-£ï€:‰1/’ü7$×ccªØß\ÝÃmyÄörî)xÎ:IVf½‚;ˆàyPM &8ËòôÃÞü ð­í¬=¥ôvð^}¾4´Õ¯mÑn>ÿš‚9Grz{Ö—‡üáÿ ë:Ž£¦iâÆêõ|¹&¹1‰O-#rDKæM)Û$wÀ=­Ý½ôBh%ŽXHÿYäUªàåøMá[oûv}ÌÔÖsv$k‰|‘)ò³/”dòÿåŒ]¿å¥‡á_…­µ‹MR×F1ßÚLn Ù<¢$r%g•æÜþú^q‘æž•ÝÑ@î»àíıÜjš|7Sˆ~Î ÏÖ1,rÿ#ˆý@®t|"𮟥ÝYG¥:ZÜKÌÐÞ܉‘¢Ë1ÊÍËÇqÆ3Æìþ9ðô­“ëŸl¹Ô—~g[¡œbÿ®žW=¶­/­¯C±Ë²C†6ÿ–Åfø“Â:G‹ìÃYÓ×P²'>L®ûsøW8ß|fºø}3t²Ç*yÒl“Ì9–o/ÌÀ”ž³æç½t¶ö›â ý+¸åÔl#ŠâêÛþy$»¼¯ýjÍŽ«c}swµäÙÈ#¸H¤ã8é'½s:oà è&F´Òù³ÅtÏ,òM,³G!–9<É$$‘$„òy&¢ÿ…QáŸ.ÙÎê Ì·Q5ž¥uFeæ@†9AŽ#>P".>íwôPáÿxÂúΣ©iºØn¯—d³ ‰d&1#Iå¤nH‰|É¥;cdŽø¯ÅŸùfÿ®7úI-wUÂ|Xÿ‘foúãwÿ¤’Ð%¥ë‚?ÚVûM›T´¹øsÌHso'Ÿò²:úâ¼óÀüzžÒõ]JhN}óŒ×(>6|Fñxe¬·ö®¡ÌÜ$yØ÷¯«ntm?P‚âÚæÊÖæ`GYG¤ƒ¡©K²‘•¤ Ã(ŒaBcãòn•ñëâ'‹otƒo>¢gDÑ5m¨Éio£-Ôæb$“Íò·~èy}ýk×umcTñ'Äí\éW—Ú¬%{…¶˜ˆ¯µI¢% ˜ó$0‘'•ž·žØ¯M›ÃÚUɲyôÛ) ͱ’Ú2mò1û£ÿ,ÿ Ë„xsÁ×ðip‹}>çÄZ„î°óÝÝ4m<ÇêcŒ’}…節_üwâM~ÊËÅ~$·º³¼ÓÝÂiÑZýžoìý6øÍ×þ¢ÅŽŸ¹Ï\Ö¦…ñKPðÿì蚦›y¥E Ñ`ø‹¦%®¤÷ßkñ›ÚÚnã´™^k‚Ç  < Ó‡Äâ·~h±<Ú&¿âë85mfm2I šÂÆ0²Ü¼G"X¼æ1EySÝkÛ…ôQ¤Ç¥diãNCº;²Gäí1úVn¿?‡|=ÿŒ5coa'Ù¡µ»Ôf2Ãæ·“>ždÍ­_ixŠ= Kû_í©-ÍÚiÍ8ûCć`Ó?çŠñ? |S×õ Æ$›Z³ºÕô×ÖÛáõ´1 Õ­Œ¢óï|ÉV)ÏüöéÀ¯}° ÄþR‹‚žX˜'ï6Uhô=5'ÔÒÊÕoÞ?-ï–èdêGµ|°ß¼c…5ÅÒü]¥ø¢TÓ|?|ºü1˜mf¾Ô S[É Rôò€À¼ù³ÉÁ«ßõOkŸ~/é·~$¹Žï@Ô§Ó[RµŠ8å»¶x¡“Êc°âlf,tõô­®‰¦éÖÆ4Û;{WÊÑGqÆÒ²àw>½k;Çî‡àkZγi£ÚC-ÅèÆ]ýÏîÀýæsøÑ ‰åì¡øW ü1Ò-/ôû{ =zÎÑ´óeio Dòq*ÄŠ¢/+·•\çÄŸŒ^%Ñüy©Áeâ=/E·Ó5ÝM³Ðn­cyuX/d‡Í»I'>lñ~ë§Ùæ=pkÙµQáßézN«¬x\ß—K{uÕ4}÷PùC¤‰˜‡¯j£â„šW‰¼Q¦êÚÅܶúzÛy:$fŽÕZ)<ȉÄ^qýæ?ve1~íxã•öŒÌȵ;ïü_Ôf°½¸ÃÞ … – iÂE©êrÇó,¸à¬þšÌ{Ã^-ãêÞ9ý–{™ù–y~¹$Ÿ¨ª¾4Õ|3ðëÃ^#ñN³gmŒVâmBâ/2I#3ËŠf‡Žx¯âO|-á?‹–ñêö:‡ˆ¼9¨éðéšÍÆž©[”„”’5$/Í—¿qî+OâŠ|Sà=KÃ~¸ñÕ®›o}g©ßÝx—P°…ZÝ`Úyg~ðK3ŒþçŠ÷Y,,åK”k{y¹Œþ»ýi5 &ÏV„C}imy?™åÜD%\ôñÏ‹ÿh/‰íe·i©Ùèjú%­ìš4šgœ_ì‹­Façö­D?ö×é^áoâ$‹ö“]>}NÖawá©'ŠÖCo ¸ˆyYããÿ×^¥>›atXÜZ[ÌÎÜ™aF$ðŸáX-¡øy|W½¼9juÆícRþÎLCŒý£ËÿYϯ—Úç-köŽñl~¶’×SÓSÄqø_ÅZ•à[_4Áye2Åh|¯®áƒéèk¦øñ§ZðëøÂïÇZÌ®Ÿ£Zß\,–öf˜ ]J{@3æy¢¹éœz׿Çáí%fº•të1qs¸Ü°‚=ònÿžž¿Z‹Sð®«ÇZ†™kr‰qÚ£D0&Š_9[ê%ýà>¼ÓÇõ_|O¾ø+¦—â)´¿_¼šýÕÅɘZÉ(ËZÀqÊÅ‘ Ä82ðÜTZV¿7Íæ¯~ÂþãI±Ô®®„Rq$±O¨K(üOó¯[ÿ…áæÓ¯µí›O±ÙÜËkq6üì–#ûØÿ WŒø–ÒóYÒ|[g¥§Û¯®t½r h£ÿ–“K>£åŸzR[Á´¼Þ#yZ÷ÂòÛÙGòý§M¾7!¦ï»˜>*ø}¼!¬øžîyl4ÍÍûl·äÂ#ˆJN#Ïü³“ü÷ùÎÿS—QÕQ_x‚Ú&˜—³›K¿ó¤näÅäûm]†àÍsÆß³·Äí"ÇF¹ÓnõÃqk¥é·àÛþƒm>fp<Ø¥äû×%9Íî\O\ðgÅ øî[ØtT »h¼ë«;ëyl®àŒñæfŽ)D~’ŠëF¹§}žƒ}•7ɆAûÊù›â‡Áïˆ_a“P}6ͧxz]'OÓeÔc’âù¦¸´žxî bhbŒ¥ ‹Îÿ\Ç=+ ûà·¤øwC›ÃþÕ‰×S»¾·Ä’h—6Rϸ¶ˆEňºYâQƒÏï[„Hiô^L­úœÚ}Çœ9w†(åÆ;’có­}#ÆZ&¿áý;]³Ô-äÓ/âŠ[[‘ à yŸóÖ—ðĶŸüq¯\ÚY_iÞ&ºÕmm–âh·èé-¼Q­ä8ÿžÞW•0½?º9 Íà®»„ü ×½'T‡ÃÚmÖ‘{ᆺ²HnneŽÓT„ÅȆaûÏÞâ|cƒª¥Ô¬á½ŽÒK¸#º“ý\>`ó­hWÃZÏÁjÞ*Ôü‰iâ?Úx'öÆ“ÝGš=ÂÜ^æî $~pHò¿çˆM}ÀÌ‚‘½‰${P´QEQEÃjÒãPbcÿû?õQçþZ]×s^w¯y?ð˜jÛöÈ.ÏïyóÒïÖ€>uÖtÝoZ×£ºÕÍ…­µ ‘lïæŽoõóÊê,5­ZÆëIŠ}BæHZêÒÎE»Ž/Þù²Åüñö«ðxŠîXu=U´·ŠöÖ–ý>YÿÕúe5X³øqâ¹5K õ}jÎK+K˜îÞßOÒä‚YLCÍçÍš¸y*{@9«ßŠÓøCÄnuYá›OÑçÒÒÚÖâòÒÌB$‡÷¿¾›ÉŠ¥¶ý¢´Kÿ ]x‚×O½šÒßÃâ¾L@˜’i¢–ùkûšÞñÀ½Ä> ¸Ö^mgNÕçžÚô\i³•0Íl&†)A–çÊ—ÉúV^¥û2økRðõ†ž!°µµÒî4—žÎö/6úÒâ_6X¦>O•þº»€¦?hÝ.ÏY¹‡SÑu-7F‚ú÷O̯ F¹†×íSG;ÎÁˆʳõß‹,:Ÿ€n,|!©A­s:[›OµÝÚýŽYa•›ûšî®>øzúoqcuwj5kfH&—‰¦žÖkYâ?ôÇÊ›¥3@ø9a M¡ÎujÃBrúrjw‘MölÅåîÕy4ÁŸÚ×Á¿ÚZ%¤"}š”VrÈe»µŠkµËåÈ%—ΛùåW®¿hxì¼7·7…µk+­ZmÂæ{ÛHb¹’¦‡&YeýÌ_¹é5tžø¤ø.çK¹Ðo¼C£ý–(a¹[+˜Öø¡“̇ÎóbÉÿ]ÿ,¼š¹uðfÆ_Â#k­éº8–æwK'‰e˜I,ÒËóa›÷9›¥uqŸ6Õ%1ùeãó|¬cô5«£ÿÈE+#Iðô~Ñ´Ý*ÂÒkm?N¶†ÖÕ÷¡a‡ ­ÍÎàj1þîOû÷@ qF*Agsÿ<äÿ¿t¿c¹ÿžrߺ‹ç¹ÿ`™?ôtUè_c¹ÿžrߺóÏŠpÉ®oþa/ÿ£¡­iÿʧÀxçŠ?Ö7Ôÿ:ô¯ƒ¿ñãàÿû\ÿ=V¼×ÅëêzWÁßøñðý„.ž«_Aº£ÃËÿŽ{ÐéE¥ùiõ‚•ó&§ðG⻊'Óõ`ë«Ga>tþ ½µ"Ö(¬üÍ7d_êVO"lÝ©ó¿|s_Mž•SÃò,hßõçþ‹ë`~لψõØÿã=õ¤­oñ¡¼ko*>&Ô@¶tDÄ1ÄžT>l"SÎJÙÿ†VøÕ§M§ÅaãuŽÎËX:…£É¯]Í(ŒÍh|©¼Ø†G”xé_nQ^¹™ð¯ˆd?Š·Þ»ðûø¤j–×¶" í¯üEzCÝKgu Ó´§“–h“Ò\د©| á{ßêZ¬×VÑ%½Ü:|PÉ¥5Á”Gn"}ÑH<¨qÛÉÿYÕ¹¯F¢€>>øû4üJñÄïøÃž!´Ò,uã<ÑËo¬ÞBe'IqG,1Åå%M挟܊ÕÐ?g?/Žüâß­ž©¨Z_kÿÚ~^½vMµ½ýÌsÛ,9¼#Ê8‚ ⾬¤¥p‰­¿e?ˆ>ð6‡áßêÐh‹§Þê{ÓÄñü×7b[}CŽ­ òè~YI9#ì?>ø·áÏ‹|G}­kmªéš¤.Â95î¿Ò¡y>vKÄèÓÁþX ÷š)åøsâ=+⎷â+Ÿ<×›åxv $¹‚bzM,³dÆßôÊ,(÷¯.øÍðâ?¾*jž!Ðüq¨hº|ºRÚXÃm¬5´voåÌ$2@ —ÎÉ’#þº†¾¨¢€>Aøoû>üGø}«hsÛA¥¬VÞ(½Ö¯íGŠïdYOa¨‚BahýìBoßg˜Gs^¡ðáE×ÁûiMiZj:õÖ§gyü÷XæžI¿{£÷rÇ»‚|ÃÉæ½¶Š\À|«âxÏâо'¯†ÿ¶ô­&Kí!¢´Õuý2=Iá–oí­ä˜aš/$y°ŽHª_gˆÿþ+i~#ÖüIm¨Yê·Pj·mq¨´zp³Ù5¯”!Þ›Íê#¥}qID}Њ(¦\'Åùfÿ®7úI-wY®âÇü‹3׿ý$–€<7Aø¬Ø|~—ÄZ–¿}pu¹u[oYÝi)n¶†?ÜÚÌïö‡—Ö/"3åw§øà—Šô›¯Ëq¢Å¢¶¹¥Þ[øzOµE ð›,˜³@²ò²ÇÉÂäy}Z½Zßã÷…%ñ’xk̽ñ¤®¤mÏÙÿËɵÿÏP=¿4žñÝì0O4 ¤ZËq«µÔ>QÒ¼·)¶çŸÝ·Ë)Ç¢š>È}£‘ð—Ã!'ÁønËáܵMKF“K•„öÇûFçì²Eç4°I)>žlß½#®jÍÅš„ kz‡ÇÉ|I¨éúþ¡æjú}í–µ¥Üé „Åxd7ý¬EçE)Ä9óDÇ rk§øûUèš7û­sö:ޝ¨Üxy¼Ah¿a‘൉ƒ$2ÜÿÏ8üÈŸ”Ÿzëoÿh é>1OܵÓÝ-Õ¶Ÿ=ìVÙ´‚êãT2IÓ?½„qÿ=Gá¨HôNþ=6ÊâîMæ8Ìq o'éë_,K­1›Æ^9¼‡SÔ4é§Ž'·S,;-|Ü mb Üy‹!þ.>—¯ øñÚëÃ^øªiT:…Ÿ„ͯÛe“Î[«™eýý±Š.u°ò{Ëìjⵯ׶ϪÛÙx ßVð<6‹ZÁ±Ék:…‰Ó¼—ò¡’_$âè™ü©|¡ûšçì·ªkÑø=µï éÚÀÓ¬SÏÃO¶}dÜ\Z!i9X­q#®+Ýßãï‡WNûY²ÖøÖE2iÎo¤¹X|ã—ÿ\{׬mKö¡ðmŠhæÞÃ^Õ_SŒ£ÓôÉ%x¥3ýE)ÿ–RùÙ‹¹ç½x¥—ìÕâËëÏ \x‚Ã]½¿¶ÑtKk[½÷Hß¡=¬•D·1I4GÍù–g÷½H¯wÓ4mCƾ9ñ·‰ïlÌ mk7†tk£°ÁÝs8öšo*?¥ºÒMûPx&Þm;ý"ú[+›+;ùïc·Ì]sÚŽtHÅt:çÄ}Çz7‡-íìïiwªê™J}ŠÊ<3?ÄLÞTb?MÇ8ˆä—÷‚'€| ýu?…𥶩ÿ¢ÂÚkÙ_YZIjÍwÒôÈ|–Ì‚<}ª ÏlØÖž‹ðïÅwß³¥ß‚G.ü5ö-~KÆÐ¦»±†ßRÓdÕ&»–¶¹”FŸf"Þùc§lãѾ~Ò>øtöZ>™®Ã9´{Ø¢¾ÓþÌÒ~æ ŒQäÿ­ò®à“ôׯ¦–‹ñ—O ÿá1ÖLæÔ'Ó–Ò )|ó9¼kHí?òÒ_4»r;rhÏîø˜å1cÇï$ýô»yÆ®ËÅ߳ܚ¿Â_Š^¶ðn“2]kO¨x_K•mDàÇ&ˆcŸ4L}F}ëVÚGT¹ø'yãø|w¤Ok¨Ãiö Ó笠ê?d”Db<É÷¿]ׇ¾5h:ÝÖŸk¾¡c{«O¡‹;Û=’Gu >syœñû ^ôÇügøz¶ëðòß S‡ÃºÌÖÒéi–ɦCŸ¼†>Xš/7þ™­yGÆ_ ¤ßÜ^xjÇSñ±®øfÿK½›Qµ{Í&Ö+¨L¶¢ØÉçqäÏ6bÄI'þXW³\þÕ^‰´ÿ²éúþ«ý¤Åý¦I!®¥µôýä2þ¬½'ö›Ñfñγ öŸ:hC£ÜXx†=ä‰`¿„¾Õ)ÿWûÌ~c4D›ÂÚ>§âˆ~)ñ–£nÖßgi|;¡A<[ŒÊÃϹõÞa×8¼}¾k| ñÏ… øq·/´K:óÄËsc¿Är´Ÿ3yË'œrrs?–cÎJöí㟇¼Aâ¹4-µX.?´o4ˆnn-<«i®-IG™äeN>†Ÿá:WŠõ}OI·Ò5‹]fÆØ_gÞÚùSOÄ%žz{u ,ñ‡À]BãÁ¿ü-¡øbÎ×Þ"Ôtû3Nµš(!pu&LG°Î¥ïŒ­§ávоéž&ðµ¤Z’Oá[(í-`ûtâ/*ô¤’E˜Ä3)#÷ƒÍ$Ílø‡âŽºß³å÷Ä-âÊ+ë(.o¼›ý6Q±Å,¿¹1 ¿tÿ&<ß§§wã‰ÚÃùt+KË;ëýG\y!±°Òí¼Ù]Ò3$‡ò •üoû&ø›\²Žkß iþ'ñ:-®›ý«-Ì~uİèWÐnÝ3“ÿmi¡—®qív¾Õ,ÿifñ? ï!ÓîtFÒo|DŸÙŠ×.f„²‘uçIØÀÅ‘éÚ»¿ˆ^;>Ñ´si§>£«ë—öÚ~Ÿ¥É7Ù䑤ùŸ'Ëò¡Y¥=ÿuëXIñÏNoŒÀCNfØeþÕ2GöS'üñó7+ýf9Æ9£íàZ—À¯êþÿ„;1hþñvˆ—’ßÙ4W³_ÜC%°€ |ÌK’Þi‡Å]7ƒ>x‹àl~,Ô<+á[[ÝV;› [cC%̺µÁ°iÄ@³Aô‰Hàƒ_YQ@G¬~Ï^ñÂ-/áþ²&Ôô{”<òŸÞ\N3¾ysÕŒ§Í?í}++áý•¶‡ã K{döVm·E¢ŽóPú~Uî5â¾ÿ‘ÆçgúÍÚ—þ–j8 A¸ñÔ6°ù’Y\—~>ÑkÓþÿTºWt½[ÃÒkh6¶‰ ²]/ËœHO°Å|øŸü6œ¶÷Þ#†ÞâHAº´–牛ÉýÕijÚMÏŠ?dÚiv—º¬Ú®Ÿ¨­¤Ú¼“\ù¤ãdGžIàP(žÅkñ#Â7†²x“L¾ó¥1!³¼Ž\¸„ÍÝŸùç?JÙð׈ôÿh:vµ¥\ý§L¾n­îü´ŒŒƒú׈\øÄ^ºð.a}yª$º•ËjgOÓ£°·H¿³& ÊÚ€¸ó¼Œy™Ç ¯!Ð<9ñ"{φútÚ¿‹<¦ZøwF´²Z¥u‘1Q]Em¸ÿÙZ8°Q¨Go v³€&—˜D>hòÔœóŽ}ÃNK¨l`†òá/ï•"Žæî8¼£,½æò¨qŃ¥-oHÿ‚}*¥[Ò?ä ŸJéiJZCÒ€{×ñ;LJáÇ‚õ/®¨ëía™ý¥'™4ŸJì½sÞ4ðÚx³ÂÖ„Óý•5;)lZQ™å‰c1œƳ˜¢yoŠi|1´öþÔ5K+} ßÄšíÌGéV2ç0þøá&>\yÿUïSøããÌžñž«¦Ùi«¬XZ[éÐAöy¶Íuª^Ͳ xÙ¿w´B<Ò}ÇãKųtÞ"†Þζ™ayáÛo øŠÙ,<éXÂÕˆ™?ÑŸ÷³Œâ^&¨t?ƒzŸŠ¼¨ÝêîþñŽ¥â7ñS¬q]›'†a”N$ŤQ)þ9y­ +Þ~Ôê¶²‹oê7z¶™m¨ÝkvòÞ7Ó£²˜C7Ïÿ-¹“+?^k®Ö¾)j·^4]+ÂÚ/ü$ÐÁ fu†Xávy¦Ž;H„²~ìyŠ.%ÿ¶מ3Ä_²u—‹¼%káýsQÑõX.u ½C[¾»ðôS]ÝÍrs)µ’Y´$~ï)ÆkJËáGŠuÏüG{}B?x›Å‹ÛÚß­¿Ú%°Ó ?f´]‘Kt­'yǯJ "ƒö•¹Õ´»4¿^ê>)¸ºÔá›BMBÞ4…lX-Ì¿i?»’2Ó@|Íí]6ñãNñŒ<¤éz©q¤ø·J}^Ë]–/*Ø/—戽|ͤ;sX2|ñ G‡Kñ~•á@‚ãN·»Ò<;‹5°œBÏnÖ²ÝJw‰a†_7Ìê:rMuV?l4=CáÉÓ.ä‹OðU„úu¥¬‘ù²Ln± ÍœÆ@P}èÇÿ‰éã¿ø¿J>ÕtY|;s¸—TË7K 8–4íîøõÁé^\G…ü ‡¼iâÿ­éžO½œnb[ýždÎ3ÏN•ÛÐHzRÒ”Óþ:òŽ_ò Ö?ì 'þн`ÿŸÎ¼Ÿãüƒ5ûIÿ£â«¥ñ£)üÎÞ+Ö¶x…ôï..ð%Ìß÷ê½sàïüxø?þÂ?ÏU¯.ñ=Œk¨=Ø{0“ɺ˜Åÿ~¿ÔרüÿÿØBçùêµôY×ûª<<¿øç½”P:Q_–ŸX!é^gá½C\»ð½¥ôŽŸ¦iq³Ä&Ie”yGÊçëågó¯L¯ Ñõ{}?B´Ó¯m¯R{I¤ºŠ{;Ye‹>d²Ãþ«þº×«ûf5|iãxCY‚ÄÝé÷!­|ß7È—ÿVü-ÿÏM;þù—ÿVÄûÛÏë±]iºv¤ð%·”%6_üj¹?ìÝk?ò Ô¿ðëÿWÞ`há*Pýáàb«W§?Ýž™ÿ Ä¿ó×Nÿ¾eÿãÔÂßñ/üõÓ¿ï™øõyŸöfµÿ@ÍKÿ.¿øÕÙš×ý5/üºÿãUÝõls—ÛbÏLÿ…¿â_ùë§ß2ÿñê?áoø—þzéß÷Ì¿üz¼Ïû3Zÿ f¥ÿ€_üjìÍkþš—þ]ñª>­€îÛzgü-ÿÿÏ];þù—ÿQÿ Ä¿ó×Nÿ¾eÿãÕæÙš×ý5/üºÿãTfk_ô Ô¿ðëÿQõlpöسÓ?áoø—þzéß÷Ì¿üzø[þ%ÿžºwýó/ÿ¯3þÌÖ¿è©à×ÿ£û3Zÿ f¥ÿ€_üj«`;‡¶Åž™ÿ Ä¿ó×Nÿ¾eÿãÔÂßñ/üõÓ¿ï™øõyŸöfµÿ@ÍKÿ.¿øÕÙš×ý5/üºÿãT}[Ü=¶,ôÏø[þ%ÿžºwýó/ÿ£þÿ‰ç®ÿ|ËÿÇ«Ìÿ³5¯új_øuÿƨþÌÖ¿è©à×ÿ£êØáí±g¦Âßñ/üõÓ¿ï™øõð·üKÿ=tïûæ_þ=^gý™­Ð3RÿÀ ¯þ5Gönµÿ@ÍKÿ.¿øÕVÀwm‹=/þÿ‰?ç®ÿ|ËÿǪ_øMµÙjv·æÜÅ…Ô¿¹ùe-yönµÿ@ÍKÿ.¿øÕt^ µÔ-¤Ö~Ûms _Ø÷_ë­¤‡þYÓZâÅa°”èOÙ4+Wœÿxz§‡þÛx[Ç3x‹M¿Š {«Ù5 «Ó-§s<ÉûÓÔƒÍHÏ÷Æ™¢~ÏZFˆúùŸWÔuVñ=µÅ·‰åÈIJà,ÌF<³A¢><¼ákJÕ5ˆ~?\è÷·6SØ·†þßoåÛ˱Ÿ9#"Rf1ÉÓ¨Š"+ÎüûAxÆãöZ¯ˆ-teu jºä6š5¤25”«„I$Óy¾h#ž?Çæ~Éî­¡ü6¼‡ÁzŸ…|Aâ{Ïé7v#O =¬vòÇnbòˆó"_ÞHz玽+<ü¹“Hðe»x¦áµÏ 4cª>¸m¹I"Æîäö®WÅsxƒÆ?³Çе-YÑu[™ü9.§bþŽæÐÛÉöIdˆ¬¾~eÏPuÇQÍoÅñçIºø?h’[O§x’ÞD»º›2H|½4Ý+FãƒåäœsÖ—Y‹qû,X†´ßêšu³øn? jSÇO5ýª¼’ “÷$ýôÿ7ý65ÐGð*ÂÏâ-ÇŠ4»ømZöîëÛIô»k‰%•!Š/’i›c_\,0Mx󼱘¬&~áuþ·¯‡ë^€>.øþ÷âíç‡ô_ Û]x_GÔtý.öâàÛ$Œ'Šd”Ë%üs/úÓöI<ï(ã­jÐZ˜»k Ÿ°…ñ÷ncó0kÎaø%gsðf?Þj“4“>¡ª@¿¾º½ó„ÓN}Úl·ã^¯_*|JñF¿â„?¼g¤j³ÙØKwi§xrdB±Û]Ç—'¸ó¦3!ÿ¦1¯­@â‚6Ú¦©©ëV:íæ›¯\ëQkÖ·±C­o2Z Ll#GåqƒëíTtÿÙ¯C±JòõmAÍ“Àååò˜ÜH·æþYã¬Óšç¯>/øÚÛPŸÃ&ãÃK¯Ÿ¦·ö ƒb¶öµ‘à‡Ÿõ8ó±žs\«~Ðÿu˜<5ý„Þ³ûYKk¹¯4ë«•–C«?Έ¤Ñ~ë½üh½Ò?f?Ã:LÚ>ª–¯ke§Ù\ý¯Iµ»k“i“ ¨ÒƒäËåðHÏQÇÜøgáü–wþ9ÔµK¤ŸQñ5×–eŒ`ÁgB(!¾–O¬Í^£~Ñÿ|]>™'‡ü-c=­¾‘£jz„.Ö±™~מR“ɨFaŒ û™s]­‹Ýj?Zßþ'Öµ‹KY'>)¶½¹Il­XØÁk b.%Y ,qŒD”’Elx7à ·îà¼ÑõÛ´½³µ{{iDPll­OûE/ÖCF“ðX~MáGIJßíÕ?¶´ûû[(­æ²»ûIºóF|ÀçÎl’sÆG¸æ¼Qñ3įkñ ƾ»€iš3Å hÖ×Je³¹ºD//eò¹>[HÐðxû<¾µCÁÿ<}ã üE´Õ¯|=¥¤i’\Ú^A¥\ˆqæ¥k(–µùŸó‰¿å¯~”sèšïÂc[:¤|cu‹4µ$:âið•1Ü óbû9ÈÇî¢>¼~Nð·ÁÛ? xƒÃApÃþÑF›£YIÌ‘Îÿ,×2±ÆXĪ3ÿM&õÎ^|Cñ §†~iººf“©ø¶Þ57úÌ2]Ám‹Ÿhr¼Òþï™yÆI=k'â‡Çx*ÿ\K+¯ ¼>ðÅ¿ˆ/Zê Kjí4²Çå[/îFa¦æxG~WÚè!mn<µˆü´§ïôúV•£j–zαws­]ßÙ^~˦M IŒbQ…'šy>a=8À®OÄ ¾ñ'ÅÍÃDòZXèÐg^xˆ vÙÛ{‰NÇþ¸{×ðûãÖ»â?Šz6‰yoi{áO[jw:F©ˆ³b,§ ʽÜÒË×91CޏíL®ÛàL àkÿͯÝÏ¢M©.§kÙâ[}ö±“þZ~÷¹æ¦¿øo>§>©a­ÝXë âñ7†æX&kO²„g¨òx®Jø½ãÛ?ÙúçâV°Þ¹–âÞF´Ótý:êÞ;yZëÊŠifÌL@6LD_J¹ãù¼w·ð¿N‹ÄÚ$ÚåοsÔ-l.!°ò×NšOžÛí›ä€UÙRÃKx­¢ñF tI,´‹+ý0ÛE‹ÄÓ3 8€GJËâWˆüEà/…^#žM.í?[iÚ­ª[\«<¿i’ØÉ¢ëÿ«ÿW ›®HÍmxßãˆ<9ñ7þh ÓeÖµ{5¼6e†B&¶y Ôš_Þg0à Ò1÷¢ë@$?4ëk»;¯í ¶k]S×Àqº[¸æ‰ãú;céX>ý›m¾k «éÚG©Ã `ÛM‘AbFþd‚ <Ùt9ïéXžøÍãß|Mšðͬ^ MsQÑÌêm£¸ŒZ´Ñ 7 ö–C˜†aû(#Í8®¯ÂÚÞ½ãí[Ç>!Ñïœhпö7‡¢Ýû™¦„âæèñÎf&/QäIýê±á¯é§ü%ÕþêÚ寳£êQ\ÀÓ% ¶•IJË/OS/éGŒþÞøûÁ:n¬ø/^’æúïEµ˜ÜdÏîÈÄR|ßë"Ûʵ~ ÙxæËÁÁ> ÝXÝëßh”ïÓÆ"òüÊÈý õ+«/ hV6ó˶µâ=3H½ž|¹#µžê(äÇ9cˆ¿í­hè Žt^êøÝéÞÐÆ¤ÛÜÉæÊ¥Ïï®d$dÈÉ 1ç=¥þö+¿û4_h7Zyæ?,¾ßÞyyÎ?:òŒ7—Z?Ä„÷–zö£f·Þ"þ˽ÒáºÅ¬öïey/ïb=ÃE5æ‘|AñdŸå/õ bÿ„ðxwû¯ì¢ÄZF@Ùó<ͲKwæÿÓ|aðŸ[Ñ\έ{§ßé:Í›kBÅía+uwkϧýt»®ò¸mZ(åñŽ©¾(æÿ‰}ŸQÿM.èÀõ/‰ºî©ØhÚ^’÷âH˜¥ÿ–£ÿ ×WcñÍqkÖ“}k%ÌÑCö>ÒX¢–^çµqóÏq¥êr=Î…­êv·6Ö“[]éÚd·\y¾ÿUKiu>µ®hv¶×m jVÓÍ>§¤Éi QC/›(Ëï\<õ9À‰žðö¬Úf§â½'OÔV/8Ú]jqÇ0‡þzùU=ï¼?a¡ÙëW^"Ó­ôkÁ›MBkè¾Ë0ÿ¦2ךø—àÅÿˆ¼W®j¥t§†÷Ä~Õ¡[¥ýìQiòbäýs\¶·û9kw3ØÜCê[êÚýÂhë©Í§À-µ óóD<žŸ¹ÿ¦ÕÜ®øÿâÞà ôÛÛû{ZþîÖÎßL†ú/µ°–hb2˜¿í­Y·ø™ GojÚŽ«k¢5ÅôÖ–÷÷ñFn¤Šo'1~ú¼ž€zΙ©Ke£XhO¢]êšûßßÝÊ.ì#°XBÙãÉýî< ?í¹¬¿üñƽàûÍÆþÊ]:ö]a¥³ŽüÚ)šîóε›ÌÍçŸúc@Jnp^@})7¿÷ä¨ma1ÚÛG'U‚(ߎÕg­3{ÿ~J½£;ÿhÇóÉU*æÿ!è Þÿß’ïýù) qF(ÛßûòVˆÈo.aÿ]"^eõÆ ñwÆŸø3KñíïˆlÜøu"}RÎÊâ)n­côš?ùeô¯)ñÇìÇ>­â-í,­õ/Çá«?]i_ÚÓX-¬pJfv…D2ùË/\ÓNÖ>x²ú×â‹¥¾›£ø{XŽ[‹-:kÓtöȦ3äù°Å7ïLÐþû™úе§Äo ¿öH_éRÿlø–<:œY¿±ÏQïLÿ…™á\êãþÍ'vŒqª?öœx°>’Ÿùe^9㟀ÚÿÄ/ˆËâ½wK³¸µÕtûM;RÑàñ öÆÈCu4ÂH¥þûýqÿž?êzV•ïÁ=^?[[Û¦‰wªéþ6¼ñ\×O‹+À×“Í rIäðGŸ×þ{s@†>;øSÄ^mbç]Ót}6}^ïG²žóSŽ(ïäŠb3 ŸòÔuýk°ÿ„ŸLo7þ'6%ÙÓ_ý#ý]Øëÿ¦µó‰?g߈:ÏÃûÍ7ÒâµÕ&×§Ô4{-U¢¶CvÄÚþó쟿Šç~äù=Ekø[À7Ÿíjmî>ÓáÍÒÛRÕØÁ/”Þ ·´û$gÌ?ë¿q)—þÙA@ÝuñÃÖº¦¥¦®»gqªØ[}¦ïN‚ö3w~žPýè¬Ï ü`🋼#}G[¶¹¸‡O·ÑîfºH"š(e’hÿå—3EùÓ>*¶í'^dh®2§#ý|5æZ×Á?^|<Ðt$}ÇŠ-a¸x®]zò+ÉwæÅ¼žW›uäÄbÇŸþ»Éï^›ñbB4½qƒo-£HVOAçÅZSþ!•O€ñÏ­o©þué_ãÇÁÿö¹þz­y¯Š?Ö·Ôÿ:ô¯ƒ¿ñãàÿû\ÿ=V¾‡:ÿuG‡—ÿ÷¡ÒŠJ+òÓë¢òbÿž+ÿ~êZ1@ˆ¼˜¿çŠÿߺ<˜¿çŠÿߺ—bجEäÅÿáb/&/ùâŸ÷î&/ùâŸ÷î¥Å£™÷ˆ¼˜¿çŠߺ<˜¿çŠߺ—bŽgÜ,EäÅÿáb/&/ùâŸ÷î&/ùâŸ÷î¥Å£™÷ y1Ïÿ¿ty1Ïÿ¿u.(ÅϸX‹É‹þx§ýû£É‹þx§ýû©qF(æ}Åb/&/ùâŸ÷î¸ÿŠ*±ønPª‘&ìãþÝ%®ÐWñOþEÙ¿ëßþ’Ë]ç3&·øKàXüL¾'_ø~?YδºM¸¾2z™|¿3>ý~•¯má-Â=?HI÷CØqÅVµøEàkOKâ;Oh6^!™ä¹}f *Þ;Ö–O½)”Gæy§¹=rsšîóFh•Ò|¡èÞ¶ðÜThvѬIer†UÚ¿8ßægÏÏÔÕMOោ5{K‹-CÂ>¿µŸæžÞçM†h¤a,² 1ã‰&–NŠiORIísFhŒ_…> >ЗÃ%¼Ã¢ÿe[ý¿_õ>_—Ÿz¹{ð÷·óhò\øcE¹“Glé¯.›†ÄãþX’?uøbº|Ñšç´Ÿ i~¼ÕnlíÖÞ}Zëí·¥ßýkùb<ŸÁTU]/áç…t f]_OðÖ‹§jÒv¾´Ó¢ŠäîûäÊN{ó]^hÍd[øM¶ÒWIƒM´JH„Ke´bÔØF:jÈÒ>xWÖv¶ºG†tM&ÞÒs=¼VZl0¬2§™ú õè+®Í Rø3à-v+µ_økU†ÀI¢^i6óÇl®ûäXóòÁ~HHÉæ·O…41¨Øj £é¿oÓá6Öw"Î?6Ö,cÊŽLf5éÀâ·óFh”“á÷…ijx„xsJ‹Ä’Ãä¶·Œbü®:yÀy¼zfµ<9áÛ èz~‰¥[-ž•aÛÁn½ÀØ ×Í ®gÅ~°ñf.ªÆfµ™£9 åÈ’!ó#’?I˜?‡çÒæŒÐâï…ž ñíå½×м# x–öVÞ}cM·¹š!×™c8¦¾ÐSÅ?ð‘N!û?ÙÆ¶l£ûY‡9ò|ßõ˜ïŽžÕÐæŒÐcðÓöKâ’út7kâyšmZõóã¼&–Ñ6AÊ\ygŒS4¯„> ðþ‰©éZ…ô ZêP.“A±ŠÅääÄx$Woš3@ºf™m¤éÖ¶pGkempAmú¸£€¦ðåÞÿ’†?ë½ïþ–j5ì•ã~ÿ’†?ë½ïþ–j4ì”QEQEQEQEQEQEÄjò9jŸö´ÿÑ—uÛשÿÈåªØ>ÓÿF]Ðj?tO¦—¢M¢ÞjwÏa¤ÚŸùê+¶Ó¼gáÝN{XMµý¤×&(bŠóKº„‡ü²óe‹5ãߊü;៲뺥ž‘4ÚvŸ5¼·óEš/#þYTžñ—…u¿ho‡u«-Vùõ+mÖÚ}ǘ"X¦—õ®.yû@>ŠþÌ´ÿŸaTµ†ÒtM*ïSÔ¥¶³Ó­¢–[«É¤ò¢Š#ÿ-k¼oðÇÄŸüSâË ?QÌׇN…¨%ÔžRÛ aþÐýÙ›Êò|¯8K޵È[|&ñ¥þ—¯éhº¶Ÿ¬\xs[Ó|M«k:šÏÄ7ÓF±¶ýîqÁ91CåDqÇ»@ú¶ k+‹xç†Ý$‹ÍŠH†<ÊmävV³\ÉbŠ/6_zð [áÞ½â…Zn™¦iZ†‹áÉ®t«{Kb¶wŠ‚îÔÝÊ×Boõ~P–³”­ý—kÿ>Âì»_ùö~ô  ?Ù–¿óì+;Y»Ñü=¤ÝêZ¤Ö¶}´^d·—2yQEýtôÿÞ^õçÿ|޾x£G—I¶ÖæŸN¹–· Uî|³åé¥) %íWÇ^ Ðn4XoüC¡XË­ø—¥Î¡F÷€t ýïoZ[Ïø{Lñ%΃s°ø™kñĚŅ®·¦ëÚƒi¶ÞD³C%‹QÊãþZ\ýªoøïÍNêÿâO€4»mâÿž±ƒWÁÒå¹Õ¢Ž;ñŒæ dÄ߆j_xïÂÞÕ#°Ö¯íôéd±»Ôžâê_*(m¢0‰d–S+™¢ë^ ¬xoÄQüø_ðÿVðŽªú|ž·µñ æ™cöÛ¨)6iÈò䗜ʻž¸#¦T¿Ô¥ø­ñHðûø§S¶h¼5 i0ë ´mîÀõísH$çþ]p)é·_¾ÙøzÇÄ“xËÑxnîO*ÓR—V‡ì“IÙ"”Éåöè+rï]Ðíµ{*}JÆ=Rö eµ²k%š!É1ÄOïïÅ|åcàËß xšk>.F½¯gq¦Ciq>¥~`w½[iNmäÍ3Ò~r3]7„¾ ê^ñÀ©ßM¶—Vð÷‡åÒuÍb¼ü¶I q‰O$y â€=›KÕô cSÔ´û û+ÛÍ9ã‚òÚÖäK%±Ç $yýßJ×þ˵?òì+Ëþxx/â‡Äû‹]ÛHÑuIôû«SoŠ9äò_Γ¹”ŸÒ½~€(fZÿϰ£û.×þ}…_¤=(öe¨ÿ—e¯/øÓo¶™¬yiåÄ–NGýwнpÿŸÎ¼Ÿãüƒ5ûIÿ£â­)ÿÊá(ÿXßSüëÒ¾ÿǃÿì!süõZó_¬o©þué_ãÇÁÿö¹þz­}uþê/þ9ïC¥”Wå§ÖQ@Q@Q@Q@Q@Q@Q@Q@Q@®cÆVqêV¶–r‰ 72MžWýq–ºqX^(Ç™¥oÿŸƒÿ¢e®ŠÄ13Õ5R§Ä:¡9ë¶×ÿŒÑCþ† Oþùµÿã5Ó¬Çüìð_ìû?úXÿà$5ï&uú5?ûæ×ÿŒÑCþ† Oþùµÿã5ö}Ÿý¬ð?³ìÿècÿ€ÓLêô0j÷ͯÿ£:‡ý ŸýókÿÆj?ìû?úXÿà$4gÙÿÐ:Çÿ!  3¨ÐÁ©ÿß6¿üfŒêô0j÷ͯÿ¨ÿ³ìÿècÿ€ÑýŸgÿ@ëü†€$ΡÿC§ÿ|Úÿñš3¨ÐÁ©ÿß6¿üf£þϳÿ uþCGö}Ÿý¬ð“:‡ý ŸýókÿÆhΡÿC§ÿ|Úÿñšû>ÏþÖ?ø Ùöô±ÿÀHhLêô0j÷ͯÿ£:‡ý ŸýókÿÆj?ìû?úXÿà$4gÙÿÐ:Çÿ!  3¨ÐÁ©ÿß6¿üfŒêô0j÷ͯÿ¨ÿ³ìÿècÿ€ÑýŸgÿ@ëü†€$ΡÿC§ÿ|Úÿñš3¨ÐÁ©ÿß6¿üf£þϳÿ uþCGö}Ÿý¬ð“:‡ý ŸýókÿÆhΡÿC§ÿ|Úÿñšû>ÏþÖ?ø Ùöô±ÿÀHhLêô0j÷ͯÿ£:‡ý ŸýókÿÆj?ìû?úXÿà$5 Ï¥ZIåÉo¦Ãÿ]a†€-gPÿ¡ƒSÿ¾møÍÔ?è`Ôÿï›_þ3T>Ó£ÿÏ='þüCKöþyé?÷â½Cþ† Oþùµÿã4gPÿ¡ƒSÿ¾møÍQûNÿ<ôŸûñ iÑÿçž“ÿ~!  ÙÔ?è`Ôÿï›_þ3Fuú5?ûæ×ÿŒÕh[K¼“ËŽßN›þ¹C KýŸgÿ@ëü†€$ΡÿC§ÿ|Úÿñš3¨ÐÁ©ÿß6¿üf¨ý§F‹ï¦’íŒ4}§GÿžzOýø†€/gPÿ¡‡Tÿ¾møÍr>ÞÈßê¼yO] }5ý¿sýبþß¹þìuâ?³‡Š¯|Uá]I/µK¯]Ø\Å ž Y¢–Æøù<›Ib†ûõÜׯÐïíëîEKý¿sÇËBŠ¿ý¿sýتî®ÜB0R:ëš?ü„c ·ûvãû‘Ñý½qýÈê€éK@·n?¹)×n û‘ÕzWñß@O|ñmœ’j¨Ón¦+cu-¬‡Ê‹þzÅÖ‰„LþݸÃfXëÖšmž…l,lç†,z}´^VØq€]†;WÇ¿àf‚ÃûNIüBþÓSÀ¦Ån?w¬àùþO•÷nsöBHÿ–KŽ™¯\ðï‡ãøŸñ7Äwó]Áky*xbÒçLºòdkk2VäC0ìo%ž/ûcJ~àD÷ŸíëîÇYVºýžwk£Ãö+)gY$†Ò8ÌyUIdvÿYù“_ üD·¼o„Ÿ åaˆ§€C[®±ËF÷dAå MCþyÕë^!ÖôE>!xïÆl÷þðæká«}>ywK%Ã42݉{LæÖ"éÞZ`}?ý½sýØèþÞ¸þìuñÓ¼cá_ ÂC©C¯ü;oíéæƒA»’ïLÒõ9š­ìH¸Ä0ùË?å¯#šôx[\µñçÀ]WÄ“êóx™¼;skªÃuu!Šìõ-æEÓÍÎsøÐÒÿÛ×ÝŽíÛîE^)ðsA_ üRø½ sꤷZeÑžþæY™¼Øeæ#Ö½v€/nÜr*?·®?¹Q¢€.ÿnÜr:óŸ‹WÏ{¤kÌ»Ñ_ÿGC]Åp_ÿä­ÿØ&OýiOø†U>ÆüQþ±¾§ù×¥|ÿÿØBçùêµæ¾(ÿXßSüëÒ¾ÿǃÿì!süõZúëýÕ_üsÞ‡J((¯ËO¬ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€X^#ÿ+þ¾þ‰–·Eaxþ>t¯úø?ú&ZÞ‡ñ Jƒ¥”WÐQEÀ(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š+•¸ñ7‡| à»Ý_Ó’ì>¶¶H¶¢YIšò;HsÆå¤5ÕW|kðv¡ñà>« iº*x¦áµØnΑ,°Â·QC«G4Ñ~û÷_ꢚ€g½ŸhŠH:FŸÇý;EGü#º'ý4ÿüоGð÷ìóu©_øU5¿iÖ> O_ë£Á—RZ^A¢Ù¶ŸäÛ¨†9<~Õûï&(~´ý'àn¼Ÿ#º½ð¤R7Šooõ/ý¦“VÑç†hàÒš?7ÎÚ<Ø¢òŒb @%+S#ë_øGtOúiÿø ðŽèŸôÓÿð*øk¿³_Äoí]Nñ½½Ï†®$³ðΨ¦öÙáý*H®4¬7™n¥Yc<ñçžið~Î~5Ÿá†½á};Â#EðâëºV¥ƒ>”ºÖ¿mÈn¢½–7–Îñ²bòçúÒ?}“@^øÛF±±²Ó.­ì-à¹MN×2ÛD"ÿ–µ‰å’êÏîdŽÚQÿ«Î¾xJ?‡ü)áøíõ}:8µ$—û;]žÆ{«S-Ñ›Ê?a-ŸóÈB8¯Añ_ü‹Z·ýyKÿ¢¨NžÇÂú$}¢ OEÆkX†`x#\ð·íuiôýÙMÕ®´{5¤`ù°œKø^QñÏÁ7ž,ñÇ€u‰|ÄŸ iVwpÜxni­%Ô­ ¶¼]Ë2„2ß÷ç+Çþ!þÏ2ñ<ÂO@¼Õ¼=kâkP}L¹Ñ¥{äÔ#i?—¨)ˆy_¾‡÷£ÏýÖ>Ýÿ„{Eÿ FŸÿ€ñWšøgL³¸Õu4šÚ6çˈß+þ&•à‰ðSÇ)ñËÀZÒè÷riþ¸Ó­í|M,ÚYº}*+1ÔW×^gÛ'œË$¹òÿr}+×>è·ÞðVƒ¥ê‘¬ZžŸ¤[YÜÀ²y€KÅØ›õ Ñý…¦Ð>ûò(þÂÓ?èýù-õÍÕ¬{í¬žîoùåÞUgøBòò÷E³–îÁ„¥Ë/3{ÖC-ÿaiŸô‡þüŠ?°´ÏúÃÿ~E_ÅE$’™wl%  ¿ØZgýáÿ¿"ì-3þðÿß‘WâºÅPþÂÓ?èýùaiŸô‡þüŠ¿Š1@?°´ÏúÃÿ~EØZgýáÿ¿"¯âŒPì-3þðÿß‘Gö™ÿ@øïÈ«ø£Cû Lÿ |?÷äTÖ¶úsJÖÖÑA$‡2ùÕœQ@S{ᯇڅ–‰/ŠŸÃï¨I§Àc“DÒˆ±ô®‹Gø=áOÞ¥þ—¡é}â $¶ö@¥y^­áX|GâšY¯Y­ì´ÿôD›É‚OÜËj×Ò­_F×WÇÙ窞ý¡¼IâOk^/Ö¯t-ÃúG†´íRæ+}"æîY¥º²’lÆ~×þ«$~çÿ#q@ûý€?çàÿߪ?°üüûõ^¡|løâ¬4co¡è¾$—Åk Kw}¥Hè-[Nû_˜-!¼”yÙÿ¦ÔžøÕñC^ЄÂÏÃ:޽©ø^}{Jµ²‚[E‚K{¿&hdófý÷úúP¿`ùø?÷ê¯hú‚´Ÿóιo…^0oø.V[Øo[[É °“N+,S~ú/&Y¦Ç?ôØ×{¤ÈA>”kûÏÁÿ¿T`ùîïÕmÒÅþÁóÜÿߪ†r¥l)¯:øêº¯ü*_ÜèZõχ5;]:k˜õ H#–dòã2~ìIÇnõ2|¡°3.ì]¸ß÷¾o½õ¬o øNð–‰g¤h°géÖŠñÁÏ&Õ’_2AûÌðÏ¥|ÛñâˆcÑ®ï›Åך߇¾iþ#ÒìmåŒ.µ)ŸÍI|Þe0Á–{ÍŸJê¼C¬Xë¾ ø‘ã w^Õü;¥h©gá{Y´)CÎ'O.æå"Är1§š8N:ù?P@¦“"+y"–ûÄ!ýkÃ^°ð„¶Ú4ÙÐÉq-Û€Ï!ie›ÍšLÈO$“ù×ÌÞ'×¼qáßéúg‰|o¨xbûKð¦¯âç7–Ëqsqÿ¸·º”þD&1/•€O9Áõ“x‘|m­x‡Å¾+Õ5èÞðͽ¥ÕÆœá$‚öò8nï0G˜A#¶¿úÇèhèì¹wïûl»ñÛNqéQ…e,º/ƒ_0Cyã8ì|á¿xïYð–Ÿ}¹ª®¢×6é©GF°Ú]Lc1y‹ÓË/'˜zžµÒx7ž.×<[ð/YÖu¹íí¼Oá™§Ô¼=ºE]U˜Ë'~¬@{ÙÑ P¦åŠ®p | õ¤þÁóÜÿߪó„rëüRø©¤k~#ŸÄ1Ù]iòZ$GVQË Ÿ*$O\÷¯j  Oìÿ=Ïýú£ûÏsÿ~«nÐ'öÿžçþýWš|`±:f°ù¾fŠýë´Uì‡üþuäÿyÓ5ûIÿ£â­)ëPʧÀxGŠ?Ö7Ôÿ:ô¯ƒ¿ñãàÿû\ÿ=V¼×ÅëêzWÁßøñðý„.ž«_Cº£ÃËÿŽ{ÐéE¥ùiõEPEPEPEPEPEPEPEPEP+ Äñó¥×ÁÿÑ2Öè¬/ÿÇΕÿ_ÿDË[Ðþ!‰Pt¢ÒŠú (¢‹€QE\Š(¢àQE¢Š(¸QEÀ(¢Š.EQp (¢‹€Vеm>k‘§j–Z¼žo•ug,ÞWþF­ú8¢à`}ƒÄ¿ôÒðS'ÿ£ì%ÿ Ö“ÿ‚™?øõoÑEÀÀû‰è5¤ÿà¦Oþ=GØ-ç‡ô‹Étvݧ½Î­dpa$~èàÆ+ŸÒ>øfÃáô>ÕôÛéBâK©WÄVÑ^}¢êYŒ­4‰·ac,¬Ü×ZôZ(‡‹á7‚•§i±ø?Ãë¥é³ »-=t›q œØâh£òÿw/=G5KMøQ YxWXÐu ;iú¾¥uª_E­[ÅuijNeÛ$dm)ñÿ\—¿_E¢€8Að{ÀcBü!>þÁ?kGöM¿ÙDÙÏåyXó}ñŸzé.4{+«Û{Ù¬í§¾³ßä\<ÓCæuòØýÌ÷õÇ5¯EcÙèö—W—vö–ñ]]ùM5ÌQm’ã`ÄfI:ÉÀÏLâ¶(¢€ (¢€#zòoŽò Õÿì 'þнeëÊ>6C$ún£Qù²d¿þŽŠ´§ñ¢gðâõõ5é_x±ðý„.÷+\v½à_M#ìC¯üû í¾ZÉikáKyã’)#Ô.|Ø¥ÿ¸µ{™½ju0«ÙÌñptêS­ïžê(¢ŠüÈúp¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(ÅV»°¶¾ˆGuoÔ~“Eæÿ:³F(ØFoö—ÿ@«?üŽøGô¿úYÿà4u¥ÍÖ¾ÕŠÆoü#ú_ý¬ÿð:?áÒÿègÿ€ÑÖ—4sGµac7þý/þVø ðéô ³ÿÀhëKš9£Ú°±›ÿþ—ÿ@«?üŽøGô¿úYÿà4u¥ÍÑíXXÍÿ„Kÿ UŸþGGü#ú_ý¬ÿð:ÒæŽhö¬,fÿÂ?¥ÿÐ*Ïÿ££þý/þVø isG4{V3áÒÿègÿ€ÑÑÿþ—ÿ@«?üŽ´¹£š=« ¿ðéô ³ÿÀhèÿ„Kÿ UŸþGZ\ÑÍÕ…ŒßøGô¿úYÿà4tÂ?¥ÿÐ*Ïÿ£­.hæjÂÆoü#ú_ý¬ÿð:?áÒÿègÿ€ÑÖ—4sGµac7þý/þVø ðéô ³ÿÀhëKš9£Ú°±›ÿþ—ÿ@«?üŽøGô¿úYÿà4u¥ÍÑíXXÍÿ„Kÿ UŸþGGü#ú_ý¬ÿð:ÒæŽhö¬,fÿÂ?¥ÿÐ*Ïÿ££þý/þVø isG4{V3áÒÿègÿ€ÑÑÿþ—ÿ@«?üŽ´¹¢jÂÆoü#ú_ý¬ÿð:óßEz¦¬‰“Osÿ§ Êõ:ó-A¶Ôm®n%y#”êÑõ_ô»¯Gï33¦V/ü"–ž²߸¿øÍðŠZzÉÿ~âÿã5éµEbÿÂ)ië'ýû‹ÿŒÑÿ¥§¬Ÿ÷î/þ3@TV/ü"–ž²߸¿øÍðŠZzÉÿ~âÿã4µEbÿÂ)ië'ýû‹ÿŒÑÿ¥§¬Ÿ÷î/þ3@TV/ü"–ž²߸¿øÍðŠZzÉÿ~âÿã4µEbÿÂ)ië'ýû‹ÿŒÑÿ¥§¬Ÿ÷î/þ3@TV/ü"–ž²߸¿øÍYµðúÚÿǽÝÔ?õÂ8‡þÑ §Á¿ò%ø{þ¼ ÿÑB·«ÊáÑ¿çÚü´ÿã4£ϴø iÿÆkS#Õ¨¯)ÿ„ FÿŸh?ðÓÿŒÑÿÿ>Ðà%§ÿ V¢¼§þ-þ} ÿÀKOþ3Gü Z7üûAÿ€–Ÿüf€=ZŠòŸø@´oùöƒÿ-?øÍðhßóíþZñšõj+ÊáÑ¿çÚü´ÿã4£ϴø iÿÆhÕ¨¯)ÿ„ FÿŸh?ðÓÿŒÑÿÿ>Ðà%§ÿ V¢¼§þ-þ} ÿÀKOþ3Gü Z7üûAÿ€–Ÿüf€=ZŠòŸø@´oùöƒÿ-?øÍðhßóíþZñšõj+ÊáÑ¿çÚü´ÿã4£ϴø iÿÆhÕ¨¯)ÿ„ FÿŸh?ðÓÿŒÑÿÿ>Ðà%§ÿ V¢¼§þ-þ} ÿÀKOþ3Gü Z7üûAÿ€–Ÿüf€=ZŠòŸø@´oùöƒÿ-?øÍðhßóíþZñšõZòÿŠgÉ–âWÿ–zkãþÿCQ£ϴø iÿÆjkO ØÙ E³›)òåò-¢‡ÿhÐŽ»ow&¢ò ÔþÌreÛåæo©?óʸ¿Ëçj:‰þª]VôãþªÖ°økáð8Ó­ÓO³ÿã5v-14ÿøccÈÇí†/˜ç¥¥×øW5m!3SШ t¢¾lØ(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š+…ðßüƒîì!ÿ¥sWtk„ðßüƒîì!ÿ¥sW£‚øÌfiÅ.((¯\bŠ(Å¢Š1F(¢€ QŠ( bŠ(Å”Q@+.ÓI³×üXÑj0-í½µx£¸ˆIæžEjVnŸuö_ß{i£ÿFÐïhžñN‹g¬i^™§^B'‚ê+qûÐ{ôª>3·ð'€<5u®ë:U…¶™fÑ,’ýœIŒÉå1ô¯’¡ømñSð"jñø¼øŸHøseý˜Ë¨]­Âë"òîWó"'Måù93gõÍlxÇÂþ"Öü%ñVÂûÃ^-Ö>#]ßMx—!®fÒîôȯbšlóþ‹æù1¨òïD¢cÞµ2>¼>ðÂ’?°4þ?éÜ…aÅcàYü[wáht­;ûrÚÆRkSèÓK4BOÇɘWÍž#OŠ2xŠçAÑ,üKiªŸëZõ†µ4g.>—yöO2sû¤1\Íî%Úä-üãH4OàoxÏAÕ¤ð¾‡awqâV¹–þâæé¦Ô!µši³7îšA˜¥òˆ#ÉëšûÆžÒ4 ïôí6 >ö »Q° ýt_çð­&Y€èãß ´K@ø3s£©k7ñO«Ç,êúEÆŸ-”>t8Š(.f–U‹ÓΛֽ†O¾ÿS@@ʇJ¶Ö¼cok ÞØÅc%À†bù±á]ü >ÿ Ÿÿ€ãü+$ÆqùëFsÏý¶Š¾Cð‡‚>&C¢xÍ|A{ã¯\XÝÙ˧éÚu핽ơ-âýžîùnå¶>W'1G¼àzV@}·ÿ†?è§ÿà8ÿ Äðõ|Su¬ÛišVŸ+hÚÓnÉ€|“¡—þþÃ_5O¡xÒË@ð„^8Ó<]­éÖÆ©ÿ zhs^u ²˜¶½·)±98Š9Qƒ¶¦³Åž ñÕ߈“þ&þÁ‡ÆWZÄu´²Ésey£Å3ïeò¦ˆ‰b½‹¶kP>Žñ…Ÿ€¼ á›­{[Òì-4»#‡—ìàùyý×ÿZ·€<2 Øôî?¾ Ö< ãÍkáî›§x£Ã¾9Öµ«­'GMmÚêh,®Vq=çÛ£ˆíŠnbɺÏCϽoÁ¾ñïü4x‡WÖí¢]zú[EƒG¾“N½Òͯî!û_Úͬ@dp!ù°äñ@ëáèšØê¶«æ {;Ù¡ƒòÊ/A[¬_6]~OúŠOÿ¢¡­zÈÔËÕ5ÄÒn­­…­íÝÕÀ•£ŽÒÜI(ò¸ëYºGÄ 3Yñ=æ€Å®­i5µÒyrGní`|WÐüA}%÷‡u‹ýS¶¶²_ÙirßÄ%ó¡–(fŠ(¼ß&_+÷ÕðoÁËá=C†Hµ ÏÌ×Z·Šu{­:æÔ]ê2FwÈf’(IHÉ1Ež€;r·‰¼}´šxƒÄšV†÷òùV£P¾ŠÔË/¬^oJ±áÿéž):Ÿö\Æåt½Bm"|Œywpœy_ù¼CöƒÒõ/Çx›Á~×µˆ/¤I¦ÙÆÞ]KCÕ!ùßd¼˜‘öP%Éó<输W''¿h^&×­ãU%v¹”ØËa.–D3M|¯+í^Ióqùö O¬è¯4_xðx{[¹ðΑã}&M3AÓ¯N›â‹ÉÆ«â+K‘,é›1ýÔÑÅ$2ˆñ þ|Dt5×üð§Ä˜¾$îñ™ÕbѬ!ºñ$]O¹óQ1²Çý:bð¿é´^•©‘í–?¼;}àëÏ® ƒCµ{¨î.ñʦò¥ÿÑ5£â龓IMRèÆu;øôÛeÿž³Iþª/¾A¹øKãß x"i|3¥xžÃXÕ­|VuK{+» ›üùeÓ¥†/ùe6?Ôù5Ñx›À~"Ôþ8i—:–ƒâOW·ñÕŽ¥a«ÆneÑbÑ"Š1"¿–|ˆ¥ŠQ6£Î÷ $}7yâÍ3MñNáé§1êÚ¬ÜYEÿ=_ë¿ôulvâ¾-ð×Ãÿˆøž½;Iñ5ŸÄ‹Ox’ GÄ:ï›.œ×³tóm)ÿEò‡'€ü¶«ø]âKCÐìµ;¿I§Ýx—Hæ‘uc©iÒ­æ2‰å¼ši¹0‰fŽc #½}“Š¡yÿ#/…ÿëö_ý%º®;à6™­hÿbÒõá~—:v¯©ÛZRYe»{(µ¦e—ýgîL¾®ÂïþFO ÿ×ì¿úKu\õ¾S¹¢Š+悊( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( × á¿ùÜÿØBÿÿJæ®ì× á¿ùÜÿØBÿÿJæ¯Gñ˜ÌÓ( t¢½p (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ ÄÔ´ýB Qu 5-¤”Û}–[KÉ<¡ÿ{ÖÝ(ÎñOýôüÍÿÈ”yÞ)ÿ ~ÿƒ¿ù·¸£Š`ùÞ)ÿ ~ÿƒ)¿ù;Å?ôÑÿðe7ÿ"Vö(Ås–þ±äZßA¦XZ$±O$^M,ߺ—ÍëåC]A䓌RVN·â½ÃmÔõ{-8Ëþ¬]ÝE!Ö4ûñ¨A¨i¦ÚGH¥´’É<¯7ÍòúÞ§ýM7ÎñIÿ˜~ÿƒ¿ùªÿÂÓðgý zGþÇIÿ OÁô5hÿø[ó¼Sÿ@ýÿ3ò%wŠ¿è£ÿàÊoþDªŸð´üÿCVÿ±Ô±|Fð¤¿sÄZPÿ·è¨7âŸúèÿø1›ÿ‘(ó¼Sÿ@ýŸú‰MÿÈ•J×4ívfÓõ5bÿYöI<Ú¿ŠÊÐ4ù´Ë)ÚæhÚæk‰®åx†"ˆçÒµh¢ÑéF¥PEPŠ1PÞ^Úé–²ÜÞÝGgiúÙe—ýUsŸð´üækÑÿð6:êqFrßð´üÿCVÿ±Ñÿ OÁô5hÿøu8£ÍÅñ7«ñ>/ý¾ÅVôïèZÔVÖZÖŸww'ú¸¡¹Š\PÏzϼÿ‘—Âÿõû/þ’ÝV‡¥gÞÈËáúý—ÿInª+ âŠ(¯š6 (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€\'†ÿäsÿa ÿý+š»³\'†ÿäsÿa ÿý+š½Æc3Lt¢ÒŠõÀ(¢Š(¢Šä~%|B³ø[áa¯_i׺©ûe¥Œ6ZrBòË4²ù0Åûï+þZË\þ•ûAx<èW÷ÚýûxB].ÿûQ±ñŠÞH.Ä>iˆË)¿u7ü±«ß~MñkÁ+áë{¿°çT±»žA,±Š+¨¥”C,_½†SåžÞE«þÉš•­ÂO¡êM™®ÞjºlWºÎ©e{{Õ ŠhîõœL_òÊcçîO©zÖ•ñãÁÚ¿ŽµO &«¼²²µÕc!òî­e†Y¼è¿ïÕXµøÃá­~ êºwˆ,õmY´a=­ô»C4ÜÏcû¯õUç¶_¼_á|G§x3UÑ|)¥ëž´ÒKC-ÓÞhw6°Ë 2Z‰$‡÷ŸòÔÅÐVo€ÿfÿ躶­ªj:|W«âÛOKnš¶¡ª*Å-œ‹ö»¸üæ–O7’yÇJÔÌö/ü\ðoĹ.“¾&ÒüG5—ü|.Ÿpeý s¾%øûá+ xîóBÖt¯k>Ón¯î4ËKØüÌÚÅŽµ•ð£àV£ðîëÀ2^êv‡Ãþ—ÃW"ØK‹‰êc˜{~âcø×iû.x†ã°xvû^Ñ¿±´?ë~Юtë9EÕÒ_D"3ÝÞ<>h›9&³™qøÎÿÇ´g‡<'‡4ǽ²ºñF±q¥*ø|^Ÿ´¬7RÅ›ŸûkKñãý—þ'Ùøm ¹ÔK—Užæâþ+H£÷Ý¿ëÔÍ\dŸ³ÿŒ†¡¼wþ:V±¬hzî·¹n~×ise œ"oÝâXe€fc’%5èž+ø[}â/xÓZMJÒ }wÂ)áèà—ýl.%š_8ÿÓ/ßÒDDÏðÇí¡^é>&Õ¼Gwcá/GÔ­¬–ææú9¢—̳†ïýoý¶®¶ÓâÏ‚¯µÍ3E¶ñN›y©ê¶Ÿl²³‚æ)f¹‡þ{E^yû(xŽ->&µ×4ë«+S´Ö,íã¼Ô,~Ó*i#O–)f´ÄÐ’ Ðú‘äà×Oáÿ:÷„|iá ßÿcøSHÓ­"‡U};S¾ž]I–AjÐÉ:b|ée3Pµx]Ó¼S¢Ùk=å¶£¤ÞB'´»´“͆Pzó-oöÑ<9âXê:ˆEðÝÊXêÞ#ŽÖ)¬,¥’(eýàŠ_;þ[®òƽ7Ã骮…§¦ºtöÕü‘öÅÒ„¢Ñý1ó»W‡ø›àW¼G¨|MÑm5 Lðg®RmBø,ÒêPDm!´–â1€ý×úß4Ðjzu÷ÆoéÚ®«¦Ýx§KƒPÑíþÝ{i%Æe†Ûþzæ“Ä?|áKóc¬x§LÓï”Ä­—½_õ_ú&¼Z_€zÆ‹ñ Ox›SŸÅÓbÕ–®5 JòK+«*+H´¿+ÊQã̓÷³Ïpq<û;xî…ZuÂÏŸãÙ.帋ĚÕÖ›ªi–±Â-,G R‰¥ò"tSqûÒ éA‘õ™â/Z¹Ô`±¿·¹¹Ò¦6÷iœZL"óH—ßÊšªh–z7è^K¦½}6ßË’êDØÓÈß<ŽqÓs¹?vO¢ß5äþ#Oj:>©cà½ZÃÃ×פñÍ«ÝZ›¯²DwCm%”ûñëšõÚùóÇÒêú}¾©©hÒ|_wo©L.4» 72GÛÊš_ÝyŸõÖ€;95õÍMr+D×Gº¢Å·E!Ž{SŒžHçÿ"Öàéë\_„-.­õM'ûJËL²Õ[F¾’ê ßèÞu¯î”ËQדë]¥djy­¿Çï\ø[MÖž ¯¨x‡þxtÓûP½ûYµ1žxÀlÿÏ*·ÿ ÷áÌÚV§ªEã éš\ÑCyp·‘âßÍÿSŠãgý›¤»ø±¯k³ëj<+¨Awum¡Áïlõií~Éqv|Â1ÿ]f5ÊxöRÕü5£höw“éFûKºÑZËë:¥ü×v}À‘WeÙòí9Éò àdóÒµ‰”Ï`›ã_ƒç–þÇO×ôÍCV¶ÓµMŠæ!,‘ù^o'þXÓí¾2xD\øoOÔ|G¥éºþ¿kiwe¤½ì~l¾pýדÿ=~µÄx£àçŠõω%ÕtÝFÇAðî¹ewik õÔ£Všk?³D÷¾XŠ)!ÄCΆR|¨‡nG5?ìÍâOìÛ­ß[Ñá׬ô+}vY jK¥Áß³v”Kårf1}=ë–ß~]kº¾‰‹ôím+ΖþÄ^ÇæÚù_ë¿ïÏJ»áñ|zDš?ˆ4ý@ê¯2éâ+‚>Õ$?½˜Å^a®þÎwú¥”Ît=aSÅÚæ½-†¦%6·0j6×q}–Y _ôñ>ßJéþxGÇžÒl,uífËX¶þк’Xnn®¯¦°·òâò-!ºš8¥” r|é¹>mvž'Š95ß@è6ÖTºL3¼ùsVôï‹v|B_Ýhzö•}t·-§Þ_Xìµ½eòÜŽ âLÅUñLj<ÿa”ÿÑVW„üã;/Œú߉u«mV°žI­´ûøõ;ƒs§Øq²Þ;Sm凒XÃK'›Î QÙv·¢ß&§Ñ?ïš–Š`q^!šâÛÆ:sØÆ“Ýÿcj^L,Þ_™'iýk…²ÇÖžÖ!ñÖ§¤ësŽòÎóNµ’Ûȗηd™, Kœž¤g§MñÜ^k¶ˆíe–]*ôDo óâó<ûOùdz×–øfê~Õo¼Iୠ¸¶†==⺚áñû飖/ùf刾 g¸é.;dÿ:ͼÿ‘—Âÿõû/þ’ÝV•Ïü}IÓïŸZͼÿ‘—Âÿõû/þ’ÝW=oášÅQ_4lQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE¸O ÿÈ>çþÂÿúW5wf¼ÿ@ó?³î|³ÿ‰…ÿúïúüš½Æc3`t¢«bóûÖÿ•»õ·ü«×Í[~¶ÿ•»õ·ü¨Í[~¶ÿ•»õ·ü¨ÍªØ»õ·ü¨Åß­¿å@qF*¶.ýmÿ*1wëoùPœ 1š­‹¿[ÊŒ]úÛþTgtªØ»õ·ü¨Åß­¿å@p ªØ»õ·ü¨Åß­¿å@p(ÅVÅß­¿åF.ýmÿ*³Š1U±wëoùQ‹¿[Ê€,â²oü7eªÝ}ª_´Cuåùf[KÛ›Y¼¯ûg,Uw~¶ÿ•¼þõ½eÿÂ!cÿ?šïþõ?þJ£þ ùü×ð©ÿò]jbïûÖÿ•»õ·ü¨22ÿá°ÿŸÍwÿúŸÿ%Òÿ£ î!¾;ù²ã[¿ëëþ¶´ñwëoùQ‹Ïï[þT[Mдý)¦k(Yd˜ùFI.%¹”ýL³V—Z­þ™½oF.ýmÿ* K8âŒUl]úÛþTbïÖßò  8£[~¶ÿ•»õ·ü¨Î(ÅVÅß­¿åF.ýmÿ*5=.ËU·û=äBh|Ï3æ—Êò嬡àûûfº~ºþ§ÿÉu«‹ÁüVôbïÖßò  ¿øCì?çó]ÿÁþ§ÿÉtÂaÿ?šïþõ?þK­L]ÿzßò£~¶ÿ•F[x3HžD’Hõ‘#òã–Mnþ\õÿ¦¿ôÊoáÚXå³LÑ7|úÔ±ÿß™e­,^zßò£ŸÞ· Ô³ßžµŸyÿ#/…ÿëö_ý%º©ñyýëʪ˿þ_ ï1Ÿø˜Íþ§þ½n«:ßû¢Š+æÍ‚Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( × á¿ùÜÿØBÿÿJæ®ì× á¿ùÜÿØBÿÿJæ¯Gñ˜ÌÓ( t¢½p (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€(j—w&êÂÊР¹Ô® ²O(ÌQb¦àÛ*µÿˆÿèaÒÿðS/ÿ%Vmü›³UžS«E{ sæi“ýW•æ[ÍÇeĵÃÚ~Ù5Ó¼ §k›‹ßhvÆ…Œ÷2 ÐYyw=Ék†OÝc¡ö4ŸCê6:ï‡-­ï¯õ ?P„ÝC Ác-´‡Í—ÊëæËÿ=ª{ßù¼1ßý2_ý%º®CÃÞ6ñ'Œ¾yÞ.Ð_ú´:í­ „[Ëw1‹¸¼©¢ŠOÞŒñÇ×ðëïägðÇý~Ëÿ¤·UÏ[àÅQ_4u…Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@®Ãò¹ÿ°…ÿþ•Í]Ù®Ãò¹ÿ°…ÿþ•Í^Ž ã1™¦:Q@éEzàQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEaø–a¦Ýèú”¿ñéezÒÉ7üò?d–ý«\4þøI?Œ[ÅShz%LjMÔwO~ñÉ+¬ÑçÉ•SýP›’r+Õ”•9ÔS÷Iÿ=þú 4·ÓþÚøÖçÅñiú=¿Š®?ããVX¿ÒJùb=ûºùžHÍô⣗Fø]6—m¦I¤h²ivújè±ÙËî#´ôýÜ»W¸Ÿü#Þ!ÿ Ž™ÿ€rñê?áñýtÏü“ÿWYF]«ÜOþïÿÐGLÿÀ9?øõðx‡þ‚:gþÉÿÇ«¬À£®Õî'ÿ÷ˆè#¦àŸüzøG¼CÿA3ÿäÿãÕÖ`QG×j÷“ÿ„{Ä?ôÓ?ðOþ=Gü#Þ!ÿ Ž™ÿ€rñêë0(À£ëµ{ÉÿÂ=âúéŸø'ÿ£þïÿÐGLÿÀ9?øõu˜`QõÚ½ÀäÿáñýtÏü“ÿQÿ÷ˆè#¦àŸüzºÌ 0(úí^àrðx‡þ‚:gþÉÿǨÿ„{Ä?ôÓ?ðOþ=]f}v¯p9?øG¼CÿA3ÿäÿãÔÂ=âúéŸø'ÿ®³Œ >»W¸Ÿü#Þ!ÿ Ž™ÿ€rñê?áñýtÏü“ÿWYF]«ÜOþïÿÐGLÿÀ9?øõðx‡þ‚:gþÉÿÇ«¬À£®Õî'ÿ÷ˆè#¦àŸüzøG¼CÿA3ÿäÿãÕÖ`QG×j÷“ÿ„{Ä?ôÓ?ðOþ=Gü#Þ!ÿ Ž™ÿ€rñêë0(À£ëµ{ÉÿÂ=âúéŸø'ÿ£þïÿÐGLÿÀ9?øõu˜`QõÚ½ÀäÿáñýtÏü“ÿQÿ÷ˆè#¦àŸüzºÌ 0(úí^àrðx‡þ‚:gþÉÿǨÿ„{Ä?ôÓ?ðOþ=]f}v¯p9?øG¼CÿA3ÿäÿãÔÂ=âúéŸø'ÿ®³Œ >»W¸Ÿü#Þ!ÿ Ž™ÿ€rñê?áñýtÏü“ÿWYF]«ÜOþßÿÐGLÿÀ9?øõðŽø‡þ‚:gþÉÿÇ«¬À£®Õîÿι'üÄtÏÂÞ_þ=E·…õCªé—·×ö®¶rI(·†x|©bçÌ”ÿÏZìqF*~·0 ((¬ ÿÙukopp-4.9/desktop0000644000175000017500000000037112333076512012563 0ustar micomico[Desktop Entry] Name=ukopp GenericName=Copy Files to Removable Media Comment=Full and incremental backup with media verification Categories=Utility;Archiving Type=Application Terminal=false Exec=/usr/bin/ukopp Icon=/usr/share/ukopp/icons/ukopp.png ukopp-4.9/debian-control0000644000175000017500000000110712333076511014007 0ustar micomicoPackage: ukopp Version: 4.9 Architecture: amd64 Section: utils Installed-Size: 1180 Maintainer: Mike Cornelison Priority: optional Homepage: http://kornelix.com/ Depends: libc6, libgtk-3-0, gksu, binutils Description: Copy files to backup media. Copies only new and modified files and is therefore quite fast. Specify directories or files to include or exclude at any level. Report disk/backup differences at summary, directory or file level. Optionally retain prior file versions for a specified time or version count. Optionally verify backup files. ukopp-4.9/zfuncs.cc0000644000175000017500000143670012333076511013017 0ustar micomico/************************************************************************** zfuncs.cpp collection of Linux and GDK/GTK utility functions Copyright 2006-2014 Michael Cornelison source code URL: http://kornelix.com contact: kornelix@posteo.de This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . These programs originate from the author's web site: http://kornelix.com Other web sites may offer them for download. Modifications could have been made. If you have questions, suggestions or a bug to report: kornelix@posteo.de ***************************************************************************/ // zfuncs.cpp version v.5.8 #include "zfuncs.h" /************************************************************************** Table of Contents ================= System Utility Functions ------------------------ printz printf() with immediate fflush() apppause printf() message and wait for user ACK zpopup_message popup message window, thread safe (no GTK) zappcrash abort with traceback dump to popup window and stdout TRACE trace() and tracedump() implement the TRACE macro catch_signals trap segfault, crash with zappcrash() beroot restart image as root, if password is OK timer functions elapsed time, CPU time, process time functions /proc file functions parse data from various /proc files zsleep sleep for any amount of time (e.g. 0.1 seconds) global_lock reserve and release a global resource (all processes/threads) start_detached_thread simplified method to start a detached thread synch_threads make threads pause and resume together zget_locked, etc. safely access parameters from multiple threads shell_quiet format and run a shell command, return status shell_ack "" + popup error message if error shell_asynch "" + return immediately and query status later comand_output start shell command and read the output as records signalProc pause, resume, or kill a child process runroot run a command or program as root user checkinstall check if a list of programs are all installed fgets_trim fgets() with trim of trailing \r \n and optionally blanks samedirk test if two files/directories have the same directory path renamez like rename() but works across file systems parsefile parse filespec into directory, file, extension cpu_profile measure CPU time spent per function or code block pagefaultrate monitor and report own process hard page fault rate String Functions ---------------- comparison macros code legibility improvers strField get delimited substrings from input string strParms parse a string in the form "parm1=1.23, parm2=22 ..." strHash hash string to random number in a range strHash2 hash input string to random output string of characters a-z strncpy0 strcpy() with insured null delimiter strnPad add blank padding to specified length strTrim remove trailing blanks strTrim2 remove leading and trailing blanks strCompress remove imbedded blanks strncatv catenate multiple strings with length limit strcmpv compare 1 string to N strings strToUpper convert string to upper case strToLower convert string to lower case repl_1str replace substring within string repl_Nstrs replace multiple substrings within string strncpyx convert string to hex format StripZeros remove trailing zeros (1.23000E+8 >> 1.23E+8) blank_null test string for null pointer, zero length, and all blanks strdupz duplicate string and add space at end clean_escapes replace 2-character escapes ("\n") with the escaped characters UTF-8 functions deal with UTF-8 multibyte character strings Number Conversion and Formatting -------------------------------- convSI string to integer with optional limits check convSD string to double with optional limits check convSF string to float with optional limits check convIS integer to string with returned length convDS double to string with specified digits of precision formatKBMB format a byte count with specified precision and B/KB/MB/GB units Wildcard Functions ------------------ MatchWild match string to wildcard string (multiple * and ?) MatchWildIgnoreCase works like MatchWild() but ignores case SearchWild wildcard file search (multiple * and ? in path or file name) SearchWildIgnoreCase works like SearchWild() but ignores case in file name Search and Sort Functions ------------------------- bsearch binary search of sorted list HeapSort sort list of integer / float / double / records / pointers to records MemSort sort records with multiple keys (data types and sequence) Misc Functions -------------- bitmap_new, etc. functions to create, modify, and read a bitmap pvlist_create, etc. functions to manage a list of variable strings rand functions int and double random numbers with improved distributions spline1/2 cubic spline curve fitting function Qtext FIFO queue for text strings, dual thread access Application Admin Functions --------------------------- zinitapp etc. initialize application directory and data files locale_filespec find a locale-dependent installation file (translate-xx.po etc.) showz_userguide display user guide (optional help topic) showz_logfile display application log file showz_textfile show application text file (README, changelog, etc.) showz_html show a local or remote HTML file zmake_menu_launcher create desktop icon / launcher ZTX(), etc. translate GUI and message text strings for non-English locales GTK Utility Functions --------------------- zmainloop do a main loop to process menu events, etc. zthreadcrash crash if called from a thread other than main() wprintx etc. printf() to window at specified row or next row wscroll scroll text window to put line on screen wclear clear a wprintf window wscanf read text from edit window, line at a time wfiledump dump text window to a file wfilesave wfiledump with save-as dialog wprintp print text window to default printer textwidget funcs intercept mouse clicks on text windows, get clicked text get_mouse_device get the screen and mouse device for a widget menus / toolbars simplified GTK menus, toolbars and status bars popup menus create popup menus gmenuz customizable graphic popup menu Vmenu vertical menu/toolbar in vertical packing box zdialog, etc. simplified GTK dialogs write_popup_text write rows of text to a popup window popup_command run a shell command with output in a popup window zmessageACK popup message, printf format, wait for user ACK zmessLogACK same with parallel output to STDOUT zmessageYN popup message, printf format, wait for user Yes / No zmessage_post popup message, printf format, show until killed zdialog_text popup dialog to get 1-2 lines of text input from user zdialog_choose popup dialog to show a message, select a button, return choice poptext_mouse popup message at current mouse position + offset poptext_window popup message at given window position poptext_killnow kill popup message popup_image show an image in a small popup window move_pointer move the mouse pointer within a widget/window zgetfile simplified file chooser dialog print_image_file dialog to print an image file using GTK functions drag_drop_connect connect window drag-drop event to user function get_thumbnail get thumbnail image for given image file zmakecursor make a cursor from an image file (.png .jpg) gdk_pixbuf_rotate rotate a pixbuf through any angle parameters functions to manage a set of numeric parameters C++ Classes ----------- xstring string manipulation (= / + / insert / overlay) Vxstring array of xstrings with auto growth HashTab hash table: add, delete, find, step through Queue queue of xstrings: push, pop first or last Tree store / retrieve data by node names or numbers, any depth ***************************************************************************/ namespace zfuncs { char zappname[20]; // app name/version char zprefix[200], zdatadir[200], zdocdir[200]; // app directories char zicondir[200], zlocalesdir[200], zuserdir[200]; char zlang[8] = "en"; // "lc" or "lc_RC" char JPGquality[4] = "90"; // JPG file save quality int open_popup_windows = 0; // open popup window count pthread_t tid_main = 0; // main thread ID int zdialog_busy; // counter, open/pending zdialogs PangoFontDescription *monofont = 0; // zdialog widget fonts PangoFontDescription *widgetfont = 0; } /************************************************************************** system-level utility functions ***************************************************************************/ // printf() and flush every output immediately even if stdout is a file void printz(cchar *format, ...) { va_list arglist; va_start(arglist,format); vprintf(format,arglist); va_end(arglist); fflush(stdout); return; } // Pause and wait for user KB input void apppause() { printf("*** press return to continue: "); getchar(); return; } // Output a message to stdout and wait for user ACK. // Works like printf(). void apppause(cchar *format, ... ) { va_list arglist; char message[200]; va_start(arglist,format); vsnprintf(message,200,format,arglist); va_end(arglist); printf("pause: %s \n",message); printf("*** press return to continue: "); getchar(); return; } // output a message to a popup window // works like printf() // separate shell process, no GTK // returns immediately and message dies after 5 seconds void zpopup_message(cchar *format, ... ) // v.5.5 { va_list arglist; char message[400], tempfile[30], command[100]; FILE *fid; int cc, err; va_start(arglist,format); // format user message vsnprintf(message,400,format,arglist); va_end(arglist); printz("popup message: \n %s \n",message); // message to log file stdout cc = strlen(message); if (cc < 30) { strcat(message," "); // lengthen short message message[30] = 0; // (no truncate box title line) } sprintf(tempfile,"/tmp/zpopup-%06d",getpid()); // write message to temp file fid = fopen(tempfile,"w"); if (! fid) return; fprintf(fid,"%s",message); fclose(fid); strcpy(command,"xmessage -buttons OK:0 -center -timeout 5 -file "); // create popup with message file strcat(command,tempfile); strcat(command," &"); // return immediately err = system(command); if (err) return; // stop compiler warning return; } /**************************************************************************/ // Write an error message and traceback dump to a file and to a popup window. // Error message works like printf(). // Depends on library program addr2line(). void zappcrash(cchar *format, ... ) // rev. v.5.8 { using namespace zfuncs; static int crash = 0; struct utsname unbuff; va_list arglist; FILE *fid; int ii, err, nstack = 100; char message[300]; void *stacklist[100]; char *arch, **stackents; if (crash++) { printf("zappcrash re-entered \n"); // re-entry or multiple threads crash return; } uname(&unbuff); arch = unbuff.machine; va_start(arglist,format); vsnprintf(message,300,format,arglist); va_end(arglist); printf("*** zappcrash: %s %s %s \n",arch,zappname,message); // output message to stdout nstack = backtrace(stacklist,nstack); // get traceback data (can fail) if (nstack > 100) nstack = 100; stackents = backtrace_symbols(stacklist,nstack); // traceback records in memory for (ii = 0; ii < nstack; ii++) // output traceback records to stdout printf(" %s \n",stackents[ii]); fid = fopen("zappcrash","w"); // open zappcrash file (can hang here) fprintf(fid,"\n*** zappcrash: %s %s %s \n",arch,zappname,message); fprintf(fid,"*** please send to kornelix@posteo.de *** \n"); for (ii = 0; ii < nstack; ii++) // output traceback records fprintf(fid," %s \n",stackents[ii]); fclose(fid); tracedump(); // output TRACE data v.5.7 err = system("cat zappcrash tracedump > zappcrash2"); // combine zappcrash and tracedump err = system("mv -f zappcrash2 zappcrash"); err = system("xdg-open zappcrash"); // popup zappcrash text file if (err) printz("*** xdg-open failure \n"); exit(1); } /**************************************************************************/ // application initialization function to catch some bad news signals // the signal handler calls zappcrash() to output a traceback dump and exit void catch_signals() { void sighandler(int signal); struct sigaction sigact; sigact.sa_handler = sighandler; sigemptyset(&sigact.sa_mask); sigact.sa_flags = 0; sigaction(SIGTERM,&sigact,0); // v.5.6 sigaction(SIGSEGV,&sigact,0); sigaction(SIGILL,&sigact,0); // SIGILL/FPE/BUS added v.5.3 sigaction(SIGFPE,&sigact,0); sigaction(SIGBUS,&sigact,0); return; } // catch segfaults and produce backtrace dumps on-screen void sighandler(int signal) { const char *signame = "unknown"; if (signal == SIGTERM) exit(1); // v.5.6 if (signal == SIGSEGV) signame = "segment fault"; if (signal == SIGILL) signame = "illegal op"; if (signal == SIGFPE) signame = "float exception"; if (signal == SIGBUS) signame = "bus error"; zappcrash("fatal signal %s",signame); return; } /**************************************************************************/ // Implement the TRACE macro. // v.5.7 // Trace program execution by function and source code line number. // tracedump() dumps last 50 uses of TRACE macro, latest first. namespace tracenames { char filebuff[50][100]; char funcbuff[50][40]; int linebuff[50]; int ftf = 1, ii; }; // args are source file, source function name, source code line number // these all come from the compiler and are captured by the TRACE macro void trace(cchar *file, cchar *func, int line) { using namespace tracenames; if (ftf) { ftf = 0; for (ii = 0; ii < 50; ii++) { filebuff[ii][99] = 0; funcbuff[ii][39] = 0; linebuff[ii] = 0; } ii = 0; } if (line == linebuff[ii] && strcmp(func,funcbuff[ii]) == 0) return; // same as last call, don't duplicate if (++ii > 49) ii = 0; strncpy(&filebuff[ii][0],file,99); strncpy(&funcbuff[ii][0],func,39); linebuff[ii] = line; return; } // dump trace records to STDOUT void tracedump() { using namespace tracenames; FILE *fid; int kk = ii; fid = fopen("tracedump","w"); while (1) { if (linebuff[kk] == 0) break; fprintf(fid, "TRACE %s %s %d \n",&filebuff[kk][0], &funcbuff[kk][0],linebuff[kk]); if (--kk == ii) break; } fclose(fid); return; } /**************************************************************************/ // This function will restart the current program with root privileges, // if the correct (sudo) password is given. It does not return. // "argc" and "argv" are passed in the command line. // Use the original argc and argv (optional, may be omitted). // argv[0] is omitted to avoid passing the program name twice. // argv[] parameters are enclosed in quotes to avoid gksu eating them. // bugfix v.5.6 void beroot(int argc, char *argv[]) { int cc1, cc2, ii, err; char command[1000]; if (getuid() == 0) return; // already root strcpy(command,"which gksu > /dev/null 2>&1"); // Debian err = system(command); strcpy(command,"gksu \""); if (err) { strcpy(command,"which beesu > /dev/null 2>&1"); // Fedora, just to be different err = system(command); strcpy(command,"beesu \""); } if (err) { printz("*** cannot find gksu or beesu \n"); abort(); } cc1 = strlen(command); // gksu (or) beesu cc2 = readlink("/proc/self/exe",command+cc1,990); if (cc2 <= 0) { printz("*** cannot get /proc/self/exe \n"); abort(); } command[cc1+cc2] = 0; // gksu or beesu for (ii = 1; ii < argc; ii++) // append command line parameters strncatv(command,990," ",argv[ii],null); strcat(command,"\" &"); // return immediately printz("%s \n",command); err = system(command); exit(0); } /**************************************************************************/ // get time in real seconds (since 2000.01.01 00:00:00) // (microsecond resolution until at least 2030) double get_seconds() { timeval time1; double time2; gettimeofday(&time1,0); time2 = time1.tv_sec + 0.000001 * time1.tv_usec - 946684800.0; // v.5.2 return time2; } /**************************************************************************/ // start a timer or get elapsed time with millisecond resolution. void start_timer(double &time0) { timeval timev; gettimeofday(&timev,0); time0 = timev.tv_sec + 0.000001 * timev.tv_usec; return; } double get_timer(double &time0) { timeval timev; double time; gettimeofday(&timev,0); time = timev.tv_sec + 0.000001 * timev.tv_usec; return time - time0; } /**************************************************************************/ // start a process CPU timer or get elapsed process CPU time // returns seconds with millisecond resolution void start_CPUtimer(double &time0) { time0 = CPUtime(); return; } double get_CPUtimer(double &time0) { return CPUtime() - time0; } /**************************************************************************/ // get elapsed CPU time used by current process // returns seconds with millisecond resolution double CPUtime() { clock_t ctime = clock(); double dtime = ctime / 1000000.0; return dtime; } /**************************************************************************/ // Get elapsed CPU time used by current process, including all threads. // Returns seconds with millisecond resolution. double CPUtime2() { struct rusage usage; double utime, stime; int err; err = getrusage(RUSAGE_SELF,&usage); if (err) return 0.0; utime = usage.ru_utime.tv_sec + 0.000001 * usage.ru_utime.tv_usec; stime = usage.ru_stime.tv_sec + 0.000001 * usage.ru_stime.tv_usec; return utime + stime; } /**************************************************************************/ // get elapsed process time for my process, including threads and child processes. double jobtime() { double jiffy = 1.0 / sysconf(_SC_CLK_TCK); // "jiffy" time slice = 1.0 / HZ char buff[200]; double cpu1, cpu2, cpu3, cpu4; FILE *fid; char *pp; fid = fopen("/proc/self/stat","r"); if (! fid) return 0; pp = fgets(buff,200,fid); fclose(fid); if (! pp) return 0; parseprocrec(pp,14,&cpu1,15,&cpu2,16,&cpu3,17,&cpu4,null); return (cpu1 + cpu2 + cpu3 + cpu4) * jiffy; } /**************************************************************************/ // convert a time_t date/time (e.g. st_mtime from stat() call) // into a compact date/time format "yyyymmddhhmmss" void compact_time(const time_t DT, char *compactDT) // v.5.5 { struct tm *fdt; int year, mon, day, hour, min, sec; fdt = localtime(&DT); year = fdt->tm_year + 1900; mon = fdt->tm_mon + 1; day = fdt->tm_mday; hour = fdt->tm_hour; min = fdt->tm_min; sec = fdt->tm_sec; compactDT[0] = year / 1000 + '0'; compactDT[1] = (year % 1000) / 100 + '0'; compactDT[2] = (year % 100) / 10 + '0'; compactDT[3] = year % 10 + '0'; compactDT[4] = mon / 10 + '0'; compactDT[5] = mon % 10 + '0'; compactDT[6] = day / 10 + '0'; compactDT[7] = day % 10 + '0'; compactDT[8] = hour / 10 + '0'; compactDT[9] = hour % 10 + '0'; compactDT[10] = min / 10 + '0'; compactDT[11] = min % 10 + '0'; compactDT[12] = sec / 10 + '0'; compactDT[13] = sec % 10 + '0'; compactDT[14] = 0; return; } /**************************************************************************/ // Read and parse /proc file with records formatted "parmname xxxxxxx" // Find all requested parameters and return their numeric values int parseprocfile(cchar *pfile, cchar *pname, double *value, ...) // EOL = 0 { FILE *fid; va_list arglist; char buff[1000]; const char *pnames[20]; double *values[20]; int ii, fcc, wanted, found; pnames[0] = pname; // 1st parameter values[0] = value; *value = 0; va_start(arglist,value); for (ii = 1; ii < 20; ii++) // get all parameters { pnames[ii] = va_arg(arglist,char *); if (! pnames[ii]) break; values[ii] = va_arg(arglist,double *); *values[ii] = 0; // initialize to zero } va_end(arglist); if (ii == 20) zappcrash("parseProcFile, too many fields"); wanted = ii; found = 0; fid = fopen(pfile,"r"); // open /proc/xxx file if (! fid) return 0; while ((fgets(buff,999,fid))) // read record, "parmname nnnnn" { for (ii = 0; ii < wanted; ii++) { // look for my fields fcc = strlen(pnames[ii]); if (strnEqu(buff,pnames[ii],fcc)) { *values[ii] = atof(buff+fcc); // return value found++; break; } } if (found == wanted) break; // stop when all found } fclose(fid); return found; } // Parse /proc record of the type "xxx xxxxx xxxxx xxxxxxxx xxx" // Return numeric values for requested fields (starting with 1) int parseprocrec(char *prec, int field, double *value, ...) // EOL = 0 { va_list arglist; int xfield = 1, found = 0; va_start(arglist,value); while (*prec == ' ') prec++; // skip leading blanks while (field > 0) { while (xfield < field) // skip to next wanted field { prec = strchr(prec,' '); // find next blank if (! prec) break; while (*prec == ' ') prec++; // skip multiple blanks xfield++; } if (! prec) break; *value = atof(prec); // convert, return double found++; field = va_arg(arglist,int); // next field number value = va_arg(arglist,double *); // next output double * } while (field > 0) { *value = 0; // zero values not found field = va_arg(arglist,int); value = va_arg(arglist,double *); } va_end(arglist); return found; } /**************************************************************************/ // sleep for specified time in seconds (double) // signals can cause early return void zsleep(double dsecs) { unsigned isecs, nsecs; timespec tsecs; if (dsecs == 0.0) return; isecs = unsigned(dsecs); nsecs = unsigned(1000000000.0 * (dsecs - isecs)); tsecs.tv_sec = isecs; tsecs.tv_nsec = nsecs; nanosleep(&tsecs,null); return; } /**************************************************************************/ // Lock or unlock a multi-process multi-thread resource. // Only one process/thread may posess a given lock. // A reboot or process exit or crash releases the lock. // fd = global_lock(lockfile) returns fd > 0 if success, -1 otherwise. int global_lock(cchar *lockfile) // v.5.2 { int err, fd; fd = open(lockfile,O_RDWR|O_CREAT,0666); // open or create the lock file if (fd < 0) { printz("*** global_lock(), %s \n",strerror(errno)); return -1; } err = flock(fd,LOCK_EX|LOCK_NB); // request exclusive non-blocking lock if (err) { close(fd); return -1; } return fd + 1; // return value is >= 1 } int global_unlock(int fd, cchar *lockfile) { int err = close(fd-1); remove(lockfile); if (err < 0) return -1; else return 1; } /**************************************************************************/ // Start a detached thread using a simplified protocol. // Will not make a zombie out of the calling thread if it exits // without checking the status of the created thread. // Thread should exit with pthread_exit(0); void start_detached_thread(void * threadfunc(void *), void * arg) { pthread_t ptid; pthread_attr_t ptattr; int pterr; pthread_attr_init(&ptattr); pthread_attr_setdetachstate(&ptattr,PTHREAD_CREATE_DETACHED); pterr = pthread_create(&ptid,&ptattr,threadfunc,arg); if (pterr) zappcrash("start_detached_thread() failure"); return; } /**************************************************************************/ // Synchronize execution of multiple threads. // Simultaneously resume NT calling threads. // from main(): synch_threads(NT) /* setup to synch NT threads */ // from each thread: synch_threads() /* suspend, resume simultaneously */ // // Each calling thread will suspend execution until all threads have suspended, // then they will all resume execution at the same time. If NT is greater than // the number of calling threads, the threads will never resume. void synch_threads(int NT) { static pthread_barrier_t barrier; static int bflag = 0; if (NT) { // main(), initialize if (bflag) pthread_barrier_destroy(&barrier); pthread_barrier_init(&barrier,null,NT); bflag = 1; return; } pthread_barrier_wait(&barrier); // thread(), wait for NT threads return; // unblock } /**************************************************************************/ // Safely access and update parameters from multiple threads. // A mutex lock is used to insure one thread at a time has access to the parameter. // Many parameters can be used but there is only one mutex lock. mutex_tp zget_lock = PTHREAD_MUTEX_INITIALIZER; int zget_locked(int ¶m) // lock and return parameter { mutex_lock(&zget_lock); return param; } void zput_locked(int ¶m, int value) // set and unlock parameter { param = value; mutex_unlock(&zget_lock); return; } int zadd_locked(int ¶m, int incr) // lock, increment, unlock, return { int retval; mutex_lock(&zget_lock); retval = param + incr; param = retval; mutex_unlock(&zget_lock); return retval; } /**************************************************************************/ // Format and run a shell command. // Print a status message to stdout if there is an error. // returns 0 if OK, +N if error int shell_quiet(cchar *command, ...) // v.5.5 { char *cbuff; va_list arglist; int cc, err; cc = strlen(command) + 1000; cbuff = (char *) malloc(cc+1); va_start(arglist,command); // format command vsnprintf(cbuff,cc,command,arglist); va_end(arglist); err = system(cbuff); if (! err) { free(cbuff); return 0; } err = WEXITSTATUS(err); // special BS for subprocesses v.5.8 if (strnEqu(command,"diff",4)) { free(cbuff); return err; } // no diagnostic for these if (strnEqu(command,"cmp",3)) { free(cbuff); return err; } if (cc > 200) cbuff[200] = 0; if (err == 127) // special case v.5.8 printz("%s \n *** %s \n",cbuff,"command not found"); else printz("%s \n *** %s \n",cbuff,wstrerror(err)); // log error message free(cbuff); return err; } // Format and run a shell command. // Print a status message to stdout if there is an error. // Also pop-up an error message window if error. // Thread safe: GTK is not used. // returns 0 if OK, +N if error int shell_ack(cchar *command, ...) // v.5.5 { char *cbuff; va_list arglist; int cc, err; cc = strlen(command) + 1000; cbuff = (char *) malloc(cc+1); va_start(arglist,command); // format command vsnprintf(cbuff,cc,command,arglist); va_end(arglist); printz("shell: %s \n",cbuff); // v.5.6 err = system(cbuff); if (! err) { free(cbuff); return 0; } err = WEXITSTATUS(err); // special BS for subprocesses v.5.8 if (strnEqu(command,"diff",4)) { free(cbuff); return err; } // no diagnostic for these if (strnEqu(command,"cmp",3)) { free(cbuff); return err; } if (cc > 200) cbuff[200] = 0; if (err == 127) { printz("%s \n *** %s \n",cbuff,"command not found"); // special case v.5.8 zmessageACK(null,0,"%s \n %s",cbuff,"command not found"); } else { printz("%s \n *** %s \n",cbuff,wstrerror(err)); // log error message zmessageACK(null,0,"%s \n %s",cbuff,wstrerror(err)); // popup error message } free(cbuff); return err; } /**************************************************************************/ // Start a shell command from a new thread and return immediately. // The thread waits for the shell command and gets its status. // If there is an error, it is logged to STDOUT. // The calling process can get the status asynchronously. // The command format works like printf(). // // handle = shell_asynch(command, ...) // Start the command and return a reference handle. // // err = shell_asynch_status(handle) // Return the command status for the given handle: // -1 = busy, 0 = complete OK, +N = error (use wstrerror(err)). // // The status MUST be queried for allocated space to be freed. namespace shell_asynch_names { char *command[10]; int status[10]; mutex_tp mlock = PTHREAD_MUTEX_INITIALIZER; } int shell_asynch(cchar *Fcommand, ...) // v.5.5 { using namespace shell_asynch_names; void * shell_asynch_thread(void *); va_list arglist; static int ii; mutex_lock(&mlock); // block other callers for (ii = 0; ii < 10; ii++) if (command[ii] == 0) break; if (ii == 10) zappcrash("shell_asynch > 10 calls"); command[ii] = (char *) malloc(2000); // allocate memory va_start(arglist,Fcommand); // format command vsnprintf(command[ii],2000,Fcommand,arglist); va_end(arglist); start_detached_thread(shell_asynch_thread,&ii); // pass command to thread status[ii] = -1; // status = busy return ii; // return handle } void * shell_asynch_thread(void *arg) // thread function { using namespace shell_asynch_names; int err; int ii = *((int *) arg); // capture handle mutex_unlock(&mlock); // unblock other callers err = system(command[ii]); // start command, wait until done if (! err) { status[ii] = 0; return 0; } err = WEXITSTATUS(err); // special BS for subprocesses v.5.8 if (err == 127) printz("%s \n *** %s \n",command[ii],"command not found"); // special case v.5.8 else printz("%s \n *** %s \n",command[ii],wstrerror(err)); // log error message status[ii] = err; // set status for caller return 0; } int shell_asynch_status(int handle) // get command status { using namespace shell_asynch_names; int ii = handle; if (status[ii] == -1) return -1; // return busy status free(command[ii]); // free memory command[ii] = 0; return status[ii]; // return completion status } /************************************************************************** Run a shell command and get its outputs one record at a time. The outputs are returned one record at a time, until a NULL is returned, indicating the command has finished and has exited. The new line character is removed from the returned output records. Use contx = 0 to start a new command. Do not change the returned value. Up to 9 commands can run in parallel, with contx values 1-9. To get the command exit status: status = command_status(contx). If the command is still busy, -1 is returned. To kill a command before output is complete: command_kill(contx); ***/ FILE * CO_contx[10] = { 0,0,0,0,0,0,0,0,0,0 }; int CO_status[10]; char * command_output(int &contx, cchar *command, ...) // simplify, allow parallel usage { FILE *fid; va_list arglist; char buff[10000], *prec; if (contx == 0) // start new command { for (contx = 1; contx < 10; contx++) if (CO_contx[contx] == 0) break; if (contx == 10) { printz("command_output(), parallel usage > 9 \n"); // v.5.6 return 0; } va_start(arglist,command); // format command vsnprintf(buff,9999,command,arglist); va_end(arglist); fid = popen(buff,"r"); // execute command, output to FID if (fid == 0) { CO_status[contx] = errno; // failed to start printz("command_output: %s\n %s\n",buff,strerror(errno)); // v.5.5 return 0; } CO_contx[contx] = fid + 1000; CO_status[contx] = -1; // mark context busy } fid = CO_contx[contx] - 1000; prec = fgets_trim(buff,9999,fid,1); // next output, less trailing \n if (prec) return strdup(prec); // return output to caller CO_status[contx] = pclose(fid); // EOF, set status CO_contx[contx] = 0; // mark context free return 0; } int command_status(int contx) // get command exit status { int err = CO_status[contx]; return WEXITSTATUS(err); // special BS for subprocess } int command_kill(int contx) // kill output before completion v.5.5 { FILE *fid; if (! CO_contx[contx]) return 0; // context already closed v.5.8 fid = CO_contx[contx] - 1000; CO_status[contx] = pclose(fid); // close context and set status CO_contx[contx] = 0; // mark context free return 0; } /**************************************************************************/ // Signal a running subprocess by name (name of executable or shell command). // Signal is "pause", "resume" or "kill". If process is paused, kill may not work, // so issue resume first if process is paused. int signalProc(cchar *pname, cchar *signal) { pid_t pid; FILE *fid; char buff[100], *pp; int err, nsignal = 0; sprintf(buff,"ps -C %s h o pid",pname); fid = popen(buff,"r"); // popen() instead of system() if (! fid) return 2; pp = fgets(buff,100,fid); pclose(fid); if (! pp) return 4; pid = atoi(buff); if (! pid) return 5; if (strEqu(signal,"pause")) nsignal = SIGSTOP; if (strEqu(signal,"resume")) nsignal = SIGCONT; if (strEqu(signal,"kill")) nsignal = SIGKILL; err = kill(pid,nsignal); return err; } /**************************************************************************/ // run a command or program as root user // sucomm: root user access command, "su" or "sudo" // command: shell command or filespec of the program to start // returns 0 if successfully started, else returns an error code int runroot(cchar *sucomm, cchar *command) { char xtcommand[500]; int err; if (strcmp(sucomm,"sudo") == 0) { snprintf(xtcommand,499,"xterm -geometry 40x3 -e sudo -S %s",command); err = system(xtcommand); if (err) err = WEXITSTATUS(err); // special BS for subprocesses v.5.8 return err; } if (strcmp(sucomm,"su") == 0) { snprintf(xtcommand,499,"xterm -geometry 40x3 -e su -c %s",command); err = system(xtcommand); if (err) err = WEXITSTATUS(err); return err; } return -1; } /**************************************************************************/ // Check if a list of programs are all installed // If any are missing, pop-up a window of missing programs // Returns the number of missing programs (zero if none). int checkinstall(cchar *prog1, ...) // null terminated list { va_list arglist; char *buff, errmessage[200] = "missing programs:\n"; cchar *prog, *pp, *missprogs[20]; int contx, found, Nmiss = 0; va_start(arglist,prog1); prog = prog1; while (prog) { contx = 0; found = 0; while (true) { buff = command_output(contx,"whereis %s",prog); if (! buff) break; pp = strchr(buff,':'); if (pp) pp = strchr(pp,'/'); if (pp) found = 1; free(buff); continue; } if (! found) { if (Nmiss == 20) break; missprogs[Nmiss] = prog; Nmiss++; } prog = va_arg(arglist,cchar *); } va_end(arglist); if (Nmiss) { for (int ii = 0; ii < Nmiss; ii++) strncatv(errmessage,199,missprogs[ii]," ",null); zmessageACK(null,null,errmessage); } return Nmiss; } /**************************************************************************/ // fgets() with additional feature: trailing \n \r are removed. // optional bf flag: true if trailing blanks are to be removed. // trailing null character is assured. char * fgets_trim(char *buff, int maxcc, FILE *fid, int bf) { int cc; char *pp; pp = fgets(buff,maxcc,fid); if (! pp) return pp; cc = strlen(buff); if (bf) while (cc && buff[cc-1] > 0 && buff[cc-1] <= ' ') --cc; else while (cc && buff[cc-1] > 0 && buff[cc-1] < ' ') --cc; buff[cc] = 0; return pp; } /**************************************************************************/ // return true if both files are in the same directory // both files may be files or directories int samedirk(cchar *file1, cchar *file2) { int cc1, cc2; cchar *pp1, *pp2; if (! file1 || ! file2) return 0; pp1 = strrchr(file1,'/'); pp2 = strrchr(file2,'/'); if (! pp1 && ! pp2) return 1; if (pp1 && ! pp2) return 0; if (! pp1 && pp2) return 0; cc1 = pp1 - file1; cc2 = pp2 - file2; if (cc1 != cc2) return 0; if (strncmp(file1,file2,cc1) == 0) return 1; return 0; } /************************************************************************** Parse a pathname (filespec) and return its components. Returned strings are allocated in static memory (no free needed). Missing components are returned as null pointers. input ppath outputs /name1/name2/ directory /name1/name2/ with no file /name1/name2 directory /name1/name2/ if name2 a directory, otherwise directory /name1/ and file name2 /name1/name2.xxx if .xxx < 8 chars, returns file name2 and ext .xxx, otherwise returns file name2.xxx and no ext returns 0 if no error, else 1 ***************************************************************************/ int parsefile(cchar *ppath, char **pdirk, char **pfile, char **pext) { struct stat statb; static char dirk[1000], file[200], ext[8]; char *pp; int err, cc1, cc2; *pdirk = *pfile = *pext = null; cc1 = strlen(ppath); if (cc1 > 999) return 1; // ppath too long strcpy(dirk,ppath); *pdirk = dirk; err = stat(dirk,&statb); // have directory only if (! err && S_ISDIR(statb.st_mode)) return 0; pp = (char *) strrchr(dirk,'/'); if (! pp) return 1; // illegal pp++; cc2 = pp - dirk; if (cc2 < 2 || cc2 == cc1) return 0; // have /xxxx or /xxxx/ if (strlen(pp) > 199) return 1; // filename too long strcpy(file,pp); // file part *pfile = file; *pp = 0; // remove from dirk part pp = (char *) strrchr(file,'.'); if (! pp || strlen(pp) > 7) return 0; // file part, no .ext strcpy(ext,pp); // .ext part *pext = ext; *pp = 0; // remove from file part return 0; } /**************************************************************************/ // Move a source file to a destination file and delete the source file. // Equivalent to rename(), but the two files MAY be on different file systems. // Pathnames must be absolute (start with '/'). // Returns 0 if OK, +N if not. int renamez(cchar *file1, cchar *file2) // v.5.8 { char *pp1, *pp2; int err, Frename = 0; if (*file1 != '/' || *file2 != '/') return 1; // not absolute pathnames pp1 = strchr((char *) file1+1,'/'); pp2 = strchr((char *) file2+1,'/'); if (! pp1 || ! pp2) return 2; *pp1 = *pp2 = 0; if (strEqu(file1,file2)) Frename = 1; *pp1 = *pp2 = '/'; if (Frename) { err = rename(file1,file2); if (err) return errno; else return 0; } err = shell_quiet("mv -f %s %s",file1,file2); return err; } /************************************************************************** utility to measure CPU time spent in various functions or code blocks cpu_profile_init() initialize at start of test cpu_profile_enter(fnum) at entry to a function cpu_profile_exit(fnum) at exit from a function cpu_profile_report() report CPU time per function Methodology: cpu_profile_init() starts a thread that suspends and runs every 1 millisecond and updates a timer. cpu_profile_enter() and cpu_profile_exit() accumulate the time difference between entry and exit of code being measured. This may be zero because of the long interval between timer updates. Accuracy comes from statistical sampling over many seconds, so that if the time spent in a monitored function is significant, it will be accounted for. The accuracy is better than 1% as long as the measured function or code block consumes a second or more of CPU time during the measurement period. The "fnum" argument (1-99) designates the function or code block being measured. cpu_profile_report() stops the timer thread and reports time consumed per function, using the "fnum" tags in the report. The functions cpu_profile_enter() and cpu_profile_exit() subtract the timer difference and add to a counter per fnum, so the added overhead is insignificant. They are inline functions defined as follows: enter: cpu_profile_timer = cpu_profile_elapsed; exit: cpu_profile_table[fnum] += cpu_profile_elapsed - cpu_profile_timer; ***************************************************************************/ volatile double cpu_profile_table[100]; volatile double cpu_profile_timer; volatile double cpu_profile_elapsed; volatile int cpu_profile_kill = 0; void cpu_profile_init() { void * cpu_profile_timekeeper(void *); for (int ii = 0; ii < 99; ii++) cpu_profile_table[ii] = 0; cpu_profile_elapsed = 0; start_detached_thread(cpu_profile_timekeeper,null); } void cpu_profile_report() { cpu_profile_kill++; printz("elapsed: %.2f \n",cpu_profile_elapsed); for (int ii = 0; ii < 100; ii++) { double dtime = cpu_profile_table[ii]; if (dtime) printz("cpu profile func: %d time: %.2f \n",ii,dtime); } } void * cpu_profile_timekeeper(void *) { timeval time0, time1; gettimeofday(&time0,0); while (true) { gettimeofday(&time1,0); cpu_profile_elapsed = time1.tv_sec - time0.tv_sec + 0.000001 * (time1.tv_usec - time0.tv_usec); zsleep(0.001); if (cpu_profile_kill) break; } cpu_profile_kill = 0; pthread_exit(0); } /**************************************************************************/ // Returns hard page fault rate in faults/second. // First call starts a thread that runs every 2 seconds and keeps a // weighted average of hard fault rate for the last few intervals. namespace pagefaultrate_names { int ftf = 1; int samples = 0; int faultrate = 0; double time1, time2; void * threadfunc(void *); } int pagefaultrate() // v.5.8 { using namespace pagefaultrate_names; if (ftf) { ftf = 0; start_detached_thread(threadfunc,0); time1 = get_seconds(); } return faultrate; } void * pagefaultrate_names::threadfunc(void *) { using namespace pagefaultrate_names; FILE *fid; char *pp, buff[200]; double pfs1, pfs2, fps, elaps; while (true) { sleep(2); time2 = get_seconds(); elaps = time2 - time1; time1 = time2; fid = fopen("/proc/self/stat","r"); if (! fid) break; pp = fgets(buff,200,fid); fclose(fid); if (! pp) break; pp = strchr(pp,')'); // closing ')' after (short) filename if (pp) parseprocrec(pp+1,10,&pfs1,11,&pfs2,null); fps = (pfs1 + pfs2) / elaps; faultrate = 0.7 * faultrate + 0.3 * fps; } printz("pagefaultrate() failure \n"); pthread_exit(0); } /************************************************************************** strField() cchar * strField(cchar *string, cchar *delim, int Nth) Get the Nth field in input string, which contains at least N fields delimited by the character(s) in delim (e.g. blank, comma). Returns a pointer to the found field (actually a pointer to a copy of the found field, with a null terminator appended). If a delimiter is immediately followed by another delimiter, it is considered a field with zero length, and the string "" is returned. Leading blanks in a field are omitted from the returned field. A field with only blanks is returned as a single blank. The last field may be terminated by null or a delimiter. Characters within quotes (") are treated as data within a field, i.e. blanks and delimiters are not processed as such. The quotes are removed from the returned field. If there are less than N fields, a null pointer is returned. The last 100 fields are saved and recycled in a circular manner. The caller does not have to free memory. If more memory depth is needed, caller must copy the returned data elsewhere. The output string may be modified if the length is not increased. Fields within the input string must not exceed 2000 characters. Example: input string: ,a,bb, cc, ,dd"ee,ff"ggg, (first and last characters are comma) delimiter: comma Nth returned string 1: (null string) 2: a 3: bb 4: cc 5: (one blank) 6: ddee,ffggg 7: (null pointer >> no more fields) ***************************************************************************/ cchar * strField(cchar *string, cchar *delim, int Nth) { static int ftf = 1, nret = 0; static char *retf[100]; char *pf1, pf2[2000]; // 2000 limit v.5.2 cchar quote = '"'; int ii, nf, fcc = 0; static char blankstring[2], nullstring[1]; if (! string) return 0; // bad call v.5.3 if (Nth < 1) return 0; if (ftf) // overall first call { ftf = 0; for (ii = 0; ii < 100; ii++) retf[ii] = 0; strcpy(blankstring," "); *nullstring = 0; } pf1 = (char *) string - 1; // start parse nf = 0; while (nf < Nth) { pf1++; // start field nf++; fcc = 0; while (*pf1 == ' ') pf1++; // skip leading blanks while (true) { if (*pf1 == quote) { // pass chars between single quotes pf1++; // (but without the quotes) while (*pf1 && *pf1 != quote) pf2[fcc++] = *pf1++; if (*pf1 == quote) pf1++; } else if (strchr(delim,*pf1) || *pf1 == 0) break; // found delimiter or null else pf2[fcc++] = *pf1++; // pass normal character if (fcc > 1999) zappcrash("strField() too long"); // v.5.2 } if (*pf1 == 0) break; } if (nf < Nth) return 0; // no Nth field if (fcc == 0) { // empty field if (*string && pf1[-1] == ' ' && ! strchr(delim,' ')) // all blanks and non-blank delimeter, return blankstring; // return one blank if (*pf1 == 0) return 0; // no field return nullstring; // return null string } if (++nret == 100) nret = 0; // use next return slot if (retf[nret]) free(retf[nret]); retf[nret] = (char *) malloc(fcc+2); strncpy0(retf[nret],pf2,fcc+1); return retf[nret]; } cchar * strField(cchar *string, cchar delim, int Nth) // alternative with one delimiter { char delims[2] = "x"; *delims = delim; return strField(string,delims,Nth); } /************************************************************************** stat = strParms(begin, input, pname, maxcc, pval) Parse an input string with parameter names and values: "pname1=pval1 | pname2 | pname3=pval3 | pname4 ..." begin int & must be 1 to start new string, is modified input cchar * input string pname char * output parameter name maxcc int max. length for pname, including null pval double & output parameter value stat int status: 0=OK, -1=EOL, 1=parse error Each call returns the next pname and pval. A pname with no pval is assigned a value of 1 (present). Input format: pname1 | pname2=pval2 | pname3 ... null Leading blanks are ignored, and pnames may have imbedded blanks. pvals must convert to double using convSD (accepts decimal point or comma) ***/ int strParms(int &begin, cchar *input, char *pname, int maxcc, double &pval) { static int ii, beginx = 3579246; cchar *pnamex, *delim; int cc, err; if (begin == 1) { // start new string begin = ++beginx; ii = 0; } if (begin != beginx) zappcrash("strParms call error"); // thread safe, not reentrant *pname = 0; // initz. outputs to nothing pval = 0; while (input[ii] == ' ') ii++; // skip leading blanks if (input[ii] == 0) return -1; // no more data pnamex = input + ii; // next pname for (cc = 0; ; cc++) { // look for delimiter if (pnamex[cc] == '=') break; if (pnamex[cc] == '|') break; if (pnamex[cc] == 0) break; } if (cc == 0) return 1; // err: 2 delimiters if (cc >= maxcc) return 1; // err: pname too big strncpy0(pname,pnamex,cc+1); // pname >> caller strTrim(pname); // remove trailing blanks if (pnamex[cc] == 0) { // pname + null ii += cc; // position for next call pval = 1.0; // pval = 1 >> caller return 0; } if (pnamex[cc] == '|') { // pname + | ii += cc + 1; // position for next call pval = 1.0; // pval = 1 >> caller return 0; } ii += cc + 1; // pname = pval err = convSD(input + ii, pval, &delim); // parse pval (was strtod() if (err > 1) return 1; while (*delim == ' ') delim++; // skip poss. trailing blanks if (*delim && *delim != '|') return 1; // err: delimiter not | or null ii = delim - input; if (*delim) ii++; // position for next call return 0; } /**************************************************************************/ // Produce random value from hashed input string. // Output range is 0 to max-1. // Benchmark: 0.036 usec for 20 char. string 3.3 GHz Core i5 int strHash(cchar *string, int max) { uint hash = 1; uchar byte; while ((byte = *string++)) { hash *= byte; hash = hash ^ (hash >> 7); hash = hash & 0x00FFFFFF; } hash = hash % max; return hash; } /**************************************************************************/ // Hash an input string into a random printable (a-z) output string. // Returns outcc character random printable string in static memory. // Every output character is randomized from the entire input string. // With an outcc of 12, collision probability is 10**(-17). // Benchmark: 2.6 usec for 20 char. input, 12 char. output 3.3 GHz Core i5 cchar * strHash2(cchar *instring, int outcc) { int incc, ii, jj, rani = 0; int64 seed = 13579; static char outstring[40]; incc = strlen(instring); if (outcc > 39) zappcrash("strHash2() outcc > 39"); for (ii = 0; ii < outcc; ii++) { for (jj = 0; jj < incc; jj++) { seed = seed + instring[jj]; rani = lrandz(&seed); } outstring[ii] = 'a' + rani % 26; } outstring[ii] = 0; return outstring; } /**************************************************************************/ // Copy string with specified max. length (including null terminator). // truncate if needed. null terminator is always supplied. // Returns 0 if no truncation, 1 if input string was truncated to fit. int strncpy0(char *dest, cchar *source, uint cc) { strncpy(dest,source,cc); dest[cc-1] = 0; if (strlen(source) >= cc) return 1; // truncated else return 0; } /**************************************************************************/ // Copy string with blank pad to specified length. No null is added. void strnPad(char *dest, cchar *source, int cc) { strncpy(dest,source,cc); int ii = strlen(source); for (int jj = ii; jj < cc; jj++) dest[jj] = ' '; } /**************************************************************************/ // Remove trailing blanks from a string. Returns remaining length. int strTrim(char *dest, cchar *source) { if (dest != source) strcpy(dest,source); return strTrim(dest); } int strTrim(char *dest) { int ii = strlen(dest); while (ii && (dest[ii-1] == ' ')) dest[--ii] = 0; return ii; } /**************************************************************************/ // Remove leading and trailing blanks from a string. // Returns remaining length, possibly zero. int strTrim2(char *dest, cchar *source) { cchar *pp1, *pp2; int cc; pp1 = source; pp2 = source + strlen(source) - 1; while (*pp1 == ' ') pp1++; while (*pp2 == ' ' && pp2 > pp1) pp2--; cc = pp2 - pp1 + 1; memmove(dest,pp1,cc); dest[cc] = 0; return cc; } int strTrim2(char *string) { return strTrim2(string,(cchar *) string); } /**************************************************************************/ // Remove all blanks from a string. Returns remaining length. int strCompress(char *dest, cchar *source) { if (dest != source) strcpy(dest,source); return strCompress(dest); } int strCompress(char *string) { int ii, jj; for (ii = jj = 0; string[ii]; ii++) { if (string[ii] != ' ') { string[jj] = string[ii]; jj++; } } string[jj] = 0; return jj; } /**************************************************************************/ // Concatenate multiple strings, staying within a specified overall length. // The destination string is also the first source string. // Null marks the end of the source strings (omission --> crash). // Output is truncated to fit within the specified length. // A final null is assured and is included in the length. // Returns 0 if OK, 1 if truncation was needed. int strncatv(char *dest, int maxcc, cchar *source, ...) { cchar *ps; va_list arglist; maxcc = maxcc - strlen(dest) - 1; if (maxcc < 0) return 1; va_start(arglist,source); ps = source; while (ps) { strncat(dest,ps,maxcc); maxcc = maxcc - strlen(ps); if (maxcc < 0) break; ps = va_arg(arglist,cchar *); } va_end(arglist); if (maxcc < 0) return 1; return 0; } /**************************************************************************/ // Match 1st string to N additional strings. // Return matching string number 1 to N or 0 if no match. // Supply a null argument for end of list. int strcmpv(cchar *string, ...) { int match = 0; char *stringN; va_list arglist; va_start(arglist,string); while (1) { stringN = va_arg(arglist, char *); if (stringN == null) { va_end(arglist); return 0; } match++; if (strcmp(string,stringN) == 0) { va_end(arglist); return match; } } } /**************************************************************************/ // convert string to upper case void strToUpper(char *string) { int ii; char jj; const int delta = 'A' - 'a'; for (ii = 0; (jj = string[ii]); ii++) if ((jj >= 'a') && (jj <= 'z')) string[ii] += delta; } void strToUpper(char *dest, cchar *source) { strcpy(dest,source); strToUpper(dest); } /**************************************************************************/ // convert string to lower case void strToLower(char *string) { int ii; char jj; const int delta = 'a' - 'A'; for (ii = 0; (jj = string[ii]); ii++) if ((jj >= 'A') && (jj <= 'Z')) string[ii] += delta; } void strToLower(char *dest, cchar *source) { strcpy(dest,source); strToLower(dest); } /**************************************************************************/ // Copy string strin to strout, replacing every occurrence // of the substring ssin with the substring ssout. // Returns the count of replacements, if any. // Replacement strings may be longer or shorter or have zero length. int repl_1str(cchar *strin, char *strout, cchar *ssin, cchar *ssout) { int ccc, cc1, cc2, nfound; cchar *ppp; cc1 = strlen(ssin); cc2 = strlen(ssout); nfound = 0; while ((ppp = strstr(strin,ssin))) { nfound++; ccc = ppp - strin; strncpy(strout,strin,ccc); strout += ccc; strin += ccc; strncpy(strout,ssout,cc2); strin += cc1; strout += cc2; } strcpy(strout,strin); return nfound; } /**************************************************************************/ // Copy string strin to strout, replacing multiple substrings with replacement strings. // Multiple pairs of string arguments follow strout, a substring and a replacement string. // Last pair of string arguments must be followed by a null argument. // Returns the count of replacements, if any. // Replacement strings may be longer or shorter or have zero length. int repl_Nstrs(cchar *strin, char *strout, ...) { va_list arglist; cchar *ssin, *ssout; char ftemp[maxfcc]; int ftf, nfound; ftf = 1; nfound = 0; va_start(arglist,strout); while (true) { ssin = va_arg(arglist, char *); if (! ssin) break; ssout = va_arg(arglist, char *); if (ftf) { ftf = 0; nfound += repl_1str(strin,strout,ssin,ssout); } else { strcpy(ftemp,strout); nfound += repl_1str(ftemp,strout,ssin,ssout); } } va_end(arglist); return nfound; } /**************************************************************************/ // Copy and convert string to hex string. // Each input character 'A' >> 3 output characters "41 " void strncpyx(char *out, cchar *in, int ccin) { int ii, jj, c1, c2; char cx[] = "0123456789ABCDEF"; if (! ccin) ccin = strlen(in); for (ii = 0, jj = 0; ii < ccin; ii++, jj += 3) { c1 = (uchar) in[ii] >> 4; c2 = in[ii] & 15; out[jj] = cx[c1]; out[jj+1] = cx[c2]; out[jj+2] = ' '; } out[jj] = 0; return; } /**************************************************************************/ // Strip trailing zeros from ascii floating numbers // (e.g. 1.230000e+02 --> 1.23e+02) void StripZeros(char *pNum) { int ii, cc; int pp, k1, k2; char work[20]; cc = strlen(pNum); if (cc >= 20) return; for (ii = 0; ii < cc; ii++) { if (pNum[ii] == '.') { pp = ii; k1 = k2 = 0; for (++ii; ii < cc; ii++) { if (pNum[ii] == '0') { if (! k1) k1 = k2 = ii; else k2 = ii; continue; } if ((pNum[ii] >= '1') && (pNum[ii] <= '9')) { k1 = 0; continue; } break; } if (! k1) return; if (k1 == pp + 1) k1++; if (k2 < k1) return; strcpy(work,pNum); strcpy(work+k1,pNum+k2+1); strcpy(pNum,work); return; } } } /**************************************************************************/ // test for blank/null string // Returns status depending on input string: // 0 not a blank or null string // 1 argument string is NULL // 2 string has zero length (*string == 0) // 3 string is all blanks int blank_null(cchar *string) { if (! string) return 1; // null string if (! *string) return 2; // zero length string int cc = strlen(string); for (int ii = 0; ii < cc; ii++) if (string[ii] != ' ') return 0; // non-blank string return 3; // blank string } /**************************************************************************/ // make a copy of a string in heap memory and allocate more space // returned string is subject for free(); char * strdupz(cchar *string, int more) { char *pp = (char *) malloc(strlen(string)+1+more); strcpy(pp,string); return pp; } /**************************************************************************/ // clean \x escape sequences and replace them with the escaped character // \n >> newline \" >> doublequote \\ >> backslash etc. // see $ man ascii for the complete list int clean_escapes(char *string) { char *pp1 = string, *pp2 = string, *pp; char char1; char escapes[] = "abtnvfr"; int count = 0; while (true) { char1 = *pp1++; if (char1 == 0) { *pp2 = 0; return count; } else if (char1 == '\\') { char1 = *pp1++; pp = strchr(escapes,char1); if (pp) char1 = pp - escapes + 7; count++; } *pp2++ = char1; } } /**************************************************************************/ // Compute the graphic character count for a UTF8 character string. // Depends on UTF8 rules: // - ascii characters are positive (0x00 to 0x7F) // - 1st byte of multibyte sequence is negative (0xC0 to 0xFD) // - subsequent bytes are negative and < 0xC0 (0x80 to 0xBF) int utf8len(cchar *utf8string) { int ii, cc; char xlimit = 0xC0; for (ii = cc = 0; utf8string[ii]; ii++) { if (utf8string[ii] < 0) // multibyte character while (utf8string[ii+1] < xlimit) ii++; // skip extra bytes cc++; } return cc; } /**************************************************************************/ // Extract a UTF8 substring with a specified count of graphic characters. // utf8in input UTF8 string // utf8out output UTF8 string, which must be long enough // pos initial graphic character position to get (0 = first) // cc max. count of graphic characters to get // returns number of graphic characters extracted, <= cc // Output string is null terminated after last extracted character. int utf8substring(char *utf8out, cchar *utf8in, int pos, int cc) { int ii, jj, kk, posx, ccx; char xlimit = 0xC0; for (ii = posx = 0; posx < pos && utf8in[ii]; ii++) { if (utf8in[ii] < 0) while (utf8in[ii+1] < xlimit) ii++; posx++; } jj = ii; for (ccx = 0; ccx < cc && utf8in[jj]; jj++) { if (utf8in[jj] < 0) while (utf8in[jj+1] < xlimit) jj++; ccx++; } kk = jj - ii; strncpy(utf8out,utf8in+ii,kk); utf8out[kk] = 0; return ccx; } /**************************************************************************/ // check a string for valid utf8 encoding // returns: 0 = OK, 1 = bad string int utf8_check(cchar *string) { cchar *pp; unsigned char ch1, ch2, nch; for (pp = string; *pp; pp++) { ch1 = *pp; if (ch1 < 0x7F) continue; if (ch1 > 0xBF && ch1 < 0xE0) nch = 1; else if (ch1 < 0xF0) nch = 2; else if (ch1 < 0xF8) nch = 3; else if (ch1 < 0xFC) nch = 4; else if (ch1 < 0xFE) nch = 5; else return 1; while (nch) { pp++; ch2 = *pp; if (ch2 < 0x80 || ch2 > 0xBF) return 1; nch--; } } return 0; } /**************************************************************************/ // Find the Nth graphic character position within a UTF8 string // utf8in input UTF8 string // Nth graphic character position, zero based // returns starting character (byte) position of Nth graphic character // returns -1 if Nth is beyond the string length // v.5.5 int utf8_position(cchar *utf8in, int Nth) { int ii, posx; char xlimit = 0xC0; for (ii = posx = 0; posx < Nth && utf8in[ii]; ii++) { if (utf8in[ii] < 0) // multi-byte character while (utf8in[ii+1] && utf8in[ii+1] < xlimit) ii++; // traverse member bytes posx++; } if (utf8in[ii]) return ii; return -1; // v.5.5 } /************************************************************************** Conversion Utilities convSI(string, inum, delim) string to int convSI(string, inum, low, high, delim) string to int with range check convSD(string, dnum, delim) string to double convSD(string, dnum, low, high, delim) string to double with range check convSF(string, fnum, delim) string to float convSF(string, fnum, low, high, delim) string to float with range check convIS(inum, string, cc) int to string with returned cc convDS(fnum, digits, string, cc) double to string with specified digits of precision and returned cc string input (cchar *) or output (char *) inum input (int) or output (int &) dnum input (double) or output (double &) delim optional returned delimiter (null or cchar **) low, high input range check (int or double) cc output string length (int &) digits input digits of precision (int) to be used for output string NOTE: decimal point may be comma or period. 1000's separators must NOT be present. convIS and convDS also return the length cc of the string output. convDS accepts same formats as atof. Decimal point can be comma or period. convDS will use whatever format (f/e) gives the shortest result. Outputs like "e03" or "e+03" will be shortened to "e3". function status returned: 0 normal conversion, no invalid digits, blank/null termination 1 successful converstion, but trailing non-numeric found 2 conversion OK, but outside specified limits 3 null or blank string, converted to zero 4 conversion error, invalid data in string overlapping statuses have following precedence: 4 3 2 1 0 ***************************************************************************/ #define max10 (0x7fffffff / 10) // Convert string to integer int convSI(cchar *string, int &inum, cchar **delim) { char ch; int sign = 0, digits = 0, tnb = 0; cchar *pch = string; inum = 0; while ((ch = *pch) == ' ') pch++; // skip leading blanks if (ch == '-') sign = -1; // process leading +/- sign if (ch == '+') sign = 1; // (at most one sign character) if (sign) pch++; while ((*pch >= '0') && (*pch <= '9')) // process digits 0 - 9 { if (inum > max10) goto conv_err; // value too big inum = 10 * inum + *pch - '0'; digits++; pch++; } if (delim) *delim = pch; // terminating delimiter if (*pch && (*pch != ' ')) tnb++; // not null or blank if (! digits) // no digits found { if (tnb) return 4; // non-numeric (invalid) string else return 3; // null or blank string } if (sign == -1) inum = -inum; // negate if - sign if (! tnb) return 0; // no trailing non-numerics else return 1; // trailing non-numerics conv_err: inum = 0; return 4; } int convSI(cchar *string, int & inum, int lolim, int hilim, cchar **delim) { int stat = convSI(string,inum,delim); if (stat > 2) return stat; // invalid or null/blank if (inum < lolim) return 2; // return 2 if out of limits if (inum > hilim) return 2; // (has precedence over status 1) return stat; // limits OK, return 0 or 1 } // Convert string to double. int convSD(cchar *string, double &dnum, cchar **delim) { char ch; int ii, sign = 0, digits = 0, ndec = 0; int exp = 0, esign = 0, edigits = 0, tnb = 0; cchar *pch = string; static int first = 1; static double decimals[21], exponents[74]; if (first) // first-time called { first = 0; // pre-calculate constants for (ii = 1; ii <= 20; ii++) decimals[ii] = pow(10.0,-ii); for (ii = -36; ii <= 36; ii++) exponents[ii+37] = pow(10.0,ii); } dnum = 0.0; while ((ch = *pch) == ' ') pch++; // skip leading blanks if (ch == '-') sign = -1; // process leading +/- sign if (ch == '+') sign = 1; // (at most one sign character) if (sign) pch++; get_digits: while ((*pch >= '0') && (*pch <= '9')) // process digits 0 - 9 { dnum = 10.0 * dnum + (*pch - '0'); pch++; digits++; if (ndec) ndec++; } if ((*pch == '.') || (*pch == ',')) // process decimal point { // (allow comma or period) if (ndec) goto conv_err; ndec++; pch++; goto get_digits; } if ((*pch == 'e') || (*pch == 'E')) // process optional exponent { pch++; if (*pch == '+') esign = 1; // optional +/- sign if (*pch == '-') esign = -1; if (esign) pch++; if ((*pch < '0') || (*pch > '9')) goto conv_err; // 1st digit exp = *pch - '0'; edigits++; pch++; if ((*pch >= '0') && (*pch <= '9')) // optional 2nd digit { exp = 10 * exp + (*pch - '0'); edigits++; pch++; } if ((exp < -36) || (exp > 36)) goto conv_err; // exponent too big } if (delim) *delim = pch; // terminating delimiter if (*pch && (*pch != ' ')) tnb++; // not null or blank if (!(digits + edigits)) // no digits found { if (tnb) return 4; // non-numeric (invalid) string else return 3; // null or blank string } if (ndec > 1) dnum = dnum * decimals[ndec-1]; // compensate for decimal places if (sign == -1) dnum = - dnum; // negate if negative if (exp) { if (esign == -1) exp = -exp; // process exponent dnum = dnum * exponents[exp+37]; } if (! tnb) return 0; // no trailing non-numerics else return 1; // trailing non-numerics conv_err: dnum = 0.0; return 4; } int convSD(cchar *string, double &dnum, double lolim, double hilim, cchar **delim) { int stat = convSD(string,dnum,delim); if (stat > 2) return stat; // invalid or null/blank if (dnum < lolim) return 2; // return 2 if out of limits if (dnum > hilim) return 2; // (has precedence over status 1) return stat; // limits OK, return 0 or 1 } int convSF(cchar *string, float &fnum, cchar **delim) // v.5.2 { double dnum; int err; err = convSD(string,dnum,delim); fnum = dnum; return err; } int convSF(cchar *string, float &fnum, float lolim, float hilim, cchar **delim) // v.5.2 { double dnum, dlolim = lolim, dhilim = hilim; int err; err = convSD(string,dnum,dlolim,dhilim,delim); fnum = dnum; return err; } // Convert int to string with returned length. int convIS(int inum, char *string, int *cc) { int ccc; ccc = sprintf(string,"%d",inum); if (cc) *cc = ccc; return 0; } // Convert double to string with specified digits of precision. // Shortest length format (f/e) will be used. // Output length is returned in optional argument cc. int convDS(double dnum, int digits, char *string, int *cc) { char *pstr; sprintf(string,"%.*g",digits,dnum); pstr = strstr(string,"e+"); // 1.23e+12 > 1.23e12 if (pstr) strcpy(pstr+1,pstr+2); pstr = strstr(string,"e0"); // 1.23e02 > 1.23e2 if (pstr) strcpy(pstr+1,pstr+2); pstr = strstr(string,"e0"); if (pstr) strcpy(pstr+1,pstr+2); pstr = strstr(string,"e-0"); // 1.23e-02 > 1.23e-2 if (pstr) strcpy(pstr+2,pstr+3); pstr = strstr(string,"e-0"); if (pstr) strcpy(pstr+2,pstr+3); if (cc) *cc = strlen(string); return 0; } // format a number as "123 B" or "12.3 KB" or "1.23 MB" etc. // prec is the desired digits of precision to output. // WARNING: only the last 100 conversions remain available in memory. // Example formats for 3 digits of precision: // 12 B, 999 B, 1.23 KB, 98.7 KB, 456 KB, 2.34 MB, 45.6 GB, 12345 GB char * formatKBMB(double fnum, int prec) { #define kilo 1000 #define mega (kilo*kilo) #define giga (kilo*kilo*kilo) cchar *units; static char *output[100]; static int ftf = 1, ii; double gnum; if (ftf) { // keep last 100 conversions ftf = 0; for (ii = 0; ii < 100; ii++) output[ii] = (char *) malloc(20); } gnum = fabs(fnum); if (gnum > giga) { fnum = fnum / giga; units = "GB"; } else if (gnum > mega) { fnum = fnum / mega; units = "MB"; } else if (gnum > kilo) { fnum = fnum / kilo; units = "KB"; } else units = "B "; gnum = fabs(fnum); if (prec == 2 && gnum >= 99.5) prec++; // avoid e+nn formats if (prec == 3 && gnum >= 999.5) prec++; if (prec == 4 && gnum >= 9999.5) prec++; if (prec == 5 && gnum >= 99999.5) prec++; if (prec == 6 && gnum >= 999999.5) prec++; if (++ii > 99) ii = 0; snprintf(output[ii],20,"%.*g %s",prec,fnum,units); return output[ii]; } /************************************************************************** Wildcard string match Match candidate string to wildcard string containing any number of '*' or '?' wildcard characters. '*' matches any number of characters, including zero characters. '?' matches any one character. Returns 0 if match, 1 if no match. Benchmark: 0.032 usec. wild = *asdf*qwer?yxc 3.3 GHz Core i5 match = XXXasdfXXXXqwerXyxc ***************************************************************************/ int MatchWild(cchar *pWild, cchar *pString) { int ii, star; new_segment: star = 0; while (pWild[0] == '*') { star = 1; pWild++; } test_match: for (ii = 0; pWild[ii] && (pWild[ii] != '*'); ii++) { if (pWild[ii] != pString[ii]) { if (! pString[ii]) return 1; if (pWild[ii] == '?') continue; if (! star) return 1; pString++; goto test_match; } } if (pWild[ii] == '*') { pString += ii; pWild += ii; goto new_segment; } if (! pString[ii]) return 0; if (ii && pWild[ii-1] == '*') return 0; if (! star) return 1; pString++; goto test_match; } /************************************************************************** Wildcard string match - ignoring case Works like MatchWild() above, but case is ignored. ***/ int MatchWildIgnoreCase(cchar *pWild, cchar *pString) // v.5.5 { int ii, star; new_segment: star = 0; while (pWild[0] == '*') { star = 1; pWild++; } test_match: for (ii = 0; pWild[ii] && (pWild[ii] != '*'); ii++) { if (strncasecmp(pWild+ii,pString+ii,1) != 0) // the only code change { if (! pString[ii]) return 1; if (pWild[ii] == '?') continue; if (! star) return 1; pString++; goto test_match; } } if (pWild[ii] == '*') { pString += ii; pWild += ii; goto new_segment; } if (! pString[ii]) return 0; if (ii && pWild[ii-1] == '*') return 0; if (! star) return 1; pString++; goto test_match; } /************************************************************************** SearchWild - wildcard file search Find all files with total /pathname/filename matching a pattern, which may have any number of the wildcard characters '*' and '?' in either or both the pathname and filename. cchar * SearchWild(cchar *wfilespec, int &flag) inputs: flag = 1 to start a new search flag = 2 abort a running search *** do not modify flag within a search *** wfilespec = filespec to search with optional wildcards e.g. "/name1/na*me2/nam??e3/name4*.ext?" return: a pointer to one matching file is returned per call, or null when there are no more matching files. The search may be aborted before completion, but make a final call with flag = 2 to clean up temp file. A new search with flag = 1 will also finish the cleanup. NOT THREAD SAFE - do not use in parallel threads shell find command is used for the initial search because this is much faster than recursive use of readdir() (why?). (#) is used in place of (*) in comments below to prevent the compiler from interpreting (#/) as end of comments GNU find peculiarities: find /path/# omits "." files find /path/ includes "." files find /path/# recurses directories under /path/ find /path/#.txt does not recurse directories find /path/#/ finds all files under /path/ find /path/#/# finds files >= 1 directory level under /path/ find /path/xxx# never finds anything SearchWild uses simpler rules: '/' and '.' are treated like all other characters and match '#' and '?' no files are excluded except pure directories /path/#.txt finds all xxx.txt files under /path/ at all levels (because #.txt matches aaa.txt, /aaa/bbb.txt, etc.) Benchmark: search for /usr/share/#/README, find 457 from 101K files 1.9 secs. 3.3 GHz Core i5 SSD disk ***/ cchar * SearchWild(cchar *wpath, int &uflag) { static FILE *fid = 0; static char matchfile[maxfcc]; char searchpath[maxfcc]; char command[maxfcc]; int cc, err; char *pp; if ((uflag == 1) || (uflag == 2)) { // first call or stop flag if (fid) { pclose(fid); // if file open, close it fid = 0; } } if (uflag == 2) return 0; // kill flag, done if (uflag == 1) // first call flag { cc = strlen(wpath); if (cc == 0) return 0; if (cc > maxfcc-20) zappcrash("SearchWild: wpath > maxfcc"); pp = (char *) wpath; repl_Nstrs(pp,searchpath,"$","\\$","\"","\\\"",null); // init. search path, escape $ and " pp = strchr(searchpath,'*'); if (pp) { // not efficient but foolproof while ((*pp != '/') && (pp > searchpath)) pp--; // /aaa/bbb/cc*cc... >>> /aaa/bbb/ if (pp > searchpath) *(pp+1) = 0; } sprintf(command,"find \"%s\" -type f -or -type l",searchpath); // find files (ordinary, symlink) fid = popen(command,"r"); if (! fid) zappcrash(strerror(errno)); uflag = 763568954; // begin search } if (uflag != 763568954) zappcrash("SearchWild, uflag invalid"); while (true) { pp = fgets(matchfile,maxfcc-2,fid); // next matching file if (! pp) { pclose(fid); // no more fid = 0; return 0; } cc = strlen(matchfile); // get rid of trailing \n matchfile[cc-1] = 0; err = MatchWild(wpath,matchfile); // wildcard match? if (err) continue; // no return matchfile; // return file } } /************************************************************************** SearchWildIgnoreCase - wildcard file search - ignoring case Works like SearchWild() above, but case of file name is ignored. Actually, the trailing part of the path name is also case-insensitive, meaning that it is possible to get more matches than technically correct if directories like this are present: /AAA/BBB/.../filename /AAA/bbb/.../filename ***/ cchar * SearchWildIgnoreCase(cchar *wpath, int &uflag) // v.5.5 { static FILE *fid = 0; static char matchfile[maxfcc]; char searchpath[maxfcc]; char command[maxfcc]; int cc, err; char *pp; if ((uflag == 1) || (uflag == 2)) { // first call or stop flag if (fid) { pclose(fid); // if file open, close it fid = 0; } } if (uflag == 2) return 0; // kill flag, done if (uflag == 1) // first call flag { cc = strlen(wpath); if (cc == 0) return 0; if (cc > maxfcc-20) zappcrash("SearchWild: wpath > maxfcc"); pp = (char *) wpath; repl_Nstrs(pp,searchpath,"$","\\$","\"","\\\"",null); // init. search path, escape $ and " pp = strchr(searchpath,'*'); if (pp) { // not efficient but foolproof while ((*pp != '/') && (pp > searchpath)) pp--; // /aaa/bbb/cc*cc... >>> /aaa/bbb/ if (pp > searchpath) *(pp+1) = 0; } sprintf(command,"find \"%s\" -type f -or -type l",searchpath); // find files (ordinary, symlink) fid = popen(command,"r"); if (! fid) zappcrash(strerror(errno)); uflag = 763568954; // begin search } if (uflag != 763568954) zappcrash("SearchWild, uflag invalid"); while (true) { pp = fgets(matchfile,maxfcc-2,fid); // next matching file if (! pp) { pclose(fid); // no more fid = 0; return 0; } cc = strlen(matchfile); // get rid of trailing \n matchfile[cc-1] = 0; err = MatchWildIgnoreCase(wpath,matchfile); // wildcard match? if (err) continue; // no return matchfile; // return file } } /**************************************************************************/ // perform a binary search on sorted list of integers // return matching element or -1 if not found // Benchmark: search a list of 10 million sorted integers // 0.35 usecs. 3.3 GHz Core i5 int bsearch(int seekint, int nn, int list[]) { int ii, jj, kk, rkk; ii = nn / 2; // next element to search jj = (ii + 1) / 2; // next increment nn--; // last element rkk = 0; while (true) { kk = list[ii] - seekint; // check element if (kk > 0) { ii -= jj; // too high, go down if (ii < 0) return -1; } else if (kk < 0) { ii += jj; // too low, go up if (ii > nn) return -1; } else if (kk == 0) return ii; // matched jj = jj / 2; // reduce increment if (jj == 0) { jj = 1; // step by 1 element if (! rkk) rkk = kk; // save direction else { if (rkk > 0) { if (kk < 0) return -1; } // if change direction, fail else if (kk > 0) return -1; } } } } // Perform a binary search on sorted set of records in memory. // Return matching record number (0 based) or -1 if not found. // Benchmark: search 10 million sorted records of 20 chars. // 0.61 usecs. 3.3 GHz Core i5 int bsearch(char *seekrec, char *allrecs, int recl, int nrecs) { int ii, jj, kk, rkk; ii = nrecs / 2; // next element to search jj = (ii + 1) / 2; // next increment nrecs--; // last element rkk = 0; while (true) { kk = strcmp(allrecs+ii*recl,seekrec); // compare member rec to seek rec if (kk > 0) { ii -= jj; // too high, go down in set if (ii < 0) return -1; } else if (kk < 0) { ii += jj; // too low, go up in set if (ii > nrecs) return -1; } else if (kk == 0) return ii; // matched jj = jj / 2; // reduce increment if (jj == 0) { jj = 1; // step by 1 element if (! rkk) rkk = kk; // save direction else { if (rkk > 0) { if (kk < 0) return -1; } // if change direction, fail else if (kk > 0) return -1; } } } } // Perform a binary search on sorted set of pointers to records in memory. // Return matching record number (0 based) or -1 if not found. // The pointers are sorted in the order of the records starting at char N. // The records need not be sorted. // The string length of seekrec is compared. int bsearch(char *seekrec, char **allrecs, int N, int nrecs) { int ii, jj, kk, cc, rkk; cc = strlen(seekrec); ii = nrecs / 2; // next element to search jj = (ii + 1) / 2; // next increment nrecs--; // last element rkk = 0; while (true) { kk = strncmp(allrecs[ii]+N,seekrec,cc); // compare member rec to seek rec if (kk > 0) { ii -= jj; // too high, go down in set if (ii < 0) return -1; } else if (kk < 0) { ii += jj; // too low, go up in set if (ii > nrecs) return -1; } else if (kk == 0) return ii; // matched jj = jj / 2; // reduce increment if (jj == 0) { jj = 1; // step by 1 element if (! rkk) rkk = kk; // save direction else { if (rkk > 0) { if (kk < 0) return -1; } // if change direction, fail else if (kk > 0) return -1; } } } } /************************************************************************** heap sort functions void HeapSort(int list[], int nn) void HeapSort(float flist[], int nn) void HeapSort(double dlist[], int nn) void HeapSort(char *plist[], int nn) Sort list of nn integers, floats, doubles, or pointers to strings. Numbers are sorted in ascending order. Pointers are sorted in order of the strings they point to. The strings are not changed. Benchmarks: (3.3 GHz Core i5) 10 million integers: 1.5 secs 10 million doubles: 2.4 secs 2 million pointers to 100 character recs: 1.8 secs void HeapSort(char *plist[], int nn, compfunc) ---------------------------------------------- Sort list of nn pointers to strings. Pointers are sorted in order of the strings they point to, which is determined by the caller function compfunc. The strings are not changed. int compfunc(cchar *rec1, cchar *rec2) compare rec1 to rec2, return -1 0 +1 if rec1 < = > rec2 in sort order. void HeapSort(char *recs, int RL, int NR, compfunc) --------------------------------------------------- Sort an array of records in memory using a caller-supplied compare function. recs pointer to 1st record in array RL record length NR no. of records ***************************************************************************/ #define SWAP(x,y) (temp = (x), (x) = (y), (y) = temp) // heapsort for array of integers static void adjust(int vv[], int n1, int n2) { int *bb, jj, kk, temp; bb = vv - 1; jj = n1; kk = n1 * 2; while (kk <= n2) { if (kk < n2 && bb[kk] < bb[kk+1]) kk++; if (bb[jj] < bb[kk]) SWAP(bb[jj],bb[kk]); jj = kk; kk *= 2; } } void HeapSort(int vv[], int nn) { int *bb, jj, temp; for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn); bb = vv - 1; for (jj = nn-1; jj > 0; jj--) { SWAP(bb[1], bb[jj+1]); adjust(vv,1,jj); } } // heapsort for array of floats static void adjust(float vv[], int n1, int n2) { float *bb, temp; int jj, kk; bb = vv - 1; jj = n1; kk = n1 * 2; while (kk <= n2) { if (kk < n2 && bb[kk] < bb[kk+1]) kk++; if (bb[jj] < bb[kk]) SWAP(bb[jj],bb[kk]); jj = kk; kk *= 2; } } void HeapSort(float vv[], int nn) { float *bb, temp; int jj; for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn); bb = vv - 1; for (jj = nn-1; jj > 0; jj--) { SWAP(bb[1], bb[jj+1]); adjust(vv,1,jj); } } // heapsort for array of doubles static void adjust(double vv[], int n1, int n2) { double *bb, temp; int jj, kk; bb = vv - 1; jj = n1; kk = n1 * 2; while (kk <= n2) { if (kk < n2 && bb[kk] < bb[kk+1]) kk++; if (bb[jj] < bb[kk]) SWAP(bb[jj],bb[kk]); jj = kk; kk *= 2; } } void HeapSort(double vv[], int nn) { double *bb, temp; int jj; for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn); bb = vv - 1; for (jj = nn-1; jj > 0; jj--) { SWAP(bb[1], bb[jj+1]); adjust(vv,1,jj); } } // heapsort array of pointers to strings in ascending order of strings // pointers are sorted, strings are not changed. static void adjust(char *vv[], int n1, int n2) { char **bb, *temp; int jj, kk; bb = vv - 1; jj = n1; kk = n1 * 2; while (kk <= n2) { if (kk < n2 && strcmp(bb[kk],bb[kk+1]) < 0) kk++; if (strcmp(bb[jj],bb[kk]) < 0) SWAP(bb[jj],bb[kk]); jj = kk; kk *= 2; } } void HeapSort(char *vv[], int nn) { char **bb, *temp; int jj; for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn); bb = vv; for (jj = nn-1; jj > 0; jj--) { SWAP(bb[0], bb[jj]); adjust(vv,1,jj); } } // heapsort array of pointers to strings in user-defined order. // pointers are sorted, strings are not changed. static void adjust(char *vv[], int n1, int n2, HeapSortUcomp fcomp) { char **bb, *temp; int jj, kk; bb = vv - 1; jj = n1; kk = n1 * 2; while (kk <= n2) { if (kk < n2 && fcomp(bb[kk],bb[kk+1]) < 0) kk++; if (fcomp(bb[jj],bb[kk]) < 0) SWAP(bb[jj],bb[kk]); jj = kk; kk *= 2; } } void HeapSort(char *vv[], int nn, HeapSortUcomp fcomp) { char **bb, *temp; int jj; for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn,fcomp); bb = vv; for (jj = nn-1; jj > 0; jj--) { SWAP(bb[0], bb[jj]); adjust(vv,1,jj,fcomp); } } // heapsort for array of strings or records, // using caller-supplied record compare function. // HeapSortUcomp returns [ -1 0 +1 ] for rec1 [ < = > ] rec2 // method: build array of pointers and sort these, then // use this sorted array to re-order the records at the end. static int *vv1, *vv2; static void adjust(char *recs, int RL, int n1, int n2, HeapSortUcomp fcomp) { int *bb, jj, kk, temp; char *rec1, *rec2; bb = vv1 - 1; jj = n1; kk = n1 * 2; while (kk <= n2) { rec1 = recs + RL * bb[kk]; rec2 = recs + RL * bb[kk+1]; if (kk < n2 && fcomp(rec1,rec2) < 0) kk++; rec1 = recs + RL * bb[jj]; rec2 = recs + RL * bb[kk]; if (fcomp(rec1,rec2) < 0) SWAP(bb[jj],bb[kk]); jj = kk; kk *= 2; } } void HeapSort(char *recs, int RL, int NR, HeapSortUcomp fcomp) { int *bb, jj, kk, temp, flag; char *vvrec; vv1 = new int[NR]; for (jj = 0; jj < NR; jj++) vv1[jj] = jj; for (jj = NR/2; jj > 0; jj--) adjust(recs,RL,jj,NR,fcomp); bb = vv1 - 1; for (jj = NR-1; jj > 0; jj--) { SWAP(bb[1], bb[jj+1]); adjust(recs,RL,1,jj,fcomp); } vv2 = new int[NR]; for (jj = 0; jj < NR; jj++) vv2[vv1[jj]] = jj; vvrec = new char[RL]; flag = 1; while (flag) { flag = 0; for (jj = 0; jj < NR; jj++) { kk = vv2[jj]; if (kk == jj) continue; memmove(vvrec,recs+jj*RL,RL); memmove(recs+jj*RL,recs+kk*RL,RL); memmove(recs+kk*RL,vvrec,RL); SWAP(vv2[jj],vv2[kk]); flag = 1; } } delete vv1; delete vv2; delete vvrec; } /************************************************************************** int MemSort (char *RECS, int RL, int NR, int KEYS[][3], int NK) RECS is an array of records, to be sorted in-place. (record length = RL, record count = NR) KEYS[NK,3] is an integer array defined as follows: [N,0] starting position of Nth key field in RECS [N,1] length of Nth key field in RECS [N,2] type of sort for Nth key: 1 = char ascending 2 = char descending 3 = int*4 ascending 4 = int*4 descending 5 = float*4 ascending 6 = float*4 descending 7 = float*8 ascending (double) 8 = float*8 descending Benchmark: 2 million recs of 40 bytes with 4 sort keys: 2.5 secs (3.3 GHz Core i5). ***/ int MemSortComp(cchar *rec1, cchar *rec2); int MemSortKeys[10][3], MemSortNK; int MemSort(char *RECS, int RL, int NR, int KEYS[][3], int NK) { int ii; if (NR < 2) return 1; if (NK > 10) zappcrash("MemSort, bad NK"); if (NK < 1) zappcrash("MemSort, bad NK"); MemSortNK = NK; for (ii = 0; ii < NK; ii++) { MemSortKeys[ii][0] = KEYS[ii][0]; MemSortKeys[ii][1] = KEYS[ii][1]; MemSortKeys[ii][2] = KEYS[ii][2]; } HeapSort(RECS,RL,NR,MemSortComp); return 1; } int MemSortComp(cchar *rec1, cchar *rec2) { int ii, stat, kpos, ktype, kleng; int inum1, inum2; float rnum1, rnum2; double dnum1, dnum2; cchar *p1, *p2; for (ii = 0; ii < MemSortNK; ii++) // loop each key { kpos = MemSortKeys[ii][0]; // relative position kleng = MemSortKeys[ii][1]; // length ktype = MemSortKeys[ii][2]; // type p1 = rec1 + kpos; // absolute position p2 = rec2 + kpos; switch (ktype) { case 1: // char ascending stat = strncmp(p1,p2,kleng); // compare 2 key values if (stat) return stat; // + if rec1 > rec2, - if < break; // 2 keys are equal, check next key case 2: // char descending stat = strncmp(p1,p2,kleng); if (stat) return -stat; break; case 3: // int ascending memmove(&inum1,p1,4); memmove(&inum2,p2,4); if (inum1 > inum2) return 1; if (inum1 < inum2) return -1; break; case 4: // int descending memmove(&inum1,p1,4); memmove(&inum2,p2,4); if (inum1 > inum2) return -1; if (inum1 < inum2) return 1; break; case 5: // float ascending memmove(&rnum1,p1,4); memmove(&rnum2,p2,4); if (rnum1 > rnum2) return 1; if (rnum1 < rnum2) return -1; break; case 6: // float descending memmove(&rnum1,p1,4); memmove(&rnum2,p2,4); if (rnum1 > rnum2) return -1; if (rnum1 < rnum2) return 1; break; case 7: // double ascending memmove(&dnum1,p1,8); memmove(&dnum2,p2,8); if (dnum1 > dnum2) return 1; if (dnum1 < dnum2) return -1; break; case 8: // double descending memmove(&dnum1,p1,8); memmove(&dnum2,p2,8); if (dnum1 > dnum2) return -1; if (dnum1 < dnum2) return 1; break; default: // key type not 1-8 zappcrash("MemSort, bad KEYS sort type"); } } return 0; // records match on all keys } /************************************************************************** bitmap functions bitmap *bmap declare a bitmap pointer bmap = bitmap_new(nbits) create and initialize all bits to 0 bitmap_set(bmap, bit, value) set a bit value, 0 or 1 value = bitmap_get(bmap, bit) get a bit value, 0 or 1 bitmap_delete(bmap) delete a bitmap, release memory nbits int capacity of bitmap in bits bit int bit number to set or get, 0 .. nbits-1 value int true or false (1 or 0) ***************************************************************************/ // create a new bitmap with specified bit length. // initially all bits are false. bitmap * bitmap_new(int nbits) { int cc, ii; bitmap *bm; bm = (bitmap *) malloc(sizeof(bitmap)); bm->nbits = nbits; cc = (nbits + 7) / 8; bm->bits = (uchar *) malloc(cc); for (ii = 0; ii < cc; ii++) bm->bits[ii] = 0; return bm; } // set bit in bitmap to true or false void bitmap_set(bitmap *bm, int bit, bool value) { int ii, jj; uchar bit1; if (bit >= bm->nbits) zappcrash("bitmap, bit %d too big",bit); ii = bit / 8; jj = bit % 8; bit1 = 0x80 >> jj; if (value) bm->bits[ii] = bm->bits[ii] | bit1; else { bit1 = bit1 ^ 0xff; bm->bits[ii] = bm->bits[ii] & bit1; } return; } // fetch bitmap bit, return true or false bool bitmap_get(bitmap *bm, int bit) { int ii, jj; uchar bit1; ii = bit / 8; jj = bit % 8; bit1 = bm->bits[ii] << jj; if (bit1 < 127) return false; else return true; } // delete bitmap void bitmap_delete(bitmap *bm) { free(bm->bits); free(bm); return; } /************************************************************************** variable string list functions - array / list of strings pvlist * pvlist_create(int max) void pvlist_free(pvlist *pv) int pvlist_append(pvlist *pv, cchar *entry, int unique) int pvlist_prepend(pvlist *pv, cchar *entry, int unique) int pvlist_find(pvlist *pv, cchar *entry) int pvlist_remove(pvlist *pv, cchar *entry) int pvlist_remove(pvlist *pv, int Nth) int pvlist_count(pvlist *pv) int pvlist_replace(pvlist *pv, int Nth, char *entry) cchar * pvlist_get(pvlist *pv, int Nth) int pvlist_sort(pvlist *pv) These functions manage a variable length list of variable length strings. Declare such a list as: pvlist *pv; ***************************************************************************/ // Creates a pvlist with a capacity of max strings and returns a pointer. // String lengths are unlimited, but the count of strings is limited to max. // Memory is allocated for max pointers at first. Memory for the strings is // allocated and freed as the strings are added or removed. pvlist * pvlist_create(int max) { pvlist *pv; pv = (pvlist *) malloc(sizeof(pvlist)); pv->max = max; pv->act = 0; pv->list = (char **) malloc(max * sizeof(char *)); return pv; } // free memory for variable list and contained strings void pvlist_free(pvlist *pv) { int ii; for (ii = 0; ii < pv->act; ii++) free(pv->list[ii]); free(pv->list); free(pv); } // append new entry to end of list (optional if unique) // if list if full, first entry is removed and rest are packed down // return: N >= 0: new entry added at position N // N = -1: not unique, not added int pvlist_append(pvlist *pv, cchar *entry, int unique) { int ii; if (unique && pvlist_find(pv,entry) >= 0) return -1; // not unique if (pv->act == pv->max) pvlist_remove(pv,0); // if list full, remove 1st entry ii = pv->act; pv->list[ii] = strdup(entry); // add to end of list pv->act++; return ii; } // prepend new entry to list (optional if unique) // prior list entries are pushed down to make room // if list is full, last entry is removed first // return: N = 0: new entry added at position 0 // N = -1: not unique, not added int pvlist_prepend(pvlist *pv, cchar *entry, int unique) { int ii; if (unique && pvlist_find(pv,entry) >= 0) return -1; // not unique if (pv->act == pv->max) pvlist_remove(pv,pv->act-1); // if list full, remove last entry for (ii = pv->act; ii > 0; ii--) // push all list entries down pv->list[ii] = pv->list[ii-1]; pv->list[0] = strdup(entry); // add to start of list pv->act++; return 0; } // find list entry by name, return entry (0 based) // return -1 if not found int pvlist_find(pvlist *pv, cchar *entry) { int ii; for (ii = 0; ii < pv->act; ii++) if (strEqu(entry,pv->list[ii])) break; if (ii < pv->act) return ii; return -1; } // remove an entry by name and repack list // return (former) entry or -1 if not found int pvlist_remove(pvlist *pv, cchar *entry) { int ii; ii = pvlist_find(pv,entry); if (ii < 0) return -1; pvlist_remove(pv,ii); return ii; } // remove an entry by number and repack list // returns -1 if entry is beyond list end int pvlist_remove(pvlist *pv, int ii) { if (ii < 0 || ii >= pv->act) return -1; free(pv->list[ii]); for (++ii; ii < pv->act; ii++) { // pre-increment v.5.6 if (! pv->act) printz("meaningless reference %d",ii); // stop g++ optimization bug /// pv->list[ii-1] = pv->list[ii]; } pv->act--; return 0; } // return entry count int pvlist_count(pvlist *pv) { return pv->act; } // replace Nth entry with new one int pvlist_replace(pvlist * pv, int ii, cchar *entry) { if (ii < 0 || ii >= pv->act) return -1; free(pv->list[ii]); pv->list[ii] = strdup(entry); return 0; } // return Nth entry or null char * pvlist_get(pvlist *pv, int Nth) { if (Nth >= pv->act) return 0; return pv->list[Nth]; } // sort list in ascending order int pvlist_sort(pvlist *pv) { HeapSort(pv->list,pv->act); return 0; } /**************************************************************************/ // Random number generators with explicit context // and improved randomness over a small series. // Benchmark: lrandz 0.012 usec drandz 0.014 usec 3.3 GHz Core i5 int lrandz(int64 *seed) // returns 0 to 0x7fffffff { *seed = *seed ^ (*seed << 17); *seed = *seed ^ (*seed << 20); return nrand48((unsigned int16 *) seed); } int lrandz() // implicit seed, repeatable sequence { static int64 seed = 12345678; return lrandz(&seed); } double drandz(int64 *seed) // returns 0.0 to 0.99999... { *seed = *seed ^ (*seed << 17); *seed = *seed ^ (*seed << 20); return erand48((unsigned int16 *) seed); } double drandz() // implicit seed, repeatable sequence { static int64 seed = 23459876; return drandz(&seed); } /************************************************************************** spline1: define a curve using a set of data points (x and y values) spline2: for a given x-value, return a y-value fitting the curve For spline1, the no. of curve-defining points must be < 100. For spline2, the given x-value must be within the range defined in spline1. The algorithm was taken from the book "Numerical Recipes" (Cambridge University Press) and converted from Fortran to C++. ***/ namespace splinedata { int nn; float px1[100], py1[100], py2[100]; } void spline1(int dnn, float *dx1, float *dy1) { using namespace splinedata; float sig, p, u[100]; int ii; nn = dnn; if (nn > 100) zappcrash("spline1(), > 100 data points"); for (ii = 0; ii < nn; ii++) { px1[ii] = dx1[ii]; py1[ii] = dy1[ii]; if (ii && px1[ii] <= px1[ii-1]) zappcrash("spline1(), x-value not increasing"); } py2[0] = u[0] = 0; for (ii = 1; ii < nn-1; ii++) { sig = (px1[ii] - px1[ii-1]) / (px1[ii+1] - px1[ii-1]); p = sig * py2[ii-1] + 2; py2[ii] = (sig - 1) / p; u[ii] = (6 * ((py1[ii+1] - py1[ii]) / (px1[ii+1] - px1[ii]) - (py1[ii] - py1[ii-1]) / (px1[ii] - px1[ii-1])) / (px1[ii+1] - px1[ii-1]) - sig * u[ii-1]) / p; } py2[nn-1] = 0; for (ii = nn-2; ii >= 0; ii--) py2[ii] = py2[ii] * py2[ii+1] + u[ii]; return; } float spline2(float x) { using namespace splinedata; int kk, klo = 0, khi = nn-1; float h, a, b, y; while (khi - klo > 1) { kk = (khi + klo) / 2; if (px1[kk] > x) khi = kk; else klo = kk; } h = px1[khi] - px1[klo]; a = (px1[khi] - x) / h; b = (x - px1[klo]) / h; y = a * py1[klo] + b * py1[khi] + ((a*a*a - a) * py2[klo] + (b*b*b - b) * py2[khi]) * (h*h) / 6; return y; } /**************************************************************************/ // Add text strings to a FIFO queue, retrieve text strings. // v.5.7 // Can be used by one or two threads. // thread 1: open queue, get strings, close queue. // thread 2: put strings into queue. // initialize Qtext queue, empty status void Qtext_open(Qtext *qtext, int cap) { int cc; qtext->qcap = cap; qtext->qnewest = -1; qtext->qoldest = -1; qtext->qdone = 0; cc = cap * sizeof(char *); qtext->qtext = (char **) malloc(cc); memset(qtext->qtext,0,cc); return; } // add new text string to Qtext queue // if queue full, sleep until space is available void Qtext_put(Qtext *qtext, cchar *format, ...) { int qnext; va_list arglist; char message[200]; va_start(arglist,format); vsnprintf(message,199,format,arglist); va_end(arglist); qnext = qtext->qnewest + 1; if (qnext == qtext->qcap) qnext = 0; while (qtext->qtext[qnext]) zsleep(0.1); qtext->qtext[qnext] = strdup(message); qtext->qnewest = qnext; return; } // remove oldest text string from Qtext queue // if queue empty, return a null string char * Qtext_get(Qtext *qtext) { int qnext; char *text; if (qtext->qcap == 0) return 0; qnext = qtext->qoldest + 1; if (qnext == qtext->qcap) qnext = 0; text = qtext->qtext[qnext]; if (! text) return 0; qtext->qtext[qnext] = 0; qtext->qoldest = qnext; return text; } // free() any leftover strings void Qtext_close(Qtext *qtext) { for (int ii = 0; ii < qtext->qcap; ii++) if (qtext->qtext[ii]) free(qtext->qtext[ii]); qtext->qcap = 0; return; } /************************************************************************** Initialize application files according to following conventions: // new version + binary executable is at: /prefix/bin/appname // = PREFIX/bin/appname + other application directories are derived as follows: /prefix/share/appname/data/ desktop, parameters ... /prefix/share/doc/appname/ README, changelog, userguide-xx.html ... /prefix/share/appname/icons/ icon files: filename.png /prefix/share/appname/locales/ translate-xx.po ... (original) /home/user/.appname/ some installation files are copied here /home/user/.appname/logfile log file with error messages /home/user/.appname/locales translate-xx.po ... (user modified) zprefix install location normally /usr, has subtrees /bin /share /doc zdatadir installed data files /prefix/share/appname/data/ zdocdir documentation files /prefix/share/doc/appname/ zicondir icons /prefix/share/appname/icons/ zlocalesdir translation files /prefix/share/appname/locales/ zuserdir local app files /home//.appname /home//.appname/locales If it does not already exist, an application directory for the current user is created at /home/username/.appname (following common Linux convention). If this directory was created for the first time, copy specified files (following the 1st argument) from the install directory into the newly created user-specific directory. The assumption is that all initial data files for the application (e.g. parameters) will be in the install data directory, and these are copied to the user directory where the user or application can modify them. If the running program is not connected to a terminal device, stdout and stderr are redirected to the log file at /home/user/.appname/logfile char * get_zprefix() returns install top directory (has /bin and /share under it) char * get_zuserdir() returns /home/user/.appname (or /root/.appname) char * get_zdatadir() returns directory where application data files reside char * get_zdocdir() returns directory for application documentation files char * get_zicondir() returns directory for application icons char * get_zlocalesdir() returns directory for translation files ***/ cchar * get_zprefix() { return zfuncs::zprefix; } // /usr or /home/ cchar * get_zuserdir() { return zfuncs::zuserdir; } // /home/user/.appname cchar * get_zdatadir() { return zfuncs::zdatadir; } // parameters, icons cchar * get_zdocdir() { return zfuncs::zdocdir; } // documentation files cchar * get_zicondir() { return zfuncs::zicondir; } // icon files cchar * get_zlocalesdir() { return zfuncs::zlocalesdir; } // translation files int zinitapp(cchar *appname, ...) { using namespace zfuncs; char work[200]; char logfile[200], oldlog[200]; cchar *appfile; int cc, secs, err; time_t Tnow; char *chTnow; struct stat statdat; va_list arglist; FILE *fid; TRACE catch_signals(); // catch segfault, do backtrace strcpy(zappname,appname); // save app name v.5.6 #ifndef PREFIX // install location #define PREFIX "/usr" #endif strncpy0(work,PREFIX,199); // /usr or /home/ strcpy(zprefix,work); // /prefix strncatv(zdatadir,199,work,"/share/",zappname,"/data",null); // /prefix/share/appname/data strncatv(zicondir,199,work,"/share/",zappname,"/icons",null); // /prefix/share/appname/icons strncatv(zlocalesdir,199,work,"/share/",zappname,"/locales",null); // /prefix/share/appname/locales strncatv(zdocdir,199,work,"/share/doc/",zappname,null); // /prefix/share/doc/appname #ifdef DOCDIR strncpy0(zdocdir,DOCDIR,199); // flexible DOCDIR location #endif snprintf(zuserdir,199,"%s/.%s",getenv("HOME"),zappname); // /home//.appname/ cc = strlen(zuserdir); // stop humongous username v.5.3 if (cc > 160) zappcrash("too big: %s",zuserdir); err = stat(zuserdir,&statdat); // does it exist already? if (err) { err = mkdir(zuserdir,0750); // no, create and initialize if (err) zappcrash("cannot create %s",zuserdir); va_start(arglist,appname); // copy req. application files while (true) { // from /prefix/share/appname/data/* appfile = va_arg(arglist, cchar *); // to /home/user/.appname/* if (! appfile) break; err = shell_ack("cp %s/%s %s",zdatadir,appfile,zuserdir); } va_end(arglist); } tid_main = pthread_self(); // thread ID of main() process Tnow = time(0); chTnow = ctime(&Tnow); chTnow[19] = 0; if (! isatty(fileno(stdin))) { // not attached to a terminal v.5.7 snprintf(logfile,199,"%s/logfile",zuserdir); // /home//logfile v.5.5 snprintf(oldlog,199,"%s/logfile.old",zuserdir); err = stat(logfile,&statdat); if (! err) { secs = Tnow - statdat.st_mtime; // if log file age > 1 hour v.5.0 if (secs > 3600) rename(logfile,oldlog); // rename to *.old } fid = freopen(logfile,"a",stdout); // redirect output to log file fid = freopen(logfile,"a",stderr); if (! fid) printz("*** cannot redirect stdout and stderr \n"); } printz("\n =========== start %s %s \n",zappname,chTnow); fflush(0); // v.5.2 return 1; } // Find a locale-dependent installation file or user file. // file type: doc, data, locale, user, userlocale // file name: README, changelog, userguide.html, parameters, translate.po ... // Returns complete file name, e.g. /usr/share/appname/locales/translate-xx.po // Output filespec should be 200 bytes (limit for all installation files). // Returns 0 if OK, +N if not found. int locale_filespec(cchar *filetype, cchar *filename, char *filespec) // v.5.5 { using namespace zfuncs; char *pp, fname[20], fext[8]; char lc_RC[8]; // -lc or -lc_RC int cc, err; struct stat statb; filespec[0] = '/'; strcat(filespec,filetype); // leave /type as default if (strEqu(filetype,"doc")) strcpy(filespec,zdocdir); // /usr/share/doc/appname if (strEqu(filetype,"data")) strcpy(filespec,zdatadir); // /usr/share/appname/data if (strEqu(filetype,"locale")) strcpy(filespec,zlocalesdir); // /usr/share/appname/locales if (strEqu(filetype,"user")) strcpy(filespec,zuserdir); // /home//.appname if (strEqu(filetype,"userlocale")) { strcpy(filespec,zuserdir); // /home//.appname/locales strcat(filespec,"/locales"); } strncpy0(fname,filename,20); pp = strchr(fname,'.'); if (pp) { strcpy(fext,pp); // file type .fext *pp = 0; } else *fext = 0; // no type lc_RC[0] = '-'; strncpy0(lc_RC+1,zlang,6); // locale with region code: -lc_RC tryextras: cc = strlen(filespec); filespec[cc] = '/'; // /directories.../ strcpy(filespec+cc+1,fname); // /directories.../fname cc = strlen(filespec); // | pp = filespec + cc; // pp strcpy(pp,lc_RC); // /directories.../fname-lc_RC.fext strcat(pp,fext); err = stat(filespec,&statb); if (! err) return 0; strcpy(pp+3,fext); // /directories.../fname-lc.fext err = stat(filespec,&statb); if (! err) return 0; strcpy(pp,"-en"); // /directories.../fname-en.fext strcat(pp,fext); err = stat(filespec,&statb); if (! err) return 0; strcpy(pp,fext); // /directories.../fname.fext err = stat(filespec,&statb); if (! err) return 0; if (strEqu(filetype,"doc")) { // these files may be placed in strcpy(filespec,zdocdir); // /usr/share/doc/appname/extras strcat(filespec,"/extras"); // due to Linux chaos filetype = ""; goto tryextras; // try again using /extras } printz("file not found: %s %s \n",filetype,filename); return 1; } /**************************************************************************/ // Display help file in a separate process so application is not blocked. // help file: /zdatadir/userguide-lc_RC.html (or) *-lc.html (or) *-en.html // context: optional arg. show file starting at internal link = context // look for user guide file in /usr/share/data/appname/ [ extras/ ] void showz_userguide(cchar *context) // v.5.5 { char filespec[200], url[200]; int err; err = locale_filespec("data","userguide.html",filespec); if (err) { zmessageACK(null,null,ZTX("user guide not found")); return; } snprintf(url,199,"file://%s",filespec); if (context && *context) // specific topic wanted strncatv(url,199,"#",context,null); // file://.../userguide-xx.html#context showz_html(url); return; } /**************************************************************************/ // display application log file in a popup window // The log file is /home//.appname/logfile void showz_logfile() // log file v.5.2 { using namespace zfuncs; char buff[200]; fflush(0); // v.5.2 snprintf(buff,199,"cat %s/logfile",zuserdir); popup_command(buff,800,600); return; } // find and show a text file in /usr/share/doc/appname/ // or /usr/share/appname/data // the text file may also be a compressed .gz file // type is "doc" or "data" void showz_textfile(const char *type, const char *file) // v.5.5 { char filex[40], filespec[200], command[200]; int err; strncpy0(filex,file,36); // look for gzip file first v.5.7 strcat(filex,".gz"); err = locale_filespec(type,filex,filespec); if (! err) { snprintf(command,200,"zcat %s",filespec); popup_command(command,600,400); return; } strncpy0(filex,file,36); // look for uncompressed file err = locale_filespec(type,filex,filespec); if (! err) { snprintf(command,200,"cat %s",filespec); popup_command(command,600,400); return; } zmessageACK(0,0,"file not found: %s %s",type,file); return; } // show a local or remote html file using the user's preferred browser // to show a local file starting at an internal live link location: // url = "file://directory/.../filename#livelink void showz_html(cchar *url) { static char prog[20]; static int ftf = 1, err; if (ftf) { ftf = 0; *prog = 0; err = system("which firefox"); // use xdg-open only as last resort if (! err) strcpy(prog,"firefox"); // v.5.2 else { err = system("which chromium-browser"); if (! err) strcpy(prog,"chromium-browser"); else { err = system("which xdg-open"); if (! err) strcpy(prog,"xdg-open"); } } } if (! *prog) { zmessLogACK(null,"no firefox or chromium, cannot show document"); return; } shell_ack("%s %s &",prog,url); return; } /**************************************************************************/ // Creates a desktop icon / launcher and a system menu entry. // The menu name is taken from the input command, without options. // A command like "mycom -optA -optB" would generate a menu name of "mycom". // The categories should be separated by semicolons and conform to LSB categories. // The generic name is free text to describe the application, e.g. "Image Editor". // If the target system is not LSB compliant this function will not work. void zmake_menu_launcher(cchar *command, cchar *categories, cchar *genericname) { using namespace zfuncs; char appname[20], dtdir[200], dtfile[200], work[200]; cchar *xdgcomm = "xdg-user-dir DESKTOP"; cchar *pp; FILE *fid; pp = strField(command,' ',1); if (! pp) pp = "?"; strncpy0(appname,pp,20); fid = popen(xdgcomm,"r"); // get desktop directory for user locale if (! fid) { // v.5.8 zmessageACK(0,0,"%s \n %s",xdgcomm,strerror(errno)); return; } int nn = fscanf(fid,"%s",dtdir); pclose(fid); if (nn != 1) { zmessageACK(0,0,"xdg-user-dir DESKTOP failed"); return; } snprintf(dtfile,200,"%s/%s.desktop",dtdir,appname); fid = fopen(dtfile,"w"); if (! fid) { zmessageACK(0,0,"%s \n %s",dtfile,strerror(errno)); return; } fputs("[Desktop Entry]\n",fid); // [Desktop Entry] snprintf(work,199,"Name=%s\n",appname); // Name=appname fputs(work,fid); snprintf(work,199,"Categories=%s\n",categories); // Categories=Cat1;Cat2; ... fputs(work,fid); snprintf(work,199,"GenericName=%s\n",genericname); // GenericName=generic app name fputs(work,fid); fputs("Type=Application\n",fid); // Type=Application fputs("Terminal=false\n",fid); // Terminal=false snprintf(work,199,"Exec=%s/bin/%s\n",zprefix,command); // Exec=/usr/bin/appname -options fputs(work,fid); snprintf(work,199,"Icon=%s/%s.png\n",zicondir,appname); // /usr/share/appname/icons/appname.png fputs(work,fid); fclose(fid); shell_ack("chmod 0750 %s",dtfile); // make executable shell_ack("xdg-desktop-menu install --novendor %s",dtfile); // add menu entry return; } /************************************************************************** Translation Functions Translation files are standard .po files as used in the Gnu gettext system. However the .po files are used directly, and there is no need to merge and compile them into a binary format (.mo files). A translation file is one of: /translate-lc.po or *-lc_RC.po where "lc" is a standard language code and "lc_RC" a language and region code. The file may also be compressed with the file type .po.gz Translation files contain two record types: msgid "english text" msgstr "translation text" The text strings may continue on multiple lines, each such segment enclosed in quotes. The strings may contain C-format codes (%s %d etc.) and may contain escaped special characters (\n \" etc.). A text string may have a context part "context::string", where "context" is any short string, "::" is a separator, and "string" is the string to translate or the translation of a string. This is to handle the case where a single English string may need multiple translations, depending on context. The English string may be present multiple times in a .po file, each one marked with a different context and having a different translation. The context part is optional in the translation strings and is not displayed in the GUI. Initialize translations: int ZTXinit(cchar *lang) lang is "lc" or "lc_RC" or null (current locale will be used) Initializes the running application for the translation of text message strings for non-English locales. It reads a translation file which matches the English text strings in the source program to translated text strings for the user's locale. This file is uncompressed if necessary and copied into the user's files at /locales/translate-lc.po. This is done only if the file at is newer than the copy at . The local file is read and processed into a translation table for use by ZTX() and ZTX_translation_start(). lang: 2-character language code 'lc' ("de" "fr" "es" etc.) or 5-character language and region code 'lc_RC' ("de_AT" etc.) or null to use the current locale Status returned: 0 = OK, 1 = unable to process translation files. Translate a text string: cchar *translation = ZTX(cchar *english) english: text string which may have printf formats (%d %s ...) translation: the returned equivalent translation If the user language is English or if no translation is found, the input string is returned, else the translated string. example: program code: printf(ZTX("answer: %d %s \n next line"), 123, "qwerty"); A German .po file (translate-de.po) would have the following: msgid "" "answer: %d %s \n" " next line" msgstr "" "Antwort: %d %s \n" " nächste Zeile" ***************************************************************************/ namespace ZTXnames { FILE *fidr, *fidw; char buff[ZTXmaxcc], *ppq1, *ppq2; char *porec, *wporec; char Etext[ZTXmaxcc], Ttext[ZTXmaxcc]; // .po text: "line 1 %s \n" "line 2" char **etext, **ttext; // arrays, english and translations char **estring, **tstring; // merged, un-quoted, un-escaped int Ntext = 0; // array counts void ZTXgettext(char *text); char *ZTXmergetext(cchar *text); } // read and process .po file at application startup // prepare english strings and translations for quick access void ZTXinit(cchar *lang) // initialize translations { using namespace zfuncs; using namespace ZTXnames; int ii, err, Flocal = 0, Finstall = 0; time_t localdt = 0, installdt = 0; char localpo[200], installpo[200], ulocalesdir[200]; char *pp, poname[20], ponamexx[20]; struct stat statb; TRACE if (Ntext) { // free prior translation for (ii = 0; ii < Ntext; ii++) { free(etext[ii]); free(ttext[ii]); free(estring[ii]); free(tstring[ii]); } free(etext); free(ttext); free(estring); free(tstring); Ntext = 0; } etext = (char **) malloc(ZTXmaxent * sizeof(char *)); // english text and translations ttext = (char **) malloc(ZTXmaxent * sizeof(char *)); // (segmented, quoted, escaped) estring = (char **) malloc(ZTXmaxent * sizeof(char *)); // english strings and translations tstring = (char **) malloc(ZTXmaxent * sizeof(char *)); // (merged, un-quoted, un-escaped) if (lang && *lang) strncpy0(zlang,lang,6); // use language from caller else { // help Linux chaos pp = getenv("LANG"); // use $LANG if defined if (! pp) pp = getenv("LANGUAGE"); // use $LANGUAGE if defined if (! pp) pp = setlocale(LC_MESSAGES,""); // use locale if defined if (pp) strncpy0(zlang,pp,6); // "lc_RC" language/region code else strcpy(zlang,"en"); // use English } if (*zlang < 'a') strcpy(zlang,"en"); // use English if garbage printz("language: %s \n",zlang); if (strnEqu(zlang,"en",2)) return; // English, do nothing err = locale_filespec("userlocale","translate.po",localpo); if (! err) { Flocal = 1; // .po file was found in user area stat(localpo,&statb); localdt = statb.st_mtime; } err = locale_filespec("locale","translate.po",installpo); if (err) err = locale_filespec("locale","translate.po.gz",installpo); if (! err) { // install .po file was found Finstall = 1; stat(installpo,&statb); installdt = statb.st_mtime; } if (! Finstall && ! Flocal) { // no local or installed .po found printz("*** no translate-%s.po file found \n",zlang); return; } if (Finstall && Flocal) { // both local and install .po present if (installdt > localdt) { // local .po is stale, replace it printz("installed .po file is newer than local .po \n"); // v.5.5 Flocal = 0; } } if (Flocal) printz("using local .po file: %s \n",localpo); else { printz("using installed .po file: %s \n",installpo); snprintf(ulocalesdir,200,"%s/locales",zuserdir); // /home//.appname/locales mkdir(ulocalesdir,0750); // bugfix v.5.6 pp = strrchr(installpo,'/'); // extract filename from full path strcpy(poname,pp+1); pp = strstr(poname,".gz"); // uncompress/copy installed .po file if (pp) { // to local .po file v.5.5 *pp = 0; err = shell_ack("gunzip -c %s > %s/%s",installpo,ulocalesdir,poname); } else err = shell_ack("cp %s %s/%s",installpo,ulocalesdir,poname); if (err) return; pp = strstr(poname,"-en.po"); // if translate-en was used, if (pp) { // rename to the target locale code strcpy(ponamexx,poname); strcpy(poname+(pp+1-poname),zlang); // translate-en.po >> translate-xx.po strcat(poname,".po"); shell_ack("mv -f %s/%s %s/%s",ulocalesdir,ponamexx,ulocalesdir,poname); } sprintf(localpo,"%s/%s",ulocalesdir,poname); // final uncompressed local .po file printz("new local .po file: %s \n",localpo); } fidr = fopen(localpo,"r"); // open .po file if (! fidr) { printz("*** cannot open .po file: %s \n",localpo); return; } porec = 0; // no .po record yet *Etext = *Ttext = 0; // no text yet while (true) { if (! porec) porec = fgets_trim(buff,ZTXmaxcc,fidr); // get next .po record if (! porec) break; // EOF if (blank_null(porec)) { // blank record porec = 0; continue; } if (*porec == '#') { // comment porec = 0; continue; } if (strnEqu(porec,"msgid",5)) // start new english string { if (*Etext) { // two in a row printz("no translation: %s \n",Etext); *Etext = 0; } if (*Ttext) { printz("orphan translation: %s \n",Ttext); *Ttext = 0; } porec += 5; // get segmented text string ZTXgettext(Etext); // "segment1 %s \n" "segment2" ... } else if (strnEqu(porec,"msgstr",6)) // start new translation { porec += 6; // get segmented string ZTXgettext(Ttext); if (! *Etext) { printz("orphan translation: %s \n",Ttext); *Ttext = 0; continue; } if (strlen(Ttext) < 3) // translation is "" (quotes included) printz("no translation: %s \n",Etext); // leave as "" v.5.6 } else { printz("unrecognized .po record: %s \n",porec); porec = 0; continue; } if (*Etext && *Ttext) // have an english/translation pair { etext[Ntext] = strdup(Etext); // add to translation tables ttext[Ntext] = strdup(Ttext); *Etext = *Ttext = 0; Ntext++; if (Ntext == ZTXmaxent) // cannot continue zappcrash("more than %d translations",ZTXmaxent); } } fclose(fidr); printz(".po file has %d entries \n",Ntext); for (ii = 0; ii < Ntext; ii++) { pp = ZTXmergetext(etext[ii]); // merge segmented text strings estring[ii] = strdup(pp); pp = ZTXmergetext(ttext[ii]); tstring[ii] = strdup(pp); } return; } // private function // read and combine multiple 'msgid' or 'msgstr' quoted strings // output is one string with one or more quoted segments: // "text line 1 %s \n" "text line 2" ... // each segment comes from a different line in the input .po file void ZTXnames::ZTXgettext(char *pstring) { using namespace ZTXnames; int cc, scc = 0; while (true) // join multiple quoted strings { while (*porec && *porec != '"') porec++; // find opening string quote if (! *porec) { porec = fgets_trim(buff,ZTXmaxcc,fidr); // get next .po record if (! porec) return; if (strnEqu(porec,"msgid",5)) return; // end of this string if (strnEqu(porec,"msgstr",6)) return; } ppq1 = porec; // opening quote ppq2 = ppq1 + 1; while ((*ppq2 && *ppq2 != '"') || // find closing (non-escaped) quote (*ppq2 == '"' && *(ppq2-1) == '\\')) ppq2++; if (! *ppq2) return; cc = ppq2 - ppq1 + 1; // min. case is "" if (cc + 1 + scc >= ZTXmaxcc) printz("*** string is too long %s \n",pstring); else { strncpy0(pstring+scc,ppq1,cc+1); // accum. substrings, minus quotes scc += cc; } porec = ppq2 + 1; } return; } // private function // convert quoted string segments into binary form that // matches the compiled string in the source program // (remove quotes, merge strings, un-escape \n \" etc.) char * ZTXnames::ZTXmergetext(cchar *dirtystring) { static char cleanstring[ZTXmaxcc]; int ii, jj; strncpy0(cleanstring,dirtystring,ZTXmaxcc); clean_escapes(cleanstring); for (ii = jj = 0; cleanstring[ii]; ii++) if (cleanstring[ii] != '"') cleanstring[jj++] = cleanstring[ii]; cleanstring[jj] = 0; return cleanstring; } // Translate the input english string or return the input string. // Look for "context::string" and return "string" only if context found. // This function may need a few microseconds if thousands of strings must be searched. cchar * ZTX(cchar *english) { using namespace ZTXnames; cchar *pp, *pp2; int ii; if (! english) return "null"; for (ii = 0; ii < Ntext; ii++) // find english / translation pair if (strEqu(english,estring[ii])) break; if (ii < Ntext) pp = tstring[ii]; // translation else pp = english; // missing english in .po file if (strlen(pp) == 0) pp = english; // translation is "" v.5.6 for (pp2 = pp; *pp2 && pp2 < pp+30; pp2++) // remove context if present if (*pp2 == ':' && *(pp2+1) == ':') return pp2+2; return pp; } // Find all untranslated strings and return them one per call. // Set ftf = 1 for first call, will be returned = 0. // Returns null after last untranslated string. cchar * ZTX_missing(int &ftf) // v.5.6 { using namespace ZTXnames; int ii; static int next; if (ftf) ftf = next = 0; for (ii = next; ii < Ntext; ii++) if (strlen(tstring[ii]) == 0) break; // translation is "" v.5.6 next = ii + 1; if (ii < Ntext) return estring[ii]; // return english return 0; // EOL } /************************************************************************** GTK utility functions **************************************************************************/ // Iterate main loop every "skip" calls. // If called within the main() thread, does a GTK main loop to process menu events, etc. // You must do this periodically within long-running main() thread tasks if you wish to // keep menus, buttons, output windows, etc. alive and working. The skip argument will // cause the function to do nothing for skip calls, then perform the normal function. // This allows it to be imbedded in loops with little execution time penalty. // If skip = 1000, zmainloop() will do nothing for 1000 calls, execute normally, etc. // If called from a thread, zmainloop() does nothing. void zmainloop(int skip) { static int xskip = 0; if (skip) { if (++xskip < skip) return; xskip = 0; } if (! pthread_equal(pthread_self(),zfuncs::tid_main)) return; // thread caller, do nothing v.5.0 while (gtk_events_pending()) // gdk_flush() removed v.5.2 gtk_main_iteration_do(0); // use gtk_main_iteration_do v.5.2 return; } /**************************************************************************/ // crash if current execution is not the main() thread void zthreadcrash() { if (pthread_equal(pthread_self(),zfuncs::tid_main)) return; zappcrash("forbidden function called from thread"); return; } /**************************************************************************/ // write message to GTK text view window // line: +N existing lines from top (replace) // -N existing lines from bottom (replace) // 0 next line (add new line at bottom) // scroll logic assumes only one \n per message void wprintx(GtkWidget *mLog, int line, cchar *message, cchar *font) { GtkTextMark *endMark; GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; GtkTextTag *fontag = 0; int nlines, scroll = 0; if (! mLog) { // if no GUI use STDOUT printz("%s",message); return; } zthreadcrash(); // thread usage not allowed textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog)); if (! textBuff) return; // v.5.5 endMark = gtk_text_buffer_get_mark(textBuff,"wpxend"); // get my end mark if (! endMark) { gtk_text_buffer_get_end_iter(textBuff,&iter1); // new buffer, set my end mark endMark = gtk_text_buffer_create_mark(textBuff,"wpxend",&iter1,0); } nlines = gtk_text_buffer_get_line_count(textBuff); // lines now in buffer if (line == 0) scroll++; // auto scroll is on if (line < 0) { line = nlines + line + 1; // last lines: -1, -2 ... if (line < 1) line = 1; // above top, use line 1 } if (line > nlines) line = 0; // below bottom, treat as append if (line == 0) gtk_text_buffer_get_end_iter(textBuff,&iter1); // append new line if (line > 0) { gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line-1); // old line start if (line < nlines) gtk_text_buffer_get_iter_at_line(textBuff,&iter2,line); // old line end if (line == nlines) // or buffer end gtk_text_buffer_get_end_iter(textBuff,&iter2); gtk_text_buffer_delete(textBuff,&iter1,&iter2); // delete old line } if (font) { // insert new line with caller font fontag = gtk_text_buffer_create_tag(textBuff,0,"font",font,0); // fontag is textBuff specific gtk_text_buffer_insert_with_tags(textBuff,&iter1,message,-1,fontag,null); } else // insert new line with default font gtk_text_buffer_insert(textBuff,&iter1,message,-1); if (scroll) // scroll line into view gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(mLog),endMark,0,0,1,1); zmainloop(); return; } void wprintf(GtkWidget *mLog, int line, cchar *format, ... ) // "printf" version { va_list arglist; char message[1000]; va_start(arglist,format); vsnprintf(message,999,format,arglist); va_end(arglist); wprintx(mLog,line,message); return; } void wprintf(GtkWidget *mLog, cchar *format, ... ) // "printf", scrolling output { va_list arglist; char message[1000]; va_start(arglist,format); vsnprintf(message,999,format,arglist); // stop overflow, remove warning va_end(arglist); wprintx(mLog,0,message); return; } /**************************************************************************/ // scroll a text view window to put a given line on screen // 1st line = 1. for last line use line = 0. void wscroll(GtkWidget *mLog, int line) { GtkTextBuffer *textbuff; GtkTextIter iter; GtkTextMark *mark; if (! mLog) return; zthreadcrash(); // thread usage not allowed textbuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog)); if (! textbuff) return; // v.5.6 if (line <= 0) line = gtk_text_buffer_get_line_count(textbuff); line = line - 1; gtk_text_buffer_get_iter_at_line(textbuff,&iter,line); mark = gtk_text_buffer_create_mark(textbuff,0,&iter,0); // bugfix, gtk_text_view_scroll_to_iter() gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(mLog),mark,0,0,1,1); // fails with no error return; } /**************************************************************************/ // clear a text view window and get a new buffer (a kind of defrag) void wclear(GtkWidget *mLog) { GtkTextBuffer *buff; if (! mLog) return; zthreadcrash(); // thread usage not allowed buff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog)); if (! buff) return; // v.5.6 gtk_text_buffer_set_text(buff,"",-1); return; } // clear a text view window from designated line to end of buffer void wclear(GtkWidget *mLog, int line) { GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; if (! mLog) return; zthreadcrash(); // thread usage not allowed textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog)); if (! textBuff) return; // v.5.6 gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line-1); // iter at line start gtk_text_buffer_get_end_iter(textBuff,&iter2); gtk_text_buffer_delete(textBuff,&iter1,&iter2); // delete existing line return; } /**************************************************************************/ // Read from a text widget, one line at a time. // Set ftf = 1 for first call (will be returned = 0). // The next line of text is returned, or null if no more text. // A final \n character is removed if present. // Only one line is remembered, so it must be copied before the next call. char * wscanf(GtkWidget *mLog, int & ftf) { GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; static char *precs = 0, *prec1, *pret; static int cc; if (! mLog) return 0; zthreadcrash(); // thread usage not allowed if (ftf) { // get all window text ftf = 0; if (precs) g_free(precs); // free prior memory if there textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog)); // get all text if (! textBuff) return 0; // v.5.6 gtk_text_buffer_get_bounds(textBuff,&iter1,&iter2); precs = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0); prec1 = precs; // 1st record } if (! precs || (*prec1 == 0)) // no more records { if (precs) g_free(precs); precs = 0; return 0; } cc = 0; while ((prec1[cc] != 0) && (prec1[cc] != '\n')) cc++; // scan for terminator pret = prec1; prec1 = prec1 + cc; // next record if (*prec1 == '\n') prec1++; pret[cc] = 0; // replace \n with 0 return pret; } /**************************************************************************/ // dump text window into file // return: 0: OK +N: error int wfiledump_maxcc = 0; int wfiledump(GtkWidget *mLog, char *filespec) { FILE *fid; char *prec; int ftf, err, cc; if (! mLog) return 0; fid = fopen(filespec,"w"); // open file if (! fid) { zmessageACK(null,null,ZTX("cannot open file %s"),filespec); return 1; } wfiledump_maxcc = 0; ftf = 1; while (true) { prec = wscanf(mLog,ftf); // get text line if (! prec) break; fprintf(fid,"%s\n",prec); // output with \n cc = strlen(prec); if (cc > wfiledump_maxcc) wfiledump_maxcc = cc; } err = fclose(fid); // close file if (err) { zmessageACK(null,null,"file close error"); return 2; } else return 0; } /**************************************************************************/ // save text window to file, via file chooser dialog void wfilesave(GtkWidget *mLog) { int err; char *file; if (! mLog) return; file = zgetfile(ZTX("save screen to file"),"save","screen-save.txt"); if (! file) return; err = wfiledump(mLog,file); if (err) zmessageACK(null,null,"save screen failed (%d)",err); free(file); return; } /**************************************************************************/ // print text window to default printer // use landscape mode if max. print line > A4 width void wprintp(GtkWidget *mLog) { int pid, err; char tempfile[50]; if (! mLog) return; pid = getpid(); snprintf(tempfile,49,"/tmp/wprintp-%d",pid); err = wfiledump(mLog,tempfile); if (err) return; if (wfiledump_maxcc < 97) err = shell_ack("lp -o %s -o %s -o %s -o %s -o %s -o %s %s", "cpi=14","lpi=8","page-left=50","page-top=50", "page-right=40","page-bottom=40",tempfile); else err = shell_ack("lp -o %s -o %s -o %s -o %s -o %s -o %s -o %s %s", "landscape","cpi=14","lpi=8","page-left=50","page-top=50", "page-right=40","page-bottom=40",tempfile); return; } /**************************************************************************/ // Set a function to be called when a GTK text view widget is mouse-clicked. // Function returns the clicked line number and position, both zero based. // A wrapped line is still one logical line. // Note: cannot be used on text windows that are edited. // The called function looks like this: // void clickfunc(GtkWidget *widget, int &line, int &pos) void textwidget_set_clickfunc(GtkWidget *widget, clickfunc_t clickfunc) { int textwidget_mousefunc(GtkWidget *widget, GdkEventButton *event, clickfunc_t clickfunc); gtk_widget_add_events(widget,GDK_ALL_EVENTS_MASK); // connect events v.5.2 G_SIGNAL(widget,"event",textwidget_mousefunc,clickfunc); return; } int textwidget_mousefunc(GtkWidget *widget, GdkEventButton *event, clickfunc_t clickfunc) { static GdkCursor *arrowcursor = 0; GdkWindow *gdkwin; GtkTextIter iter1; int mpx, mpy, tbx, tby, line, pos; #define TEXT GTK_TEXT_WINDOW_TEXT #define VIEW GTK_TEXT_VIEW #define ARROW GDK_TOP_LEFT_ARROW if (! arrowcursor) arrowcursor = gdk_cursor_new(ARROW); gdkwin = gtk_text_view_get_window(VIEW(widget),TEXT); if (gdkwin) gdk_window_set_cursor(gdkwin,arrowcursor); // why must this be repeated? v.5.5 if (event->type != GDK_BUTTON_RELEASE) return 0; // v.5.7 mpx = int(event->x); // mouse click position mpy = int(event->y); gtk_text_view_window_to_buffer_coords(VIEW(widget),TEXT,mpx,mpy,&tbx,&tby); gtk_text_view_get_iter_at_location(VIEW(widget),&iter1,tbx,tby); line = gtk_text_iter_get_line(&iter1); // clicked line pos = gtk_text_iter_get_line_offset(&iter1); // clicked position clickfunc(widget,line,pos); // call user function return 0; // v.5.6 } // get a given line of text from a GTK text view widget // returned text is subject for free() char * textwidget_get_line(GtkWidget *widget, int line, int hilite) { GtkTextBuffer *textbuffer; GtkTextIter iter1, iter2; char *text, *ztext; int cc; textbuffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); if (! textbuffer) return 0; // v.5.6 gtk_text_buffer_get_iter_at_line(textbuffer,&iter1,line); iter2 = iter1; gtk_text_iter_forward_line(&iter2); if (hilite) // highlight selected line v.5.6 gtk_text_buffer_select_range(textbuffer,&iter1,&iter2); text = gtk_text_buffer_get_text(textbuffer,&iter1,&iter2,0); if (! text) return 0; ztext = strdup(text); free(text); cc = strlen(ztext); while (cc && ztext[cc-1] < ' ') cc--; ztext[cc] = 0; if (cc == 0) { free(ztext); return 0; } return ztext; } // get the clicked word within the line // words are defined by line starts and ends, and the given delimiter // returns word and delimiter (&end) char * textwidget_get_word(char *line, int pos, cchar *dlims, char &end) { char *pp1, *pp2, *ztext; int cc; if (! line) return 0; pos = utf8_position(line,pos); // graphic position to byte position if (pos < 0) return 0; // v.5.5 pp1 = line + pos; if (! *pp1 || strchr(dlims,*pp1)) return 0; // reject edge position or delimiter while (pp1 > line && ! strchr(dlims,pp1[-1])) pp1--; // find start of word pp2 = pp1; while (pp2[1] && ! strchr(dlims,pp2[1])) pp2++; // find following delimiter or EOL end = pp2[1]; // return delimiter while (*pp1 == ' ') pp1++; // no leading or trailing blanks while (*pp2 == ' ') pp2--; cc = pp2 - pp1 + 1; if (cc < 1) return 0; // all blanks? ztext = (char *) malloc(cc+1); strncpy0(ztext,pp1,cc+1); return ztext; } /**************************************************************************/ // get the screen and mouse device for a given widget // v.5.8 // returns 0 on success, +N on error int get_mouse_device(GtkWidget *widget, GdkScreen **screen, GdkDevice **mouse) { GdkWindow *window; GdkDisplay *display; // X workstation (KB, mouse, screen) GdkDeviceManager *manager; // knows screen / mouse associations zthreadcrash(); // thread usage not allowed window = gtk_widget_get_window(widget); if (! window) return 1; display = gdk_window_get_display(window); if (! display) return 2; *screen = gdk_window_get_screen(window); if (! *screen) return 3; manager = gdk_display_get_device_manager(display); if (! manager) return 4; *mouse = gdk_device_manager_get_client_pointer(manager); if (! *mouse) return 5; return 0; } /************************************************************************** simplified GTK menu bar, tool bar, status bar functions These functions simplify the creation of GTK menus and toolbars. The functionality is limited but adequate for most purposes. mbar = create_menubar(vbox) create menubar mitem = add_menubar_item(mbar, label, func) add menu item to menubar msub = add_submenu_item(mitem, label, func, tip) add submenu item to menu or submenu tbar = create_toolbar(vbox, iconsize) create toolbar add_toolbar_button(tbar, label, tip, icon, func) add button to toolbar stbar = create_stbar(vbox) create status bar stbar_message(stbar, message) display message in status bar These functions to the following: * create a menu bar and add to existing window verticle packing box * add menu item to menu bar * add submenu item to menu bar item or submenu item * create a toolbar and add to existing window * add button to toolbar, using stock icon or custom icon * create a status bar and add to existing window * display a message in the status bar argument definitions: vbox GtkWidget * a verticle packing box (in a window) mbar GtkWidget * reference for menu bar popup GtkWidget * reference for popup menu mitem GtkWidget * reference for menu item (in a menu bar) msub GtkWidget * reference for submenu item (in a menu) label cchar * menu or toolbar name or label tbar GtkWidget * reference for toolbar tip cchar * tool button tool tip (popup text via mouse-over) icon cchar * stock icon name or custom icon file name (see below) func see below menu or tool button response function arg cchar * argument to response function stbar int reference for status bar message cchar * message to display in status bar The icon argument for the function add_toolbar_button() has two forms. For a GTK stock item referenced with a macro like GTK_STOCK_OPEN, use the corresponding text name, like "gtk-open". For a custom icon, use the icon's file name like "my-icon.png". The file is expected to be in get_zdatadir()/icons. The icon file may be any size, and is resized to 32x32 for use on the toolbar. If the file is not found, the stock icon "gtk-missing-image" is used (".png" and ".jpg" files both work). For a button with no icon (text label only), use 0 or null for the icon argument. For a menu separator, use the menu name "separator". For a toolbar separator, use the label "separator". For a title menu (no response function), set the response function to null. The response function for both menus and toolbar buttons looks like this: void func(GtkWidget *, cchar *) The following macro is also supplied to simplify the coding of response functions: G_SIGNAL(window,event,func,arg) which expands to: g_signal_connect(G_OBJECT(window),event,G_CALLBACK(func),(void *) arg) ***************************************************************************/ // create menu bar and add to vertical packing box GtkWidget * create_menubar(GtkWidget *vbox) // icon size removed { GtkWidget *wmbar; wmbar = gtk_menu_bar_new(); gtk_box_pack_start(GTK_BOX(vbox),wmbar,0,0,0); return wmbar; } // add menu item to menu bar, with optional response function GtkWidget * add_menubar_item(GtkWidget *wmbar, cchar *mname, cbFunc func) { GtkWidget *wmitem; wmitem = gtk_menu_item_new_with_label(mname); gtk_menu_shell_append(GTK_MENU_SHELL(wmbar),wmitem); if (func) G_SIGNAL(wmitem,"activate",func,mname); return wmitem; } // add submenu item to menu item, with optional response function GtkWidget * add_submenu_item(GtkWidget *wmitem, cchar *mlab, cbFunc func, cchar *mtip) { GtkWidget *wmsub, *wmsubitem; wmsub = gtk_menu_item_get_submenu(GTK_MENU_ITEM(wmitem)); // add submenu if not already if (wmsub == null) { wmsub = gtk_menu_new(); gtk_menu_item_set_submenu(GTK_MENU_ITEM(wmitem),wmsub); } if (strEqu(mlab,"separator")) wmsubitem = gtk_separator_menu_item_new(); else wmsubitem = gtk_menu_item_new_with_label(mlab); // add menu item with label only gtk_menu_shell_append(GTK_MENU_SHELL(wmsub),wmsubitem); // append submenu item to submenu if (func) G_SIGNAL(wmsubitem,"activate",func,mlab); // connect optional response function if (mtip) g_object_set(G_OBJECT(wmsubitem),"tooltip-text",mtip,null); // add optional popup menu tip v.5.2 return wmsubitem; } /**************************************************************************/ // create toolbar and add to vertical packing box int tbIconSize = 24; // valid during toolbar construction GtkWidget * create_toolbar(GtkWidget *vbox, int iconsize) { using namespace zfuncs; GtkWidget *wtbar; wtbar = gtk_toolbar_new(); // gtk_toolbar_set_style(GTK_TOOLBAR(wtbar),GTK_TOOLBAR_BOTH); // v.5.8 gtk_box_pack_start(GTK_BOX(vbox),wtbar,0,0,0); tbIconSize = iconsize; return wtbar; } // add toolbar button with label and icon ("iconfile.png") and tool tip // at least one of label and icon should be present GtkWidget * add_toolbar_button(GtkWidget *wtbar, cchar *blab, cchar *btip, cchar *icon, cbFunc func) { using namespace zfuncs; GtkToolItem *tbutton; GError *gerror = 0; GdkPixbuf *pixbuf; GtkWidget *wicon = 0; char iconpath[200]; if (blab && strEqu(blab,"separator")) { tbutton = gtk_separator_tool_item_new(); gtk_toolbar_insert(GTK_TOOLBAR(wtbar),GTK_TOOL_ITEM(tbutton),-1); return (GtkWidget *) tbutton; } if (icon && *icon) { // get icon pixbuf *iconpath = 0; strncatv(iconpath,199,zicondir,"/",icon,null); pixbuf = gdk_pixbuf_new_from_file_at_scale(iconpath,tbIconSize,tbIconSize,1,&gerror); if (pixbuf) wicon = gtk_image_new_from_pixbuf(pixbuf); } tbutton = gtk_tool_button_new(wicon,blab); // v.5.8 if (! wicon) gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(tbutton),"gtk-missing-image"); if (btip) gtk_tool_item_set_tooltip_text(tbutton,btip); gtk_tool_item_set_homogeneous(tbutton,0); gtk_toolbar_insert(GTK_TOOLBAR(wtbar),GTK_TOOL_ITEM(tbutton),-1); if (func) G_SIGNAL(tbutton,"clicked",func,blab); return (GtkWidget *) tbutton; } /**************************************************************************/ // create a status bar and add to the start of a packing box GtkWidget * create_stbar(GtkWidget *pbox) { GtkWidget *stbar; static PangoFontDescription *fontdesc; stbar = gtk_statusbar_new(); fontdesc = pango_font_description_from_string("Monospace 9"); gtk_widget_override_font(stbar,fontdesc); gtk_box_pack_start(GTK_BOX(pbox),stbar,0,0,0); gtk_widget_show(stbar); return stbar; } // display message in status bar int stbar_message(GtkWidget *wstbar, cchar *message) { static int ctx = -1; if (ctx == -1) ctx = gtk_statusbar_get_context_id(GTK_STATUSBAR(wstbar),"all"); gtk_statusbar_pop(GTK_STATUSBAR(wstbar),ctx); gtk_statusbar_push(GTK_STATUSBAR(wstbar),ctx,message); return 0; } /************************************************************************** Popup Menu popup = create_popmenu() create a popup menu mitem = add_popmenu_item(popup, label, func, arg, tip) add menu item to popup menu popup_menu(GtkWidget *widget, popup) popup the menu at mouse position GtkWidget *popup, *mitem cchar *label, *arg, *tip void func(GtkWidget *, cchar *arg) Call 'create_popmenu' and then 'add_popmenu_item' for each item in the menu. 'label' is the menu name, 'func' the response function, 'arg' an argument for 'func', and 'tip' is a tool-tip. 'arg' and 'tip' may be null. A call to 'popup_menu' will show all menu entries at the mouse position. Clicking an entry will call the respective response function. Hovering on the entry will show the tool-tip. The response function looks like this: void func(GtkWidget *, cchar *) ***/ // create a popup menu GtkWidget * create_popmenu() { int popmenu_event(GtkWidget *, GdkEvent *); GtkWidget *popmenu; popmenu = gtk_menu_new(); gtk_widget_add_events(popmenu,GDK_BUTTON_PRESS_MASK); // v.5.5 G_SIGNAL(popmenu,"button-press-event",popmenu_event,0); // no event passed in Ubuntu 13.10 v.5.7 return popmenu; } // handle mouse button event in a popup menu int popmenu_event(GtkWidget *popmenu, GdkEvent *event) // not called in Ubuntu 13.10 v.5.7 { if (((GdkEventButton *) event)->button != 1) // if not left mouse, kill menu gtk_menu_popdown(GTK_MENU(popmenu)); return 0; } // add a menu item to a popup menu GtkWidget * add_popmenu_item(GtkWidget *popmenu, cchar *mname, cbFunc func, cchar *arg, cchar *mtip) { GtkWidget *wmitem; wmitem = gtk_menu_item_new_with_label(mname); if (mtip) g_object_set(G_OBJECT(wmitem),"tooltip-text",mtip,null); // add optional popup menu tip v.5.5 gtk_menu_shell_append(GTK_MENU_SHELL(popmenu),wmitem); if (func) { if (arg) G_SIGNAL(wmitem,"activate",func,arg); // call func with arg v.5.3 else G_SIGNAL(wmitem,"activate",func,mname); // call func with menu name } return wmitem; } // pop-up the menu at current mouse position void popup_menu(GtkWidget *widget, GtkWidget *popmenu) { void popup_moveloc(GtkMenu *, int *, int *, int *, void *); // v.5.8 gtk_menu_popup(GTK_MENU(popmenu),0,0,popup_moveloc,widget,1,GDK_CURRENT_TIME); gtk_widget_show_all(popmenu); return; } // move popup menu to the right of the mouse to avoid automatically // v.5.8 // selecting the first entry when the mouse button is released void popup_moveloc(GtkMenu *menu, int *mx, int *my, int *push, void *widget) { GdkScreen *screen; GdkDevice *mouse; int err; err = get_mouse_device((GtkWidget *) widget,&screen,&mouse); if (err) return; gdk_device_get_position(mouse,&screen,mx,my); *mx += 20; *my += 10; *push = 0; return; } /************************************************************************** Customizable Graphic Popup Menu void gmenuz(GtkWidget *parent, cchar *configfile, callbackfunc) Open a popup window with a customizable graphic menu. parent parent window or null configfile menu configuration file, will be created if missing callbackfunc callback function to receive clicked menu entry: typedef void callbackfunc(cchar *menu) This function allows an application to offer a customizable menu which a user can populate with frequently used (menu) functions, arranged as desired. A menu entry selected by the user is passed to the application for execution. The initial popup window is blank. Right click an empty space on the popup window to define a new menu entry. Right click an existing entry to modify it. Use the resulting dialog to define or change the menu entry. menu text optional text appearing in the popup window menu func text that is passed to the application callback function menu icon optional menu icon: /directory.../filename.png icon size rendered icon size in the popup window, 24x24 to 64x64 pixels close checkbox: option to close the popup window when menu is used Left drag a menu entry to move it somewhere else on the popup window. The popup window can be resized to fit the contained menu entries. Left click a menu entry to select the menu. The callback function will be called to execute the menu function. If "close" was checked, the popup window will close. All menu settings are saved in the supplied configuration file whenever the popup window is closed, if any changes were made since it was opened. Icon files are copied into the same directory as the configuration file and these copies are used. The icon selected when the menu entry is created can disappear without consequence. If the [x] window kill button is pressed, the window is closed and the calling program is informed by passing "quit" to the callback function. layout of menu configuration file: popup xpos ypos width height popup window position and size posn xpos ypos ww hh menu position in popup window menu menu text menu text on popup window func funcname argument for user callback function icon /.../file.png optional menu icon file size NN optional icon size (default 24) kill optional flag, kill window when used menu menu text next menu entry ... ***************************************************************************/ namespace gmenuznames { #define maxME 200 // max. menu entries #define maxText 1000 // max. text size, all menu fields #define menuFont "sans 9" // menu font #define iconSize 24 // menu icon size (default) typedef void callbackfunc(cchar *menu); // user callback function callbackfunc *gmenuzcallback; char *menuconfigfile = 0; // configuration file from user GtkWidget *mWin, *layout; // popup and drawing windows GtkWidget *pWin; // parent window int winposx=100, winposy=100, winww=400, winhh=300; // initial popup WRT parent window struct menuent { // menu entry on popup window int xpos, ypos, ww, hh; // layout position, extent char *menu; // text on window on null char *func; // func name for user callback char *icon; // icon file or null GdkPixbuf *pixbuf; // icon pixbuf or null int size; // icon size or zero int kill; // kill popup window when menu used }; menuent menus[200]; // menu entries int NME = 0; // entry count zdialog *zdedit = 0; // active edit dialog int mpx, mpy; // mouse click/drag position int me; // current menu entry int Fchanged = 0; // flag, menu edited or resized int Fquit = 0; // popup is being closed int Fbusy = 0; // popup is active void wpaint(GtkWidget *, cairo_t *); // window repaint - draw event void resize(); // window resize event void quit(); // kill window and exit void update_configfile(); // update menu configuration file void mouse_event(GtkWidget *, GdkEventButton *, void *); // mouse event function void KB_event(GtkWidget *, GdkEventKey *, void *); // KB event function void draw_text(cairo_t *, char *, int px, int py, int &ww, int &hh); // draw text and return pixel extent void edit_menu(); // dialog to create/edit menu entry int edit_menu_event(zdialog *zd, cchar *event); // dialog event function } // user callable function to build the menu from user's configuration file void gmenuz(GtkWidget *parent, cchar *title, cchar *ufile, gmenuznames::callbackfunc ufunc) { using namespace gmenuznames; FILE *fid; int nn, xx, yy, ww, hh, size; int pposx, pposy; int xpos, ypos; char *pp, buff[maxText]; GdkPixbuf *pixbuf; GError *gerror; if (Fbusy) return; // don't allow multiple popups pWin = parent; // get parent window if (menuconfigfile) free(menuconfigfile); menuconfigfile = strdup(ufile); // get menu configuration file gmenuzcallback = ufunc; // get user callback function NME = 0; fid = fopen(menuconfigfile,"r"); // read window geometry if (fid) { nn = fscanf(fid," popup %d %d %d %d",&xx,&yy,&ww,&hh); // get popup window position and size if (nn == 4 && ww > 50 && ww < 1000 && hh > 50 && hh < 1000) { winposx = xx; // OK to use winposy = yy; winww = ww; winhh = hh; } while (true) { pp = fgets_trim(buff,maxText-1,fid,1); // read next menu entry if (! pp) break; if (strnEqu(pp,"posn ",5)) { // position in popup window if (NME == maxME) { zmessageACK(mWin,0,"exceeded %d menu entries",maxME); break; } me = NME; // new entry NME++; // entry count memset(&menus[me],0,sizeof(menuent)); // clear all menu data nn = sscanf(pp+5," %d %d ",&xpos,&ypos); // position in popup window if (nn != 2) xpos = ypos = 100; if (xpos > 1000) xpos = 1000; if (ypos > 1000) ypos = 1000; menus[me].xpos = xpos; menus[me].ypos = ypos; } if (strnEqu(pp,"menu ",5)) { // menu text if (strlen(pp+5) > 0) menus[me].menu = strdup(pp+5); // get menu text else menus[me].menu = 0; } if (strnEqu(pp,"func ",5)) { // function name if (strlen(pp+5)) menus[me].func = strdup(pp+5); else menus[me].func = 0; } if (strnEqu(pp,"icon ",5)) { // menu icon file if (strlen(pp+5)) { menus[me].icon = strdup(pp+5); gerror = 0; pixbuf = gdk_pixbuf_new_from_file(pp+5,&gerror); if (! pixbuf && gerror) printz("%s \n",gerror->message); menus[me].pixbuf = pixbuf; } else menus[me].pixbuf = 0; } if (strnEqu(pp,"size ",5)) { size = atoi(pp+5); if (size < 24) size = 24; if (size > 64) size = 64; menus[me].size = size; } if (strnEqu(pp,"kill",4)) // kill window flag menus[me].kill = 1; } fclose(fid); } Fchanged = 0; // no changes yet Fquit = 0; // not being closed Fbusy = 1; mWin = gtk_window_new(GTK_WINDOW_TOPLEVEL); // create popup window for menu entries if (! pWin) pposx = pposy = 0; // no parent window else { gtk_window_get_position(GTK_WINDOW(pWin),&pposx,&pposy); // parent window position (NW corner) gtk_window_set_transient_for(GTK_WINDOW(mWin),GTK_WINDOW(pWin)); // popup window belongs to parent } if (title) gtk_window_set_title(GTK_WINDOW(mWin),title); // v.5.8 winposx += pposx; // popup position relative to parent winposy += pposy; gtk_window_set_default_size(GTK_WINDOW(mWin),winww,winhh); // set size and position gtk_window_move(GTK_WINDOW(mWin),winposx,winposy); layout = gtk_layout_new(0,0); // create drawing window gtk_container_add(GTK_CONTAINER(mWin),layout); // add to popup window G_SIGNAL(mWin,"destroy",quit,0); // connect signals to windows G_SIGNAL(mWin,"delete_event",quit,0); G_SIGNAL(mWin,"notify",resize,0); G_SIGNAL(mWin,"key-release-event",KB_event,0); // connect KB key release event G_SIGNAL(layout,"draw",wpaint,0); gtk_widget_add_events(layout,GDK_BUTTON_PRESS_MASK); // connect mouse events gtk_widget_add_events(layout,GDK_BUTTON_RELEASE_MASK); gtk_widget_add_events(layout,GDK_BUTTON_MOTION_MASK); gtk_widget_add_events(layout,GDK_POINTER_MOTION_MASK); G_SIGNAL(layout,"button-press-event",mouse_event,0); G_SIGNAL(layout,"button-release-event",mouse_event,0); G_SIGNAL(layout,"motion-notify-event",mouse_event,0); gtk_widget_show_all(mWin); // show all widgets return; } // paint window when created, exposed, resized void gmenuznames::wpaint(GtkWidget *, cairo_t *cr) { using namespace gmenuznames; GdkPixbuf *pixbuf; char *text, *text2; int xpos, ypos, ww, hh, size, yadd; for (int me = 0; me < NME; me++) // loop all menu entries { xpos = menus[me].xpos; // window position ypos = menus[me].ypos; text = menus[me].menu; // menu text pixbuf = menus[me].pixbuf; // icon size = menus[me].size; // size if (pixbuf) { // draw icon at window position gdk_cairo_set_source_pixbuf(cr,pixbuf,xpos,ypos); cairo_paint(cr); if (! size) size = iconSize; // use default if not specified } else size = 0; yadd = 0; if (pixbuf) yadd = size + 2; // extra space under icon if (text) { text2 = (char *) malloc(strlen(text)+2); // replace "\n" with newline repl_1str(text,text2,"\\n","\n"); draw_text(cr,text2,xpos,ypos+yadd,ww,hh); // returns size of text written free(text2); } else ww = hh = 0; if (ww < size) ww = size; // menu entry enclosing rectangle hh += yadd; menus[me].ww = ww; menus[me].hh = hh; } return; } // resize event - save current window size void gmenuznames::resize() { using namespace gmenuznames; int xx, yy, ww, hh; if (Fquit) return; // ignore bogus call when killed gtk_window_get_position(GTK_WINDOW(mWin),&xx,&yy); gtk_window_get_size(GTK_WINDOW(mWin),&ww,&hh); if (xx == winposx && yy == winposy && ww == winww && hh == winhh) return; // no change winposx = xx; winposy = yy; winww = ww; winhh = hh; Fchanged = 1; // mark window changed return; } // [x] kill window // Save current menu status for next session. void gmenuznames::quit() { using namespace gmenuznames; Fquit = 1; Fbusy = 0; if (Fchanged) update_configfile(); gtk_widget_destroy(mWin); gmenuzcallback("quit"); // inform host program v.5.4 return; } // menu changed, save all menu data to menu config. file void gmenuznames::update_configfile() { using namespace gmenuznames; char *pp, *pxbfile; int pposx, pposy; FILE *fid; GError *gerror; if (pWin) gtk_window_get_position(GTK_WINDOW(pWin),&pposx,&pposy); // parent window position (may have moved) else pposx = pposy = 0; winposx -= pposx; // popup position relative to parent winposy -= pposy; fid = fopen(menuconfigfile,"w"); // open for write if (! fid) { zmessageACK(mWin,0," %s \n %s",menuconfigfile,strerror(errno)); // diagnose permissions error v.5.7 return; } fprintf(fid,"popup %d %d %d %d \n",winposx,winposy,winww,winhh); for (me = 0; me < NME; me++) // write all menu entries to file { if (! menus[me].menu && ! menus[me].pixbuf) { // no text and no icon printz("gmenuz: skip empty menu entry \n"); continue; } fprintf(fid,"\n"); // blank line separator fprintf(fid,"posn %d %d \n",menus[me].xpos, menus[me].ypos); // menu position in window if (menus[me].menu) // menu text fprintf(fid,"menu %s \n",menus[me].menu); if (menus[me].func) // menu function (text) fprintf(fid,"func %s \n",menus[me].func); if (menus[me].pixbuf) { // pixbuf image for menu icon pxbfile = strdupz(menuconfigfile,20); pp = pxbfile + strlen(pxbfile); // create a local PNG file for pixbuf snprintf(pp,20,"-pixbuf-%03d.png",me); gerror = 0; gdk_pixbuf_save(menus[me].pixbuf,pxbfile,"png",&gerror,null); // write pixbuf to file if (gerror) printz("%s \n %s \n",menus[me].menu,gerror->message); else fprintf(fid,"icon %s \n",pxbfile); // pixbuf file name in menu file free(pxbfile); } if (menus[me].size) // icon size fprintf(fid,"size %d \n",menus[me].size); if (menus[me].kill) fprintf(fid,"kill \n"); // kill window flag } fclose(fid); Fchanged = 0; return; } // mouse event function - capture buttons and drag movements void gmenuznames::mouse_event(GtkWidget *, GdkEventButton *event, void *) { using namespace gmenuznames; static int bdtime = 0, butime = 0; static int Lmouse = 0, Rmouse = 0, Fdrag = 0; static int elapsed, mpx0 = 0, mpy0 = 0; int Fclick, dx, dy, xpos, ypos; mpx = int(event->x); // mouse position in window mpy = int(event->y); if (event->type == GDK_BUTTON_PRESS) { Lmouse = Rmouse = Fdrag = 0; if (event->button == 1) Lmouse++; // left or right mouse button if (event->button == 3) Rmouse++; bdtime = event->time; for (me = 0; me < NME; me++) // look for clicked menu entry { if (mpx < menus[me].xpos) continue; if (mpy < menus[me].ypos) continue; if (mpx > menus[me].xpos + menus[me].ww) continue; if (mpy > menus[me].ypos + menus[me].hh) continue; break; } if (me < NME) { // menu item clicked (selected) mpx0 = mpx; // set new drag origin mpy0 = mpy; } else me = -1; // indicate empty space clicked } if (event->type == GDK_BUTTON_RELEASE) { Fclick = 0; butime = event->time; elapsed = butime - bdtime; // button down time, milliseconds if (elapsed < 500 && ! Fdrag) Fclick = 1; // mouse clicked if (me >= 0 && Fclick && Lmouse) { // menu entry was left-clicked if (menus[me].func) gmenuzcallback(menus[me].func); // caller user function(func) if (menus[me].kill) quit(); // close menu after app launch } else if (Fclick && Rmouse) // menu entry or empty space right-clicked edit_menu(); // edit menu else if (me >= 0 && Fdrag) { // menu entry drag ended xpos = menus[me].xpos; // align to 16-pixel raster ypos = menus[me].ypos; xpos = 16 * ((xpos + 8) / 16) + 4; // min. 4 pixels from edge ypos = 16 * ((ypos + 8) / 16) + 4; menus[me].xpos = xpos; menus[me].ypos = ypos; gtk_widget_queue_draw(layout); // repaint window Fdrag = 0; Fchanged = 1; // mark menu revised } Lmouse = Rmouse = 0; // mouse click action completed } if (event->type == GDK_MOTION_NOTIFY) // mouse movement { if (me >= 0 && Lmouse) { // menu drag underway dx = mpx - mpx0; dy = mpy - mpy0; if (Fdrag || (abs(dx) + abs(dy) > 15)) { // ignore small drags v.5.5 Fdrag++; mpx0 = mpx; // set new drag origin mpy0 = mpy; menus[me].xpos += dx; // add motion to image position menus[me].ypos += dy; gtk_widget_queue_draw(layout); // repaint window } } } return; } // KB release event function - send KB F1 key to main app void gmenuznames::KB_event(GtkWidget *, GdkEventKey *kbevent, void *) { int KBkey = kbevent->keyval; if (KBkey == GDK_KEY_F1) KBstate(kbevent,0); // v.5.6 return; } // draw text into layout and return pixel dimensions of enclosing rectangle void gmenuznames::draw_text(cairo_t *cr, char *text, int x, int y, int &w, int &h) { using namespace gmenuznames; static PangoFontDescription *pfont = 0; static PangoLayout *playout = 0; if (! pfont) { pfont = pango_font_description_from_string(menuFont); // first call, get font sizing poop playout = gtk_widget_create_pango_layout(layout,0); pango_layout_set_font_description(playout,pfont); } pango_layout_set_text(playout,text,-1); // compute layout pango_layout_get_pixel_size(playout,&w,&h); // pixel width and height of layout cairo_move_to(cr,x,y); // draw layout with text cairo_set_source_rgb(cr,0,0,0); pango_cairo_show_layout(cr,playout); return; } // dialog to create a new menu entry from user inputs void gmenuznames::edit_menu() { using namespace gmenuznames; if (me < 0) { // new menu entry if (NME == maxME) { zmessageACK(mWin,0,"capacity limit exceeded"); return; } me = NME; memset(&menus[me],0,sizeof(menuent)); // clear all data } if (! zdedit) // create dialog if not already { zdedit = zdialog_new("edit menu entry",mWin,"apply","delete","cancel",null); zdialog_add_widget(zdedit,"hbox","hb1","dialog",0,"space=3"); zdialog_add_widget(zdedit,"vbox","vb1","hb1",0,"homog"); zdialog_add_widget(zdedit,"vbox","vb2","hb1",0,"homog|expand"); // menu text [_______________] zdialog_add_widget(zdedit,"label","lab11","vb1","menu text"); // menu func [_______________] zdialog_add_widget(zdedit,"label","lab12","vb1","menu func"); // menu icon [_______________] [browse] zdialog_add_widget(zdedit,"label","lab13","vb1","menu icon"); // icon size [___|+-] close window [x] zdialog_add_widget(zdedit,"label","lab14","vb1","icon size"); // zdialog_add_widget(zdedit,"entry","text","vb2",0,"scc=40"); // [apply] [delete] [cancel] zdialog_add_widget(zdedit,"entry","func","vb2",0,"scc=40"); zdialog_add_widget(zdedit,"hbox","hb2","vb2",0,"expand"); zdialog_add_widget(zdedit,"entry","icon","hb2",0,"expand"); zdialog_add_widget(zdedit,"button","browse","hb2","browse","space=5"); zdialog_add_widget(zdedit,"hbox","hb3","vb2"); zdialog_add_widget(zdedit,"spin","size","hb3","24|64|1|24"); zdialog_add_widget(zdedit,"check","kill","hb3","close window","space=30"); zdialog_run(zdedit,edit_menu_event); } if (menus[me].menu) // stuff menu text into dialog zdialog_stuff(zdedit,"text",menus[me].menu); else zdialog_stuff(zdedit,"text",""); if (menus[me].func) // stuff menu function zdialog_stuff(zdedit,"func",menus[me].func); else zdialog_stuff(zdedit,"func",""); if (menus[me].icon) // stuff icon file zdialog_stuff(zdedit,"icon",menus[me].icon); else zdialog_stuff(zdedit,"icon",""); if (menus[me].size) // stuff icon size zdialog_stuff(zdedit,"size",menus[me].size); if (menus[me].kill) // stuff window kill flag zdialog_stuff(zdedit,"kill",1); else zdialog_stuff(zdedit,"kill",0); if (me == NME) { // new menu entry menus[me].xpos = mpx; // initial position from mouse menus[me].ypos = mpy; } return; } // menu entry dialog event function int gmenuznames::edit_menu_event(zdialog *zd, cchar *event) { using namespace gmenuznames; char text[maxText]; char *iconfile; int size; GdkPixbuf *pixbuf; GError *gerror; if (strEqu(event,"browse")) { // browse for icon file iconfile = zgetfile("select icon","file",0); if (iconfile) zdialog_stuff(zd,"icon",iconfile); } if (zd->zstat) // dialog complete { if (zd->zstat == 2) { // [delete] - delete menu entry if (me < 0 || me >= NME) return 0; for (int me2 = me; me2 < NME-1; me2++) // remove menu entry and close hole menus[me2] = menus[me2+1]; NME--; Fchanged = 1; // mark menu revised gtk_widget_queue_draw(layout); // repaint window } if (zd->zstat != 1) { // not [apply] - kill dialog zdialog_free(zdedit); return 0; } // [apply] - update menu from dialog data zdialog_fetch(zd,"text",text,maxText); if (*text) menus[me].menu = strdup(text); // menu text, optional else menus[me].menu = 0; zdialog_fetch(zd,"func",text,maxText); // menu function name strTrim2(text); if (*text) menus[me].func = strdup(text); else menus[me].func = 0; zdialog_fetch(zd,"icon",text,maxText); // menu icon file, optional strTrim2(text); if (*text) { zdialog_fetch(zd,"size",size); // icon size gerror = 0; pixbuf = gdk_pixbuf_new_from_file_at_size(text,size,size,&gerror); if (! pixbuf) { if (gerror) zmessageACK(mWin,0,gerror->message); // bad icon file zd->zstat = 0; // keep dialog open return 0; // do nothing } menus[me].icon = strdup(text); menus[me].pixbuf = pixbuf; menus[me].size = size; } else { menus[me].icon = 0; menus[me].pixbuf = 0; menus[me].size = 0; } zdialog_fetch(zd,"kill",menus[me].kill); // popup window kill flag if (me == NME) NME++; // if new menu entry, incr. count Fchanged = 1; // mark menu revised zdialog_free(zdedit); // destroy dialog gtk_widget_queue_draw(layout); // repaint window } return 0; } /************************************************************************** Vertical Menu / Toolbar Build a custom vertical menu and/or toolbar in a vertical packing box vbm = Vmenu_new(GtkWidget *vbox) create base menu Vmenu_add(vbm, name, icon, desc, func, arg) add menu item or toolbar button Vmenu *vbm cchar *name, *icon, *desc, *arg void func(GtkWidget *, cchar *name) Create a vertical menu / toolbar in a vertical packing box. Added items can have a menu name, icon, description, response function, and function argument. 'name' and 'icon' can be null but not both. 'icon' is a filespec for a .png file containing the icon. 'desc' is optional and is used as a tool tip if the mouse is hovered over the displayed 'name/icon'. When 'name/icon' is clicked, 'func' is called with 'arg'. If 'arg' is null, 'name' is used instead. To create a menu entry that is a popup menu with multiple entries, do as follows: popup = create_popmenu(); add_popup_menu_item(popup ...); // see create_popmenu() add_popup_menu_item(popup ...); ... Vmenu_add(vbm, name, icon, desc, create_popmenu, (cchar *) popup); i.e. use create_popmenu() as the response function and use the previously created menu 'popup' as the argument (cast to cchar *). ***/ namespace Vmenunames { #define menufont1 "sans 10" // menu font #define menufont2 "sans bold 10" // menu font (selected) #define fontheight 18 // menu text height in layout #define margin 5 // margins for menu text #define BGCOLOR 59000,59000,59000 // = cairo 0.9,0.9,0.9 GdkRGBA GDKgray; PangoFontDescription *pfont1, *pfont2; PangoAttrList *pattrlist; PangoAttribute *pbackground; void wpaint(GtkWidget *, cairo_t *, Vmenu *); // window repaint - draw event void mouse_event(GtkWidget *, GdkEventButton *, Vmenu *); // mouse event function void paint_menu(cairo_t *cr, Vmenu *vbm, int me, int hilite); // paint menu entry, opt. highlight } Vmenu *Vmenu_new(GtkWidget *vbox) // revised v.5.7 { using namespace Vmenunames; int cc; cc = sizeof(Vmenu); Vmenu *vbm = (Vmenu *) malloc(cc); memset(vbm,0,cc); vbm->vbox = vbox; vbm->layout = gtk_layout_new(0,0); vbm->mcount = 0; gtk_box_pack_start(GTK_BOX(vbox),vbm->layout,1,1,0); vbm->xmax = vbm->ymax = 10; // layout size v.5.8 GDKgray.red = GDKgray.green = GDKgray.blue = 0.6; gtk_widget_override_background_color(vbm->layout, (GtkStateFlags) 0, &GDKgray); pattrlist = pango_attr_list_new(); pbackground = pango_attr_background_new(BGCOLOR); pango_attr_list_change(pattrlist,pbackground); pfont1 = pango_font_description_from_string(menufont1); pfont2 = pango_font_description_from_string(menufont2); gtk_widget_add_events(vbm->layout,GDK_BUTTON_PRESS_MASK); gtk_widget_add_events(vbm->layout,GDK_BUTTON_RELEASE_MASK); gtk_widget_add_events(vbm->layout,GDK_POINTER_MOTION_MASK); gtk_widget_add_events(vbm->layout,GDK_LEAVE_NOTIFY_MASK); G_SIGNAL(vbm->layout,"button-press-event",mouse_event,vbm); G_SIGNAL(vbm->layout,"button-release-event",mouse_event,vbm); G_SIGNAL(vbm->layout,"motion-notify-event",mouse_event,vbm); G_SIGNAL(vbm->layout,"leave-notify-event",mouse_event,vbm); G_SIGNAL(vbm->layout,"draw",wpaint,vbm); return vbm; } void Vmenu_add(Vmenu *vbm, cchar *name, cchar *icon, int iconww, int iconhh, cchar *desc, cbFunc func, cchar *arg) { using namespace Vmenunames; int me, cc1, cc2, xpos, ww, hh; char iconpath[200], *mdesc, *name__; cchar *blanks = " "; // 20 blanks GdkPixbuf *pixbuf; GError *gerror = 0; PangoLayout *playout; PangoFontDescription *pfont; if (! name && ! icon) return; me = vbm->mcount++; // track no. menu entries if (name) vbm->menu[me].name = strdup(name); // create new menu entry from caller data if (icon) { vbm->menu[me].icon = strdup(icon); vbm->menu[me].iconww = iconww; // v.5.8 vbm->menu[me].iconhh = iconhh; } if (desc) { // pad description with blanks for looks cc1 = strlen(desc); // v.5.6 mdesc = (char *) malloc(cc1+3); mdesc[0] = ' '; strcpy(mdesc+1,desc); strcpy(mdesc+cc1+1," "); vbm->menu[me].desc = mdesc; } vbm->menu[me].func = func; vbm->menu[me].arg = name; // argument is menu name or arg if avail. if (arg) vbm->menu[me].arg = arg; vbm->menu[me].pixbuf = 0; if (icon) { // if icon is named, get pixbuf *iconpath = 0; strncatv(iconpath,199,zfuncs::zicondir,"/",icon,null); pixbuf = gdk_pixbuf_new_from_file_at_scale(iconpath,iconww,iconhh,1,&gerror); if (pixbuf) vbm->menu[me].pixbuf = pixbuf; } if (me == 0) vbm->ymax = margin; // first menu, top position vbm->menu[me].iconx = 0; vbm->menu[me].icony = 0; vbm->menu[me].namex = 0; vbm->menu[me].namey = 0; if (icon) { vbm->menu[me].iconx = margin; // ______ vbm->menu[me].icony = vbm->ymax; // | | if (name) { // | icon | menu name vbm->menu[me].namex = margin + iconww + margin; // |______| vbm->menu[me].namey = vbm->ymax + (iconhh - fontheight) / 2 + 2; } vbm->menu[me].ylo = vbm->ymax; vbm->ymax += iconhh + iconhh / 8; // position for next menu entry vbm->menu[me].yhi = vbm->ymax; if (margin + iconww > vbm->xmax) vbm->xmax = margin + iconww; // keep track of max. layout width } else if (name) { vbm->menu[me].namex = margin; // menu name vbm->menu[me].namey = vbm->ymax; vbm->menu[me].ylo = vbm->ymax; vbm->ymax += fontheight; vbm->menu[me].yhi = vbm->ymax; } vbm->menu[me].playout1 = gtk_widget_create_pango_layout(vbm->layout,0); vbm->menu[me].playout2 = gtk_widget_create_pango_layout(vbm->layout,0); if (name) { xpos = vbm->menu[me].namex; cc1 = strlen(name); // prepare menu name with trailing blanks cc2 = 0.3 * cc1 + 3; // so normal name longer than bold name if (cc2 > 20) cc2 = 20; // otherwise bold not 100% overwritten name__ = strdupz(name,cc2); strncpy0(name__+cc1,blanks,cc2); playout = vbm->menu[me].playout1; // normal font pfont = pfont1; pango_layout_set_attributes(playout,pattrlist); pango_layout_set_font_description(playout,pfont); pango_layout_set_text(playout,name__,-1); // compute layout pango_layout_get_pixel_size(playout,&ww,&hh); // pixel width and height of layout playout = vbm->menu[me].playout2; // bold font pfont = pfont2; pango_layout_set_attributes(playout,pattrlist); pango_layout_set_font_description(playout,pfont); pango_layout_set_text(playout,name,-1); // compute layout pango_layout_get_pixel_size(playout,&ww,&hh); // pixel width and height of layout if (xpos + ww > vbm->xmax) vbm->xmax = xpos + ww; // keep track of max. layout width } gtk_widget_set_size_request(vbm->layout,vbm->xmax+margin,0); // add right margin to layout width return; } // paint window when created, exposed, resized void Vmenunames::wpaint(GtkWidget *widget, cairo_t *cr, Vmenu *vbm) { using namespace Vmenunames; cairo_set_source_rgb(cr,0.9,0.9,0.9); // white background cairo_paint(cr); for (int me = 0; me < vbm->mcount; me++) // paint all menu entries paint_menu(cr,vbm,me,0); return; } // draw menu text into layout void Vmenunames::paint_menu(cairo_t *cr, Vmenu *vbm, int me, int hilite) { using namespace Vmenunames; GdkPixbuf *pixbuf; PangoLayout *playout; int xpos, ypos; int iconww, iconhh; cchar *name; pixbuf = vbm->menu[me].pixbuf; // icon if (pixbuf) { // draw menu icon at menu position xpos = vbm->menu[me].iconx; ypos = vbm->menu[me].icony; iconww = vbm->menu[me].iconww; iconhh = vbm->menu[me].iconhh; if (! hilite) { // erase box around icon cairo_set_source_rgb(cr,0.9,0.9,0.9); cairo_rectangle(cr,xpos-1,ypos-1,iconww+2,iconhh+2); cairo_fill(cr); } gdk_cairo_set_source_pixbuf(cr,pixbuf,xpos,ypos); // draw icon cairo_paint(cr); if (hilite) { cairo_set_source_rgb(cr,0.5,0.5,0.5); // draw box around icon cairo_set_line_width(cr,1); cairo_set_line_join(cr,CAIRO_LINE_JOIN_ROUND); cairo_rectangle(cr,xpos,ypos,iconww,iconhh); cairo_stroke(cr); } } name = vbm->menu[me].name; // menu text if (name) { // draw menu text at menu position xpos = vbm->menu[me].namex; ypos = vbm->menu[me].namey; cairo_move_to(cr,xpos,ypos); // draw layout with text cairo_set_source_rgb(cr,0,0,0); if (hilite) playout = vbm->menu[me].playout2; else playout = vbm->menu[me].playout1; pango_cairo_show_layout(cr,playout); } return; } // mouse event function - capture buttons and drag movements void Vmenunames::mouse_event(GtkWidget *widget, GdkEventButton *event, Vmenu *vbm) { using namespace Vmenunames; GdkDevice *mouse; GdkWindow *gdkwin; cchar *desc; int me, mpy, ylo, yhi; static int me0 = -1; cairo_t *cr; gdkwin = gtk_layout_get_bin_window(GTK_LAYOUT(widget)); cr = gdk_cairo_create(gdkwin); mpy = int(event->y); // mouse y-position mouse = event->device; if (event->type == GDK_MOTION_NOTIFY) // mouse inside layout { for (me = 0; me < vbm->mcount; me++) { // find menu where mouse is ylo = vbm->menu[me].ylo; yhi = vbm->menu[me].yhi; if (mpy < ylo) continue; if (mpy < yhi) break; } if (me != me0 && me0 >= 0) { // unhilight prior paint_menu(cr,vbm,me0,0); desc = vbm->menu[me0].desc; // remove prior tool tip if (desc) poptext_mouse(0,mouse,0,0,0,0); me0 = -1; } if (me == me0) goto MEreturn; // same as before if (me == vbm->mcount) goto MEreturn; // no new menu match paint_menu(cr,vbm,me,1); // hilight menu entry at mouse desc = vbm->menu[me].desc; // show tool tip if (desc) poptext_mouse(desc,mouse,30,20,0,5); me0 = me; // remember last match goto MEreturn; } if (me0 >= 0) // mouse left layout { paint_menu(cr,vbm,me0,0); // unhilight prior desc = vbm->menu[me0].desc; // remove prior tool tip if (desc) poptext_mouse(0,mouse,0,0,0,0); me0 = -1; } if (event->type == GDK_BUTTON_RELEASE) { if (event->button != 1) goto MEreturn; // refresh menu entries for (me = 0; me < vbm->mcount; me++) { // look for clicked menu entry ylo = vbm->menu[me].ylo; yhi = vbm->menu[me].yhi; if (mpy < ylo) continue; if (mpy < yhi) break; } if (me == vbm->mcount) goto MEreturn; // no menu match paint_menu(cr,vbm,me,0); // unhighlight menu if (vbm->menu[me].func) (vbm->menu[me].func)(widget,vbm->menu[me].arg); // caller function(widget,arg) } MEreturn: cairo_destroy(cr); return; } /************************************************************************** simplified GTK dialog functions create a dialog with response buttons (OK, cancel ...) add widgets to dialog (button, text entry, combo box ...) put data into widgets (text or numeric data) run the dialog, get user inputs (button press, text entry, checkbox selection ...) run caller event function when widgets change from user inputs run caller event function when dialog is completed or canceled get dialog completion status (OK, cancel, destroyed ...) get data from dialog widgets (the user inputs) destroy the dialog and free memory ***************************************************************************/ // private functions for widget events and dialog completion int zdialog_widget_event(GtkWidget *, zdialog *zd); int zdialog_delete_event(GtkWidget *, GdkEvent *, zdialog *zd); // create a new zdialog dialog // The title and parent arguments may be null. // optional arguments: up to zdmaxbutts button labels followed by null // returned dialog status: +N = button N (1 to zdmaxbutts) // <0 = [x] button or other GTK destroy action // completion buttons are also events like other widgets // all dialogs run parallel, use zdialog_wait() if needed // The status returned by zdialog_wait() corresponds to the button // (1-10) used to end the dialog. Status < 0 indicates the [x] button // was used or the dialog was killed by the program itself. zdialog * zdialog_new(cchar *title, GtkWidget *parent, ...) // parent added { using namespace zfuncs; zdialog *zd; GtkWidget *topwin, *hbox, *vbox, *butt, *hsep; cchar *bulab[zdmaxbutts]; int cc, ii, nbu; va_list arglist; TRACE zthreadcrash(); // thread usage not allowed if (! monofont) { // v.5.7 monofont = pango_font_description_from_string("Monospace 9"); widgetfont = pango_font_description_from_string("Sans Bold 9"); } va_start(arglist,parent); for (nbu = 0; nbu < zdmaxbutts; nbu++) // get completion buttons { bulab[nbu] = va_arg(arglist, cchar *); if (! bulab[nbu]) break; } va_end(arglist); if (! title) title = " "; // v.5.0 topwin = gtk_window_new(GTK_WINDOW_TOPLEVEL); // use window, not dialog v.5.5 gtk_window_set_title(GTK_WINDOW(topwin),title); vbox = gtk_box_new(VERTICAL,0); // vertical packing box gtk_container_add(GTK_CONTAINER(topwin),vbox); // add to main window gtk_box_set_spacing(GTK_BOX(vbox),2); cc = sizeof(zdialog); // allocate zdialog zd = (zdialog *) malloc(cc); if (parent) gtk_window_set_transient_for(GTK_WINDOW(topwin),GTK_WINDOW(parent)); if (nbu) // there are some completion buttons { hbox = gtk_box_new(HORIZONTAL,2); // add hbox for buttons at bottom gtk_box_pack_end(GTK_BOX(vbox),hbox,0,0,2); hsep = gtk_separator_new(HORIZONTAL); // add separator line gtk_box_pack_end(GTK_BOX(vbox),hsep,0,0,2); for (ii = nbu-1; ii >= 0; ii--) { // add buttons to hbox butt = gtk_button_new_with_label(bulab[ii]); // reverse order nbu-1...0 gtk_widget_override_font(butt,widgetfont); // smaller font v.5.7 gtk_box_pack_end(GTK_BOX(hbox),butt,0,0,2); G_SIGNAL(butt,"clicked",zdialog_widget_event,zd); // connect to event function zd->compbutt[ii] = butt; // save button widgets } } zd->compbutt[nbu] = 0; // mark EOL v.5.8 GtkWidget *space = gtk_box_new(VERTICAL,0); // make space before 1st widget v.5.7 gtk_box_pack_start(GTK_BOX(vbox),space,0,0,1); zd->parent = parent; // parent window or null zd->sentinel = zdsentinel; // validity sentinel zd->eventCB = 0; // no user event callback function zd->zstat = 0; // no zdialog status zd->disabled = 1; // widget signals disabled zd->saveposn = 0; // position not saved zd->widget[0].type = "dialog"; // set up 1st widget = dialog zd->widget[0].name = "dialog"; zd->widget[0].pname = 0; zd->widget[0].data = strdup(title); zd->widget[0].cblist = 0; zd->widget[0].widget = topwin; zd->widget[1].type = 0; // eof - no contained widgets yet return zd; } // add widget to existing zdialog // // Arguments after parent are optional and default to 0. // zd zdialog *, created with zdialog_new() // type string, one of the above widget types // name string, widget name, used to stuff or fetch widget data // parent string, parent name: "dialog" or a previously added container widget // data string, initial data for widget (label name, entry string, spin value, etc.) // options string, optional args in the form "scc=nn|homog|expand|space=nn|wrap" // scc width for text entry widget, characters // homog for hbox or vbox to make even space allocation for contained widgets // expand widget should expand with dialog box expansion // space extra space between this widget and neighbors, pixels // wrap allow text to wrap at right margin // // data can be a string ("initial widget data") or a number in string form ("12.345") // data for togbutt / check / radio: use "0" or "1" for OFF or ON // data for spin / hscale / vscale: use format "min|max|step|value" (default: 0 | 100 | 1 | 50) // data for colorbutt: use "rrr|ggg|bbb" with values 0-255 for each RGB color. // This format is written to initialize the control, and this format is read back // after the user selects a color. // // Multiple radio buttons with same parent are a group: pressing one turns the others OFF. int zdialog_add_widget ( zdialog *zd, cchar *type, cchar *name, cchar *pname, // mandatory args cchar *data, int scc, int homog, int expand, int space, int wrap) // optional args (default = 0) { using namespace zfuncs; GtkWidget *widget = 0, *pwidget = 0; GtkWidget *entry, *vbox; GtkTextBuffer *editBuff = 0; GdkPixbuf *pixbuf = 0; GdkRGBA gdkrgba; GError *gerror = 0; cchar *pp, *ptype = 0; char vdata[30], iconpath[200]; double min, max, step, val; double f256 = 1.0 / 256.0; int iiw, iip, kk, err; if (! zd) zappcrash("zdialog null pointer"); // detect destroyed dialog if (zd->sentinel != zdsentinel) zappcrash("zdialog invalid"); for (iiw = 1; zd->widget[iiw].type; iiw++); // find next avail. slot if (iiw > zdmaxwidgets-2) zappcrash("too many widgets: %d",iiw); zd->widget[iiw].type = strdup(type); // initz. widget struct zd->widget[iiw].name = strdup(name); // all strings in nonvolatile mem zd->widget[iiw].pname = strdup(pname); zd->widget[iiw].data = 0; zd->widget[iiw].cblist = 0; zd->widget[iiw].scc = scc; zd->widget[iiw].homog = homog; zd->widget[iiw].expand = expand; zd->widget[iiw].space = space; zd->widget[iiw].wrap = wrap; zd->widget[iiw].widget = 0; zd->widget[iiw].stopKB = 0; zd->widget[iiw+1].type = 0; // set new EOF marker if (strcmpv(type,"dialog","hbox","vbox","hsep","vsep","frame","scrwin", "label","link","entry","edit","text","button","togbutt", "check","combo","comboE","radio","spin","hscale", "vscale","colorbutt","image",null) == 0) { printz("zdialog, bad widget type: %s \n",type); return 0; } for (iip = iiw-1; iip >= 0; iip--) // find parent (container) widget if (strEqu(pname,zd->widget[iip].name)) break; if (iip < 0) zappcrash("zdialog, no parent for widget: %s",name); pwidget = zd->widget[iip].widget; // parent widget, type ptype = zd->widget[iip].type; if (strcmpv(ptype,"dialog","hbox","vbox","frame","scrwin",null) == 0) zappcrash("zdialog, bad widget parent type: %s",ptype); if (strEqu(type,"hbox")) widget = gtk_box_new(HORIZONTAL,space); // expandable container boxes v.5.4 if (strEqu(type,"vbox")) widget = gtk_box_new(VERTICAL,space); if (strstr("hbox vbox",type)) gtk_box_set_homogeneous(GTK_BOX(widget),homog); if (strEqu(type,"hsep")) widget = gtk_separator_new(HORIZONTAL); // horiz. & vert. separators if (strEqu(type,"vsep")) widget = gtk_separator_new(VERTICAL); if (strEqu(type,"frame")) { // frame around contained widgets widget = gtk_frame_new(data); gtk_frame_set_shadow_type(GTK_FRAME(widget),GTK_SHADOW_IN); data = 0; // v.5.3 data not further used } if (strEqu(type,"scrwin")) { // scrolled window container widget = gtk_scrolled_window_new(0,0); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(widget), GTK_POLICY_AUTOMATIC,GTK_POLICY_AUTOMATIC); data = 0; // v.5.3 data not further used } if (strEqu(type,"label")) { // label (static text) widget = gtk_label_new(data); data = 0; // v.5.3 data not further used } if (strEqu(type,"link")) { // label is clickable if (strEqu(name,"nolabel")) widget = gtk_link_button_new(data); // link is also label else // v.5.6 widget = gtk_link_button_new_with_label(data,name); // label >> link G_SIGNAL(widget,"clicked",zdialog_widget_event,zd); data = 0; // v.5.3 data not further used } if (strEqu(type,"entry")) { // 1-line text entry widget = gtk_entry_new(); if (data) gtk_entry_set_text(GTK_ENTRY(widget),data); if (scc) gtk_entry_set_width_chars(GTK_ENTRY(widget),scc); gtk_widget_override_font(widget,monofont); G_SIGNAL(widget,"changed",zdialog_widget_event,zd); } if (strEqu(type,"edit")) { // multiline edit box widget = gtk_text_view_new(); editBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); if (data) gtk_text_buffer_set_text(editBuff,data,-1); gtk_text_view_set_editable(GTK_TEXT_VIEW(widget),1); if (wrap) gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(widget),GTK_WRAP_WORD); gtk_widget_override_font(widget,monofont); G_SIGNAL(editBuff,"changed",zdialog_widget_event,zd); // buffer signals, not widget } if (strEqu(type,"text")) { // text output (not editable) widget = gtk_text_view_new(); // added v.5.5 editBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); if (data) gtk_text_buffer_set_text(editBuff,data,-1); gtk_text_view_set_editable(GTK_TEXT_VIEW(widget),0); if (wrap) gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(widget),GTK_WRAP_WORD); gtk_widget_override_font(widget,monofont); } if (strEqu(type,"button")) { // button widget = gtk_button_new_with_label(data); G_SIGNAL(widget,"clicked",zdialog_widget_event,zd); data = 0; // v.5.3 data not further used } if (strEqu(type,"togbutt")) { // toggle button widget = gtk_toggle_button_new_with_label(data); G_SIGNAL(widget,"toggled",zdialog_widget_event,zd); data = "0"; // v.5.3 default data } if (strEqu(type,"check")) { // checkbox if (data) widget = gtk_check_button_new_with_label(data); else widget = gtk_check_button_new(); G_SIGNAL(widget,"toggled",zdialog_widget_event,zd); data = "0"; // v.5.3 default data } if (strEqu(type,"combo")) { // combo box widget = gtk_combo_box_text_new(); zd->widget[iiw].cblist = pvlist_create(zdcbmax); // for drop-down list if (! blank_null(data)) { pvlist_append(zd->widget[iiw].cblist,data); // add data to drop-down list gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(widget),data); gtk_combo_box_set_active(GTK_COMBO_BOX(widget),0); } gtk_widget_override_font(widget,monofont); G_SIGNAL(widget,"changed",zdialog_widget_event,zd); } if (strEqu(type,"comboE")) { // combo box with entry box widget = gtk_combo_box_text_new_with_entry(); zd->widget[iiw].cblist = pvlist_create(zdcbmax); // for drop-down list if (! blank_null(data)) { entry = gtk_bin_get_child(GTK_BIN(widget)); gtk_entry_set_text(GTK_ENTRY(entry),data); // entry = initial data pvlist_append(zd->widget[iiw].cblist,data); // add data to drop-down list gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(widget),data); } gtk_widget_override_font(widget,monofont); G_SIGNAL(widget,"changed",zdialog_widget_event,zd); } if (strEqu(type,"radio")) { // radio button for (kk = iip+1; kk <= iiw; kk++) if (strEqu(zd->widget[kk].pname,pname) && // find first radio button strEqu(zd->widget[kk].type,"radio")) break; // with same container if (kk == iiw) widget = gtk_radio_button_new_with_label(null,data); // this one is first else widget = gtk_radio_button_new_with_label_from_widget // not first, add to group (GTK_RADIO_BUTTON(zd->widget[kk].widget),data); G_SIGNAL(widget,"toggled",zdialog_widget_event,zd); data = "0"; // v.5.3 default data } if (strcmpv(type,"spin","hscale","vscale",null)) { // spin button or sliding scale if (! data) zappcrash("zdialog_add_widget(): data missing"); // v.5.8 pp = strField(data,'|',1); err = convSD(pp,min); pp = strField(data,'|',2); err += convSD(pp,max); pp = strField(data,'|',3); err += convSD(pp,step); pp = strField(data,'|',4); err += convSD(pp,val); if (err) { min = 0; max = 100; step = 1; val = 50; } if (*type == 's') { widget = gtk_spin_button_new_with_range(min,max,step); gtk_spin_button_set_value(GTK_SPIN_BUTTON(widget),val); } if (*type == 'h') { widget = gtk_scale_new_with_range(HORIZONTAL,min,max,step); gtk_range_set_value(GTK_RANGE(widget),val); gtk_scale_set_draw_value(GTK_SCALE(widget),0); } if (*type == 'v') { widget = gtk_scale_new_with_range(VERTICAL,min,max,step); gtk_range_set_value(GTK_RANGE(widget),val); gtk_scale_set_draw_value(GTK_SCALE(widget),0); } G_SIGNAL(widget,"value-changed",zdialog_widget_event,zd); sprintf(vdata,"%g",val); data = vdata; } if (strEqu(type,"colorbutt")) { // color edit button if (! data) data = "0|0|0"; // data format: "nnn|nnn|nnn" = RGB pp = strField(data,'|',1); gdkrgba.red = f256 * atoi(pp); // RGB values are 0-1 v.5.8 pp = strField(data,'|',2); gdkrgba.green = f256 * atoi(pp); pp = strField(data,'|',3); gdkrgba.blue = f256 * atoi(pp); gdkrgba.alpha = 1.0; // v.5.8 widget = gtk_color_button_new_with_rgba(&gdkrgba); G_SIGNAL(widget,"color-set",zdialog_widget_event,zd); } if (strEqu(type,"image")) { // image widget v.5.7 snprintf(iconpath,200,"%s/%s",get_zicondir(),data); data = 0; // data not further used pixbuf = gdk_pixbuf_new_from_file_at_scale(iconpath,24,24,1,&gerror); if (pixbuf) { widget = gtk_image_new_from_pixbuf(pixbuf); g_object_unref(pixbuf); } else widget = gtk_image_new_from_icon_name("missing",GTK_ICON_SIZE_BUTTON); } // all widget types come here if (strstr("label button togbutt check radio spin",type)) // smaller font v.5.7 gtk_widget_override_font(widget,widgetfont); zd->widget[iiw].widget = widget; // set widget in zdialog if (strcmpv(type,"entry","edit","comboE",null) > 0) // for data entry widgets, stop zd->widget[iiw].stopKB = 1; // propagation of KB events if (strEqu(ptype,"hbox") || strEqu(ptype,"vbox")) // add to hbox/vbox gtk_box_pack_start(GTK_BOX(pwidget),widget,expand,expand,space); if (strEqu(ptype,"frame")) // add to frame gtk_container_add(GTK_CONTAINER(pwidget),widget); if (strEqu(ptype,"scrwin")) // add to scroll window gtk_container_add(GTK_CONTAINER(pwidget),widget); if (strEqu(ptype,"dialog")) { // add to dialog box vbox = gtk_bin_get_child(GTK_BIN(pwidget)); // dialog is a gtkwindow v.5.5 gtk_box_pack_start(GTK_BOX(vbox),widget,expand,expand,space); } if (data) zd->widget[iiw].data = strdup(data); // use heap memory return 0; } // add widget to existing zdialog - alternative form (clearer and easier code) // options: "scc=nn | homog | expand | space=nn | wrap" (all optional, any order) int zdialog_add_widget(zdialog *zd, cchar *type, cchar *name, cchar *parent, cchar *data, cchar *options) { int stat, scc = 0, homog = 0, expand = 0, space = 0, wrap = 0, begin = 1; char pname[8]; double pval; while (true) { stat = strParms(begin,options,pname,8,pval); if (stat == -1) break; if (stat == 1) zappcrash("bad zdialog options: %s",options); if (strEqu(pname,"scc")) scc = (int(pval)); else if (strEqu(pname,"homog")) homog = 1; else if (strEqu(pname,"expand")) expand = 1; else if (strEqu(pname,"space")) space = (int(pval)); else if (strEqu(pname,"wrap")) wrap = 1; else zappcrash("bad zdialog options: %s",options); } stat = zdialog_add_widget(zd,type,name,parent,data,scc,homog,expand,space,wrap); return stat; } // get GTK widget from zdialog and widget name GtkWidget * zdialog_widget(zdialog *zd, cchar *name) { if (! zd) return 0; // detect destroyed dialog if (zd->sentinel != zdsentinel) return 0; for (int ii = 0; zd->widget[ii].type; ii++) if (strEqu(zd->widget[ii].name,name)) return zd->widget[ii].widget; return 0; } // set a common group for a set of radio buttons // (GTK, this does not work) int zdialog_set_group(zdialog *zd, cchar *radio1, ...) { va_list arglist; cchar *radio2; GtkWidget *gwidget, *widget; GSList *glist; gwidget = zdialog_widget(zd,radio1); glist = gtk_radio_button_get_group(GTK_RADIO_BUTTON(gwidget)); if (! glist) zappcrash("no radio button group"); va_start(arglist,radio1); while (true) { radio2 = va_arg(arglist,cchar *); if (! radio2) break; widget = zdialog_widget(zd,radio2); gtk_radio_button_set_group(GTK_RADIO_BUTTON(widget),glist); } va_end(arglist); return 0; } // resize dialog to a size greater than initial size // (as determined by the included widgets) int zdialog_resize(zdialog *zd, int width, int height) { if (! zd) zappcrash("zdialog null pointer"); // detect destroyed dialog if (zd->sentinel != zdsentinel) zappcrash("zdialog invalid"); GtkWidget *window = zd->widget[0].widget; gtk_window_set_default_size(GTK_WINDOW(window),width,height); return 1; } // put data into a zdialog widget // private function int zdialog_put_data(zdialog *zd, cchar *name, cchar *data) { GtkWidget *widget, *entry; GtkTextBuffer *textBuff; GdkRGBA gdkrgba; int iiw, nn, kk; cchar *type, *pp; char *wdata; double val, f256 = 1.0 / 256.0; zthreadcrash(); // thread usage not allowed if (! zd || zd->sentinel != zdsentinel) { // detect destroyed dialog printz("*** zdialog_put_data(%s,%s), zdialog invalid \n",name,data); return 0; } if (! name || ! *name) { printz("*** zdialog_put_data(%s), widget name invalid \n",name); return 0; } for (iiw = 1; zd->widget[iiw].type; iiw++) // find widget if (strEqu(zd->widget[iiw].name,name)) break; if (! zd->widget[iiw].type) return 0; // log message removed v.5.8 type = zd->widget[iiw].type; widget = zd->widget[iiw].widget; wdata = zd->widget[iiw].data; if (wdata) free(wdata); // free prior data memory zd->widget[iiw].data = 0; if (data) { wdata = strdup(data); // set new data for widget zd->widget[iiw].data = wdata; if (utf8_check(wdata)) printz("*** zdialog: bad UTF8 encoding %s \n",wdata); } zd->disabled++; // disable for widget stuffing if (strEqu(type,"label")) gtk_label_set_text(GTK_LABEL(widget),data); if (strEqu(type,"link")) gtk_label_set_text(GTK_LABEL(widget),data); if (strEqu(type,"entry")) gtk_entry_set_text(GTK_ENTRY(widget),data); if (strEqu(type,"button")) // change button label gtk_button_set_label(GTK_BUTTON(widget),data); if (strEqu(type,"edit")) { textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); gtk_text_buffer_set_text(textBuff,data,-1); } if (strEqu(type,"text")) { // text output v.5.5 textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); gtk_text_buffer_set_text(textBuff,data,-1); } if (strcmpv(type,"togbutt","check","radio",null)) { if (! data) kk = nn = 0; else kk = convSI(data,nn); if (kk != 0) nn = 0; // data not integer, force zero if (nn <= 0) nn = 0; else nn = 1; gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget),nn); // set gtk widget value } if (strEqu(type,"spin")) { kk = convSD(data,val); if (kk != 0) val = 0.0; gtk_spin_button_set_value(GTK_SPIN_BUTTON(widget),val); } if (strEqu(type,"colorbutt")) { // color button data is nnn|nnn|nnn pp = strField(data,'|',1); if (pp) gdkrgba.red = f256 * atoi(pp); // RGB range is 0-1 v.5.8 pp = strField(data,'|',2); if (pp) gdkrgba.green = f256 * atoi(pp); pp = strField(data,'|',3); if (pp) gdkrgba.blue = f256 * atoi(pp); gdkrgba.alpha = 1.0; gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(widget),&gdkrgba); // v.5.8 } if (strcmpv(type,"hscale","vscale",null)) { kk = convSD(data,val); if (kk != 0) val = 0.0; gtk_range_set_value(GTK_RANGE(widget),val); } if (strEqu(type,"combo")) { if (! blank_null(data)) { kk = pvlist_prepend(zd->widget[iiw].cblist,data,1); // add to drop-down list if (kk == 0) // (only if unique) gtk_combo_box_text_prepend_text(GTK_COMBO_BOX_TEXT(widget),data); kk = pvlist_find(zd->widget[iiw].cblist,data); gtk_combo_box_set_active(GTK_COMBO_BOX(widget),kk); // make the active entry } else gtk_combo_box_set_active(GTK_COMBO_BOX(widget),-1); // make no active entry } if (strEqu(type,"comboE")) { entry = gtk_bin_get_child(GTK_BIN(widget)); if (! blank_null(data)) { kk = pvlist_prepend(zd->widget[iiw].cblist,data,1); // add to drop-down list if (kk == 0) // (only if unique) gtk_combo_box_text_prepend_text(GTK_COMBO_BOX_TEXT(widget),data); gtk_entry_set_text(GTK_ENTRY(entry),data); // stuff entry box with new data } else gtk_entry_set_text(GTK_ENTRY(entry),""); // stuff entry box with nothing } zd->disabled--; // re-enable dialog return iiw; } // get data from a dialog widget based on its name // private function cchar * zdialog_get_data(zdialog *zd, cchar *name) { if (! zd || zd->sentinel != zdsentinel) { printz("zdialog_get_data(): zd invalid \n"); return 0; // detect destroyed dialog } for (int ii = 1; zd->widget[ii].type; ii++) if (strEqu(zd->widget[ii].name,name)) return zd->widget[ii].data; return 0; } // set new limits for a numeric data entry widget (spin, hscale, vscale) int zdialog_set_limits(zdialog *zd, cchar *name, double min, double max) { GtkWidget *widget; cchar *type; int iiw; if (! zd) return 0; // detect destroyed dialog if (zd->sentinel != zdsentinel) return 0; for (iiw = 1; zd->widget[iiw].type; iiw++) if (strEqu(name,zd->widget[iiw].name)) break; if (! zd->widget[iiw].type) { printz("*** zdialog_stuff_limits, %s not found \n",name); return 0; } widget = zd->widget[iiw].widget; type = zd->widget[iiw].type; if (*type == 's') gtk_spin_button_set_range(GTK_SPIN_BUTTON(widget),min,max); if (*type == 'h' || *type == 'v') gtk_range_set_range(GTK_RANGE(widget),min,max); return 1; } // run the dialog and send events to the event function // // evfunc: int func(zdialog *zd, cchar *event) // If present, eventFunc is called when a dialog widget is changed or the dialog // is completed. If a widget was changed, event is the widget name. // Get the new widget data with zdialog_fetch(). // If a completion button was pressed, event is "zstat" and zd->zstat will be // the button number 1-N. // If the dialog was destroyed, event is "zstat" and zd->zstat is negative. // // posn: optional dialog box position: // "mouse" = position at mouse // "desktop" = center on desktop // "parent" = center on parent window // "nn/nn" = position NW corner at relative x/y position in parent window, // where nn/nn is a percent 0-100 of the parent window x/y dimensions. // "save" = save last user-set position and use this whenever the dialog is repeated, // also across sessions. // // KBstate: extern void KBstate(GdkEventKey *event, int state) // This function must be supplied by the caller of zdialog. // It is called when Ctrl|Shift|Alt|F1 is pressed or released. int zdialog_run(zdialog *zd, zdialog_event evfunc, cchar *posn) { int zdialog_KBpress(GtkWidget *, GdkEventKey *event, zdialog *zd); int zdialog_KBrelease(GtkWidget *, GdkEventKey *event, zdialog *zd); int zdialog_focus_event_signal(GtkWidget *, GdkEvent *event, zdialog *zd); int ii; GtkWidget *widget, *dialog, *entry; TRACE if (! zd) zappcrash("zdialog null pointer"); // detect destroyed dialog if (zd->sentinel != zdsentinel) zappcrash("zdialog invalid"); dialog = zd->widget[0].widget; if (posn) zdialog_set_position(zd,posn); // put dialog at remembered position for (ii = 1; zd->widget[ii].type; ii++) // *** stop auto-selection { // (GTK "feature") if (strEqu(zd->widget[ii].type,"entry")) { widget = zd->widget[ii].widget; if (gtk_editable_get_editable(GTK_EDITABLE(widget))) gtk_editable_set_position(GTK_EDITABLE(widget),-1); break; } if (strEqu(zd->widget[ii].type,"comboE")) { // also combo edit box widget = zd->widget[ii].widget; entry = gtk_bin_get_child(GTK_BIN(widget)); gtk_editable_set_position(GTK_EDITABLE(entry),-1); break; } } if (evfunc) zd->eventCB = (void *) evfunc; // link to dialog event callback dialog = zd->widget[0].widget; gtk_widget_show_all(dialog); // activate dialog gtk_window_present(GTK_WINDOW(dialog)); if (! zd->parent) // if no parent, force to top gtk_window_set_keep_above(GTK_WINDOW(dialog),1); G_SIGNAL(dialog,"focus-in-event",zdialog_focus_event_signal,zd); // connect focus event function v.5.0 G_SIGNAL(dialog,"key-press-event",zdialog_KBpress,zd); // connect key press event function G_SIGNAL(dialog,"key-release-event",zdialog_KBrelease,zd); // connect key release event function G_SIGNAL(dialog,"delete-event",zdialog_delete_event,zd); // connect delete event function v.5.5 zd->zstat = 0; // dialog not complete v.5.7 zd->disabled = 0; // enable widget events zfuncs::zdialog_busy++; // count open zdialogs return 0; // return now, dialog is non-modal } // zdialog event handler - called for dialog events. // Updates data in zdialog, calls user callback function (if present). // private function int zdialog_widget_event(GtkWidget *widget, zdialog *zd) { zdialog_event *evfunc = 0; // dialog event callback function GtkTextView *textView = 0; GtkTextBuffer *textBuff = 0; GtkTextIter iter1, iter2; GdkRGBA gdkrgba; GtkWidget *entry; static GtkWidget *lastwidget = 0; int ii, nn; cchar *wname, *type, *wdata, *event; char sdata[20]; double dval; static int cbadded = 0; TRACE if (! zd) return 1; // detect destroyed dialog v.5.0 if (zd->sentinel != zdsentinel) return 1; if (zd->disabled) return 1; // re-entry from zdialog_put_data() zd->disabled = 1; // or user event function v.5.8 for (ii = 0; ii < zdmaxbutts; ii++) { // check completion buttons if (zd->compbutt[ii] == null) break; // EOL v.5.8 if (zd->compbutt[ii] != widget) continue; zd->zstat = ii+1; // zdialog status = button no. event = "zstat"; goto call_evfunc; // call zdialog event function v.5.7 } for (ii = 1; zd->widget[ii].type; ii++) // find widget in zdialog if (zd->widget[ii].widget == widget) goto found_widget; for (ii = 1; zd->widget[ii].type; ii++) { // failed, test if buffer if (strEqu(zd->widget[ii].type,"edit")) { // of text view widget textView = GTK_TEXT_VIEW(zd->widget[ii].widget); textBuff = gtk_text_view_get_buffer(textView); if (widget == (GtkWidget *) textBuff) goto found_widget; } } printz("*** zdialog event ignored \n"); // not found, ignore event zd->disabled = 0; return 1; found_widget: wname = zd->widget[ii].name; type = zd->widget[ii].type; wdata = 0; if (strEqu(type,"button")) wdata = "clicked"; if (strEqu(type,"entry")) wdata = gtk_entry_get_text(GTK_ENTRY(widget)); if (strEqu(type,"edit")) { gtk_text_buffer_get_bounds(textBuff,&iter1,&iter2); wdata = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0); } if (strcmpv(type,"radio","check","togbutt",null)) { nn = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget)); if (nn == 0) wdata = "0"; else wdata = "1"; } if (strEqu(type,"combo")) wdata = gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(widget)); if (strEqu(type,"comboE")) { if (widget == lastwidget && cbadded) { pvlist_remove(zd->widget[ii].cblist,0); // detect multiple edits (keystrokes) gtk_combo_box_text_remove(GTK_COMBO_BOX_TEXT(widget),0); // amd replace with new entry } entry = gtk_bin_get_child(GTK_BIN(widget)); wdata = gtk_entry_get_text(GTK_ENTRY(entry)); cbadded = 0; if (! blank_null(wdata)) { nn = pvlist_prepend(zd->widget[ii].cblist,wdata,1); // add entry to drop-down list if (nn == 0) { // (only if unique) gtk_combo_box_text_prepend_text(GTK_COMBO_BOX_TEXT(widget),wdata); cbadded = 1; } } } if (strEqu(type,"spin")) { dval = gtk_spin_button_get_value(GTK_SPIN_BUTTON(widget)); sprintf(sdata,"%g",dval); wdata = sdata; } if (strEqu(type,"colorbutt")) // color button v.5.8 { gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(widget),&gdkrgba); sprintf(sdata,"%.0f|%.0f|%.0f",gdkrgba.red*255,gdkrgba.green*255,gdkrgba.blue*255); wdata = sdata; } if (strcmpv(type,"hscale","vscale",null)) { dval = gtk_range_get_value(GTK_RANGE(widget)); sprintf(sdata,"%g",dval); wdata = sdata; } // all widgets come here if (zd->widget[ii].data) free(zd->widget[ii].data); // clear prior data zd->widget[ii].data = 0; if (wdata) zd->widget[ii].data = strdup(wdata); // set new data lastwidget = widget; // remember last widget updated event = wname; // event = widget name call_evfunc: // call zdialog event function v.5.7 if (zd->eventCB) { evfunc = (zdialog_event *) zd->eventCB; // do callback function evfunc(zd,event); } if (zd->sentinel != zdsentinel) return 1; // zdialog destroyed v.5.7 zd->stopKB = zd->widget[ii].stopKB; // stop KB propagation if req. v.5.1 zd->disabled = 0; // re-enable widgets return 1; } // zdialog response handler for "focus-in-event" signal // v.5.0 // private function zdialog *zdialog_focus_zd; // v.5.8 current zdialog int zdialog_focus_event_signal(GtkWidget *, GdkEvent *event, zdialog *zd) { if (! zd) return 1; // detect destroyed dialog v.5.7 if (zd->sentinel != zdsentinel) return 1; if (zd->zstat) return 0; // already complete zdialog_focus_zd = zd; zdialog_send_event(zd,"focus"); // notify dialog event function return 0; // must be 0 } // zdialog response handler for keyboard events // key symbols can be found at /usr/include/gtk-3.0/gdk/gdkkeysyms.h // main app must provide: extern void KBstate(GdkEventKey *event, int state) // private function int KB_Ctrl = 0; // track state of KB Ctrl int KB_Shift = 0; // and Shift and Alt keys int KB_Alt = 0; int zdialog_KBpress(GtkWidget *, GdkEventKey *kbevent, zdialog *zd) { KB_Ctrl = KB_Shift = KB_Alt = 0; if (kbevent->state & GDK_CONTROL_MASK) KB_Ctrl = 1; // v.5.6 if (kbevent->state & GDK_SHIFT_MASK) KB_Shift = 1; if (kbevent->state & GDK_MOD1_MASK) KB_Alt = 1; if (KB_Ctrl || KB_Alt) { // if Ctrl or Alt pressed, v.5.6 KBstate(kbevent,1); // send next key to main app return 1; } return 0; // send to dialog widget } int zdialog_KBrelease(GtkWidget *widget, GdkEventKey *kbevent, zdialog *zd) { void zdialog_pastefunc(GtkClipboard *, cchar *, void *); int KBkey = kbevent->keyval; GtkClipboard *clipboard = 0; KB_Ctrl = KB_Shift = KB_Alt = 0; if (kbevent->state & GDK_CONTROL_MASK) KB_Ctrl = 1; // v.5.6 if (kbevent->state & GDK_SHIFT_MASK) KB_Shift = 1; if (kbevent->state & GDK_MOD1_MASK) KB_Alt = 1; if (KB_Ctrl && KBkey == GDK_KEY_v) // Ctrl+v = paste clipboard v.5.8 { clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); gtk_clipboard_request_text(clipboard,zdialog_pastefunc,widget); return 1; } if (KB_Ctrl || KB_Alt) { // if Ctrl or Alt pressed, KBstate(kbevent,0); // send next key to main app v.5.6 return 1; } if (KBkey == GDK_KEY_F1) { // send F1 (help) to main app v.5.6 KBstate(kbevent,0); return 1; } if (KBkey == GDK_KEY_F11) { // F11 (toggle full screen) to main app KBstate(kbevent,0); // v.5.6 return 1; } if (KBkey == GDK_KEY_Escape) { // ESC key is cancel dialog v.5.6 zdialog_destroy(zd); zdialog_send_response(zd,-1); return 1; } if (KBkey == GDK_KEY_Return) { // Return (Enter) key v.5.8 zdialog_send_event(zd,"enter"); return 1; } if (zd->stopKB) return 1; // ignore KB events v.5.1 return 0; // send KB input to dialog widget } // process Ctrl+v (paste text) events for a zdialog widget // v.5.8 // private function void zdialog_pastefunc(GtkClipboard *clipboard, cchar *cliptext, void *arg) { GtkWindow *window = (GtkWindow *) arg; GtkWidget *widget = gtk_window_get_focus(window); GtkTextView *textView = 0; GtkTextBuffer *textBuff = 0; zdialog *zd = zdialog_focus_zd; int ii, cc = 0; cchar *wname; char text[1000]; if (! widget) return; // widget for pasted text if (! cliptext || ! *cliptext) return; // clipboard text pasted if (! zd) return; // check zdialog is active if (zd->sentinel != zdsentinel) return; if (zd->zstat) return; for (ii = 1; zd->widget[ii].type; ii++) // find widget in zdialog if (zd->widget[ii].widget == widget) goto found_widget; for (ii = 1; zd->widget[ii].type; ii++) { // failed, test if buffer if (strEqu(zd->widget[ii].type,"edit")) { // of text view widget textView = GTK_TEXT_VIEW(zd->widget[ii].widget); textBuff = gtk_text_view_get_buffer(textView); if (widget == (GtkWidget *) textBuff) goto found_widget; } } return; // not found found_widget: wname = zd->widget[ii].name; zdialog_fetch(zd,wname,text,999); // current text in widget cc = strlen(text); if (cc > 995) return; strncpy(text+cc,cliptext,999-cc); // add clipboard text text[999] = 0; zdialog_stuff(zd,wname,text); return; } // private function called when zdialog is completed. // called when dialog is canceled via [x] button or destroyed by GTK (zstat < 0). int zdialog_delete_event(GtkWidget *, GdkEvent *, zdialog *zd) { zdialog_event *evfunc = 0; // dialog event callback function if (! zd) return 1; // detect destroyed dialog if (zd->sentinel != zdsentinel) return 1; if (zd->zstat) return 1; // already complete zd->zstat = -1; // set zdialog cancel status v.5.5 if (zd->eventCB) { evfunc = (zdialog_event *) zd->eventCB; // do callback function evfunc(zd,"zstat"); // (should do zdialog_free()) } else zdialog_destroy(zd); // v.5.8 return 0; } // Send the event "name" to an active zdialog. // The response function eventFunc() will be called with this event. int zdialog_send_event(zdialog *zd, cchar *event) { zdialog_event * evfunc = 0; // dialog event callback function if (zd && zd->sentinel == zdsentinel) { // check dialog is active evfunc = (zdialog_event *) zd->eventCB; if (evfunc) evfunc(zd,event); // call dialog event function } return 1; } // Complete an active dialog and assign a status. // Equivalent to the user pressing a dialog completion button. // The dialog completion function is called if defined, and // zdialog_wait() is unblocked. // returns: 0 = no active dialog, 1 = OK int zdialog_send_response(zdialog *zd, int zstat) { zdialog_event *evfunc = 0; // dialog event callback function if (! zd) return 0; // detect destroyed dialog if (zd->sentinel != zdsentinel) return 0; zd->zstat = zstat; // set status if (zd->eventCB) { evfunc = (zdialog_event *) zd->eventCB; // do callback function evfunc(zd,"zstat"); } return 1; } // show or hide a zdialog window // returns 1 if successful, 0 if zd does not exist. int zdialog_show(zdialog *zd, int show) { static GtkWidget *widget, *pwidget = 0; static int posx, posy; widget = zdialog_widget(zd,"dialog"); if (! widget) return 0; if (show) { // show window if (widget == pwidget) { // restore prior position v.5.4 gtk_window_move(GTK_WINDOW(widget),posx,posy); pwidget = 0; } gtk_widget_show_all(widget); // GTK bug?? window can no longer //// } // receive focus events else { // hide window pwidget = widget; gtk_window_get_position(GTK_WINDOW(widget),&posx,&posy); // save position v.5.4 gtk_widget_hide(widget); } return 1; } // Destroy the zdialog - must be done by zdialog_run() caller // (else dialog continues active even after completion button). // Data in widgets remains valid until zdialog_free() is called. int zdialog_destroy(zdialog *zd) { using namespace zfuncs; TRACE if (! zd) return 0; // detect destroyed dialog if (zd->sentinel != zdsentinel) return 0; if (zd->zstat < 0) { // destroyed by [x] button or GTK if (zd->widget[0].widget) zdialog_busy--; // bugfix v.5.8 zd->widget[0].widget = 0; // assume GTK dialog is gone } if (zd->widget[0].widget) { // multiple destroys OK if (zd->saveposn) zdialog_save_position(zd); // save position for next use v.5.6 gtk_widget_destroy(zd->widget[0].widget); // destroy GTK dialog zd->widget[0].widget = 0; zdialog_busy--; } if (! zd->zstat) zd->zstat = -1; // status = destroyed if (zd->parent) gtk_window_present(GTK_WINDOW(zd->parent)); // focus on parent window v.5.7 return 1; } // free zdialog memory (will destroy first, if not already) // zd is set to null int zdialog_free(zdialog *&zd) // reference { TRACE if (! zd || zd->sentinel != zdsentinel) return 0; // v.5.8 zdialog_save_inputs(zd); // save user inputs for next use v.5.3 zdialog_destroy(zd); // destroy GTK dialog if there zd->sentinel = 0; // mark invalid free(zd->widget[0].data); // bugfix memory leak for (int ii = 1; zd->widget[ii].type; ii++) // loop through widgets { if (strcmpv(zd->widget[ii].type,"combo","comboE",null)) // bugfix, free combo list pvlist_free(zd->widget[ii].cblist); free((char *) zd->widget[ii].type); // free strings free((char *) zd->widget[ii].name); free((char *) zd->widget[ii].pname); if (zd->widget[ii].data) free(zd->widget[ii].data); // free data } free(zd); // free zdialog memory zd = 0; // clear pointer return 1; } // Wait for a dialog to complete or be destroyed. This is a zmainloop() loop. // The returned status is the button 1-N used to complete the dialog, or negative // if the dialog was destroyed with [x] or otherwise by GTK. If the status was 1-N and // the dialog will be kept active, set zd->zstat = 0 to restore the active state. int zdialog_wait(zdialog *zd) { while (true) { zmainloop(); if (! zd) return -1; // dialog destroyed if (zd->sentinel != zdsentinel) return -1; if (zd->zstat) return zd->zstat; zsleep(0.01); } } // put cursor at named widget int zdialog_goto(zdialog *zd, cchar *name) { GtkWidget *widget; if (zd->sentinel != zdsentinel) return 0; widget = zdialog_widget(zd, name); if (! widget) return 0; gtk_editable_select_region(GTK_EDITABLE(widget),0,-1); // focus on widget gtk_widget_grab_focus(widget); return 1; } // set cursor for zdialog (e.g. a busy cursor) void zdialog_set_cursor(zdialog *zd, GdkCursor *cursor) { GtkWidget *dialog; GdkWindow *window; if (zd->sentinel != zdsentinel) return; dialog = zd->widget[0].widget; if (! dialog) return; window = gtk_widget_get_window(dialog); gdk_window_set_cursor(window,cursor); return; } // insert data into a zdialog widget int zdialog_stuff(zdialog *zd, cchar *name, cchar *data) // stuff a string { if (data) zdialog_put_data(zd, name, data); else zdialog_put_data(zd, name, ""); // null > "" v.5.2 return 1; } int zdialog_stuff(zdialog *zd, cchar *name, int idata) // stuff an integer { char string[16]; sprintf(string,"%d",idata); zdialog_put_data(zd,name,string); return 1; } int zdialog_stuff(zdialog *zd, cchar *name, double ddata) // stuff a double { char string[32]; snprintf(string,31,"%g",ddata); // outputs decimal point or comma zdialog_put_data(zd,name,string); // (per locale) return 1; } // get data from a zdialog widget int zdialog_fetch(zdialog *zd, cchar *name, char *data, int maxcc) // fetch string data { cchar *zdata; zdata = zdialog_get_data(zd,name); if (! zdata) { *data = 0; return 0; } return strncpy0(data,zdata,maxcc); // 0 = OK, 1 = truncation } int zdialog_fetch(zdialog *zd, cchar *name, int &idata) // fetch an integer { cchar *zdata; zdata = zdialog_get_data(zd,name); if (! zdata) { idata = 0; return 0; } idata = atoi(zdata); return 1; } int zdialog_fetch(zdialog *zd, cchar *name, double &ddata) // fetch a double { int stat; cchar *zdata; zdata = zdialog_get_data(zd,name); if (! zdata) { ddata = 0; return 0; } stat = convSD(zdata,ddata); // period or comma decimal point OK if (stat < 4) return 1; return 0; } int zdialog_fetch(zdialog *zd, cchar *name, float &fdata) // fetch a float { int stat; cchar *zdata; double ddata; zdata = zdialog_get_data(zd,name); if (! zdata) { fdata = 0; return 0; } stat = convSD(zdata,ddata); // period or comma decimal point OK fdata = ddata; if (stat < 4) return 1; return 0; } /************************************************************************** Widget type Combo Box with Entry -------------------------------- stat = zdialog_cb_app(zd, name, data) append to combo box list stat = zdialog_cb_prep(zd, name, data) prepend to combo box list data = zdialog_cb_get(zd, name, Nth) get combo box list item data = zdialog_cb_delete(zd, name, data) delete entry matching "data" data = zdialog_cb_clear(zd, name) clear all entries zdialog_cb_popup(zd, name) open the combo box entries These functions map and track a combo box drop-down list, by maintaining a parallel list in memory. The function zdialog-stuff, when called for a comboE widget, automatically prepends the stuffed data to the drop-down list. The private function zdialog_event(), when processing user input to the edit box of a comboE widget, adds the updated entry to the drop-down list. The drop-down list is kept free of redundant and blank entries. ***************************************************************************/ // append new item to combo box list without changing entry box int zdialog_cb_app(zdialog *zd, cchar *name, cchar *data) { int ii, nn; if (! zd) zappcrash("zdialog null pointer"); // detect destroyed dialog if (zd->sentinel != zdsentinel) zappcrash("zdialog invalid"); if (blank_null(data)) return 0; // find widget for (ii = 1; zd->widget[ii].type; ii++) if (strEqu(zd->widget[ii].name,name)) break; if (! zd->widget[ii].type) return 0; // not found if (! strcmpv(zd->widget[ii].type,"combo","comboE",null)) return 0; // not combo box nn = pvlist_append(zd->widget[ii].cblist,data,1); // append unique if (nn >= 0) gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(zd->widget[ii].widget),data); return 1; } // prepend new item to combo box list without changing entry box int zdialog_cb_prep(zdialog *zd, cchar *name, cchar *data) { int ii, nn; if (! zd) zappcrash("zdialog null pointer"); // detect destroyed dialog if (zd->sentinel != zdsentinel) zappcrash("zdialog invalid"); if (blank_null(data)) return 0; // find widget for (ii = 1; zd->widget[ii].type; ii++) if (strEqu(zd->widget[ii].name,name)) break; if (! zd->widget[ii].type) return 0; // not found if (! strcmpv(zd->widget[ii].type,"combo","comboE",null)) return 0; // not combo box nn = pvlist_prepend(zd->widget[ii].cblist,data,1); // append unique if (nn == 0) gtk_combo_box_text_prepend_text(GTK_COMBO_BOX_TEXT(zd->widget[ii].widget),data); return 1; } // get combo box drop-down list entry // Nth = 0 = first list entry (not comboE edit box) char * zdialog_cb_get(zdialog *zd, cchar *name, int Nth) { int ii; if (! zd) return 0; // detect destroyed dialog if (zd->sentinel != zdsentinel) return 0; for (ii = 1; zd->widget[ii].type; ii++) // find widget if (strEqu(zd->widget[ii].name,name)) break; if (! zd->widget[ii].type) return 0; // not found if (! strcmpv(zd->widget[ii].type,"combo","comboE",null)) return 0; // not combo box return pvlist_get(zd->widget[ii].cblist,Nth); } // delete entry by name from combo drop down list int zdialog_cb_delete(zdialog *zd, cchar *name, cchar *data) { int ii, nn; if (! zd) return 0; // detect destroyed dialog if (zd->sentinel != zdsentinel) return 0; for (ii = 1; zd->widget[ii].type; ii++) // find widget if (strEqu(zd->widget[ii].name,name)) break; if (! zd->widget[ii].type) return 0; // not found if (! strcmpv(zd->widget[ii].type,"combo","comboE",null)) return 0; // not combo box nn = pvlist_find(zd->widget[ii].cblist,data); // find entry by name if (nn < 0) return -1; pvlist_remove(zd->widget[ii].cblist,nn); // remove from memory list gtk_combo_box_text_remove(GTK_COMBO_BOX_TEXT(zd->widget[ii].widget),nn); // and from widget gtk_combo_box_set_active(GTK_COMBO_BOX(zd->widget[ii].widget),-1); // set no active entry return 0; } // delete all entries from combo drop down list int zdialog_cb_clear(zdialog *zd, cchar *name) { int ii, jj, nn; GtkWidget *widget, *entry; if (! zd) return 0; // detect destroyed dialog if (zd->sentinel != zdsentinel) return 0; for (ii = 1; zd->widget[ii].type; ii++) // find widget if (strEqu(zd->widget[ii].name,name)) break; if (! zd->widget[ii].type) return 0; // not found if (! strcmpv(zd->widget[ii].type,"combo","comboE",null)) return 0; // not combo box nn = pvlist_count(zd->widget[ii].cblist); // entry count for (jj = nn-1; jj >= 0; jj--) { pvlist_remove(zd->widget[ii].cblist,jj); // remove from memory list gtk_combo_box_text_remove(GTK_COMBO_BOX_TEXT(zd->widget[ii].widget),jj); // remove from widget } widget = zd->widget[ii].widget; gtk_combo_box_set_active(GTK_COMBO_BOX(widget),-1); // set no active entry if (strEqu(zd->widget[ii].type,"comboE")) { // stuff entry box with nothing entry = gtk_bin_get_child(GTK_BIN(widget)); gtk_entry_set_text(GTK_ENTRY(entry),""); } return 0; } // make a combo box drop down to show all entries int zdialog_cb_popup(zdialog *zd, cchar *name) { int ii; if (! zd) return 0; // detect destroyed dialog if (zd->sentinel != zdsentinel) return 0; for (ii = 1; zd->widget[ii].type; ii++) // find widget if (strEqu(zd->widget[ii].name,name)) break; if (! zd->widget[ii].type) return 0; // not found if (! strcmpv(zd->widget[ii].type,"combo","comboE",null)) return 0; // not combo box gtk_combo_box_popup(GTK_COMBO_BOX(zd->widget[ii].widget)); return 0; } /**************************************************************************/ // functions to save and recall zdialog window positions namespace zdposn_names { struct zdposn_t { float xpos, ypos; // window position WRT parent or desktop char wintitle[64]; // window title (ID) } zdposn[200]; // space for 200 windows int Nzdposn; // no. in use int Nzdpmax = 200; // table size } // Load zdialog positions table from its file (application startup) // or save zdialog positions table to its file (application exit). // Action is "load" or "save". Number of table entries is returned. int zdialog_positions(cchar *action) { using namespace zfuncs; using namespace zdposn_names; char posfile[200], buff[100], wintitle[64], *pp; float xpos, ypos; int nn, ii; FILE *fid; snprintf(posfile,199,"%s/zdialog_positions",zuserdir); // /home//.appname/zdialog_positions if (strEqu(action,"load")) // load dialog positions table from file { fid = fopen(posfile,"r"); if (! fid) { Nzdposn = 0; return 0; } for (nn = 0; nn < Nzdpmax; nn++) { pp = fgets(buff,100,fid); if (! pp) break; if (strlen(pp) < 64) continue; strncpy0(wintitle,buff,64); strTrim(wintitle); if (strlen(wintitle) < 3) continue; ii = sscanf(buff + 64," %f %f ",&xpos,&ypos); if (ii != 2) continue; strcpy(zdposn[nn].wintitle,wintitle); zdposn[nn].xpos = xpos; zdposn[nn].ypos = ypos; } fclose(fid); Nzdposn = nn; return Nzdposn; } if (strEqu(action,"save")) // save dialog positions table to file { fid = fopen(posfile,"w"); if (! fid) { printz("*** cannot write zdialog_positions file \n"); return 0; } for (nn = 0; nn < Nzdposn; nn++) fprintf(fid,"%-64s %0.1f %0.1f \n",zdposn[nn].wintitle, zdposn[nn].xpos, zdposn[nn].ypos); fclose(fid); return Nzdposn; } printz("*** zdialog_positions bad action: %s \n",action); return 0; } // Set the initial or new zdialog window position from "posn". // Called by zdialog_run(). // null: window manager decides // "mouse" put dialog at mouse position // "desktop" center dialog in desktop window // "parent" center dialog in parent window // "save" use the same position last set by the user // "nn/nn" put NW corner of dialog in parent window at % size // (e.g. "50/50" puts NW corner at center of parent) void zdialog_set_position(zdialog *zd, cchar *posn) { using namespace zdposn_names; int ii, ppx, ppy, zdpx, zdpy, pww, phh; float xpos, ypos; char wintitle[64], *pp; GtkWidget *parent, *dialog; parent = zd->parent; dialog = zd->widget[0].widget; if (strEqu(posn,"mouse")) { gtk_window_set_position(GTK_WINDOW(dialog),GTK_WIN_POS_MOUSE); return; } if (strEqu(posn,"desktop")) { gtk_window_set_position(GTK_WINDOW(dialog),GTK_WIN_POS_CENTER); return; } if (strEqu(posn,"parent")) { gtk_window_set_position(GTK_WINDOW(dialog),GTK_WIN_POS_CENTER_ON_PARENT); return; } if (! parent) { // no parent window ppx = ppy = 0; // use desktop pww = gdk_screen_width(); phh = gdk_screen_height(); } else { gtk_window_get_position(GTK_WINDOW(parent),&ppx,&ppy); // parent window NW corner gtk_window_get_size(GTK_WINDOW(parent),&pww,&phh); // parent window size } if (strEqu(posn,"save")) // use last saved window position { zd->saveposn = 1; // set flag for zdialog_free() pp = (char *) gtk_window_get_title(GTK_WINDOW(dialog)); // get window title, used as ID if (! pp) return; if (strlen(pp) < 2) return; strncpy0(wintitle,pp,64); // window title, < 64 chars. for (ii = 0; ii < Nzdposn; ii++) // search table for title if (strEqu(wintitle,zdposn[ii].wintitle)) break; if (ii == Nzdposn) return; // not found - zdialog_free() will add zdpx = ppx + 0.01 * zdposn[ii].xpos * pww; // position for dialog window zdpy = ppy + 0.01 * zdposn[ii].ypos * phh; gtk_window_move(GTK_WINDOW(dialog),zdpx,zdpy); return; } else // "nn/nn" // position from caller { ii = sscanf(posn,"%f/%f",&xpos,&ypos); // parse "nn/nn" if (ii != 2) return; zdpx = ppx + 0.01 * xpos * pww; // position for dialog window zdpy = ppy + 0.01 * ypos * phh; gtk_window_move(GTK_WINDOW(dialog),zdpx,zdpy); return; } } // If the dialog window position is "save" then save // its position WRT parent or desktop for next use. // called by zdialog_destroy(). void zdialog_save_position(zdialog *zd) { using namespace zdposn_names; int ii, ppx, ppy, pww, phh, zdpx, zdpy; float xpos, ypos; char wintitle[64], *pp; GtkWidget *parent, *dialog; dialog = zd->widget[0].widget; if (! dialog) return; // destroyed gtk_window_get_position(GTK_WINDOW(dialog),&zdpx,&zdpy); // dialog window NW corner if (! zdpx && ! zdpy) return; // (0,0) ignore v.5.7 parent = zd->parent; // parent window if (! parent) { // no parent window ppx = ppy = 0; // use desktop pww = gdk_screen_width(); phh = gdk_screen_height(); } else { gtk_window_get_position(GTK_WINDOW(parent),&ppx,&ppy); // parent window NW corner gtk_window_get_size(GTK_WINDOW(parent),&pww,&phh); // parent window size } xpos = 100.0 * (zdpx - ppx) / pww; // dialog window relative position ypos = 100.0 * (zdpy - ppy) / phh; // (as percent of parent size) pp = (char *) gtk_window_get_title(GTK_WINDOW(dialog)); if (! pp) return; if (strlen(pp) < 2) return; strncpy0(wintitle,pp,64); // window title, < 64 chars. for (ii = 0; ii < Nzdposn; ii++) // search table for window if (strEqu(wintitle,zdposn[ii].wintitle)) break; if (ii == Nzdposn) { // not found if (ii == Nzdpmax) return; // table full strcpy(zdposn[ii].wintitle,wintitle); // add window to table Nzdposn++; } zdposn[ii].xpos = xpos; // save window position zdposn[ii].ypos = ypos; return; } /**************************************************************************/ // Functions to save and restore zdialog user inputs // within an app session or across app sessions. namespace zdinputs_names { struct zdinputs_t { char *zdtitle; // zdialog title int Nw; // no. of widgets char **wname; // list of widget names char **wdata; // list of widget data } zdinputs[200]; // space for 200 dialogs int Nzd = 0; // no. zdialogs in use int Nzdmax = 200; // max. zdialogs int Nwmax = zdmaxwidgets; // max. widgets in a zdialog int ccmax1 = 99; // max. widget name length int ccmax2 = 199; // max. widget data length } // Load zdialog input fields from its file (app startup) // or save zdialog input fields to its file (app shutdown). // Action is "load" or "save". // Number of zdialogs is returned. int zdialog_inputs(cchar *action) // v.5.3 { using namespace zfuncs; using namespace zdinputs_names; char zdinputsfile[200], buff[200]; char zdtitle[100], wname[100][100], wdata[100][200]; char *pp, *pp1, *pp2, wdata2[200]; FILE *fid; int Nw, ii, jj, cc, cc1, cc2; snprintf(zdinputsfile,199,"%s/zdialog_inputs",zuserdir); // /home//.appname/zdialog_inputs if (strEqu(action,"load")) // load dialog input fields from its file { Nzd = 0; fid = fopen(zdinputsfile,"r"); // no file if (! fid) return 0; while (true) { pp = fgets_trim(buff,200,fid,1); // read next zdialog title record if (! pp) break; if (! strnEqu(pp,"zdialog == ",11)) continue; strncpy0(zdtitle,pp+11,ccmax1); // save new zdialog title pp = fgets_trim(buff,200,fid,1); // read widget count if (! pp) break; Nw = atoi(pp); if (Nw < 1 || Nw > Nwmax) { printz("*** zdialog_inputs() bad data: %s \n",zdtitle); continue; } for (ii = 0; ii < Nw; ii++) // read widget data recs { pp = fgets_trim(buff,200,fid,1); if (! pp) break; pp1 = pp; pp2 = strstr(pp1," =="); if (! pp2) break; // widget has no data cc1 = pp2 - pp1; pp1[cc1] = 0; pp2 += 3; if (*pp2 == ' ') pp2++; cc2 = strlen(pp2); if (cc1 < 1 || cc1 > ccmax1) break; if (cc2 < 1) pp2 = (char *) ""; if (cc2 > ccmax2) break; strcpy(wname[ii],pp1); // save widget name and data strcpy(wdata2,pp2); repl_1str(wdata2,wdata[ii],"\\n","\n"); // replace "\n" with newline chars. } if (ii < Nw) { printz("*** zdialog_inputs() bad data: %s \n",zdtitle); continue; } if (Nzd == Nzdmax) { printz("*** zdialog_inputs() too many dialogs \n"); break; } zdinputs[Nzd].zdtitle = strdup(zdtitle); // save acculumated zdialog data zdinputs[Nzd].Nw = Nw; cc = Nw * sizeof(char *); zdinputs[Nzd].wname = (char **) malloc(cc); zdinputs[Nzd].wdata = (char **) malloc(cc); for (ii = 0; ii < Nw; ii++) { zdinputs[Nzd].wname[ii] = strdup(wname[ii]); zdinputs[Nzd].wdata[ii] = strdup(wdata[ii]); } Nzd++; } fclose(fid); return Nzd; } if (strEqu(action,"save")) // save dialog input fields to its file { fid = fopen(zdinputsfile,"w"); if (! fid) { printz("*** zdialog_inputs() cannot write file \n"); return 0; } for (ii = 0; ii < Nzd; ii++) { fprintf(fid,"zdialog == %s \n",zdinputs[ii].zdtitle); // zdialog == zdialog title Nw = zdinputs[ii].Nw; fprintf(fid,"%d \n",Nw); // widget count for (jj = 0; jj < Nw; jj++) { pp1 = zdinputs[ii].wname[jj]; // widget name == widget data pp2 = zdinputs[ii].wdata[jj]; repl_1str(pp2,wdata2,"\n","\\n"); // replace newline chars. with "\n" fprintf(fid,"%s == %s \n",pp1,wdata2); } fprintf(fid,"\n"); } fclose(fid); return Nzd; } printz("*** zdialog_inputs bad action: %s \n",action); return 0; } // Save dialog user input fields when a dialog is finished. // Called automatically by zdialog_free(). int zdialog_save_inputs(zdialog *zd) // v.5.3 { using namespace zdinputs_names; char zdtitle[100], wname[100], wdata[200], *type; int ii, jj, Nw, cc; if (! zd || zd->sentinel != zdsentinel) { // detect destroyed dialog printz("*** zdialog_save_inputs, zdialog invalid \n"); return 0; } if (! zd->saveinputs) return 0; // zdialog does not use this service strncpy0(zdtitle,zd->widget[0].data,ccmax1); // zdialog title is widget[0].data for (ii = 0; ii < Nzd; ii++) // find zdialog in zdinputs table if (strEqu(zdtitle,zdinputs[ii].zdtitle)) break; if (ii < Nzd) { // found free(zdinputs[ii].zdtitle); // delete obsolete zdinputs data for (jj = 0; jj < zdinputs[ii].Nw; jj++) { free(zdinputs[ii].wname[jj]); free(zdinputs[ii].wdata[jj]); } free(zdinputs[ii].wname); free(zdinputs[ii].wdata); Nzd--; // decr. zdialog count for (ii = ii; ii < Nzd; ii++) // pack down the rest zdinputs[ii] = zdinputs[ii+1]; } if (Nzd == Nzdmax) { printz("*** zdialog_save_inputs, too many zdialogs \n"); return 0; } ii = Nzd; // next zdinputs table entry for (Nw = 0, jj = 1; zd->widget[jj].type; jj++) // count zdialog widgets { type = (char *) zd->widget[jj].type; if (strstr("dialog hbox vbox hsep vsep frame " // skip non-input widgets "scrwin label link button",type)) continue; Nw++; } if (! Nw) return 0; // no input widgets if (Nw > Nwmax) { printz("*** zdialog_inputs() bad data: %s \n",zdtitle); return 0; } zdinputs[ii].zdtitle = strdup(zdtitle); // set zdialog title cc = Nw * sizeof(char *); // allocate pointers for widgets zdinputs[ii].wname = (char **) malloc(cc); zdinputs[ii].wdata = (char **) malloc(cc); for (Nw = 0, jj = 1; zd->widget[jj].type; jj++) // add widget names and data { type = (char *) zd->widget[jj].type; if (strstr("dialog hbox vbox hsep vsep frame " "scrwin label link button",type)) continue; strncpy0(wname,zd->widget[jj].name,ccmax1); if (zd->widget[jj].data) strncpy0(wdata,zd->widget[jj].data,ccmax2); else strcpy(wdata,""); zdinputs[ii].wname[Nw] = strdup(wname); zdinputs[ii].wdata[Nw] = strdup(wdata); Nw++; } zdinputs[ii].Nw = Nw; // set widget count Nzd++; // add zdialog to end of zdinputs return 1; } // Restore user input fields from prior use of the same dialog. // Call this if wanted after zdialog is built and before it is run. // Override old user inputs with zdialog_stuff() where needed. int zdialog_restore_inputs(zdialog *zd) // v.5.3 { using namespace zdinputs_names; char *zdtitle, *wname, *wdata; int ii, jj; zd->saveinputs = 1; // flag, save data at zdialog_free() zdtitle = (char *) zd->widget[0].data; // zdialog title for (ii = 0; ii < Nzd; ii++) // find zdialog in zdinputs if (strEqu(zdtitle,zdinputs[ii].zdtitle)) break; if (ii == Nzd) return 0; // not found for (jj = 0; jj < zdinputs[ii].Nw; jj++) { // stuff all saved widget data wname = zdinputs[ii].wname[jj]; wdata = zdinputs[ii].wdata[jj]; zdialog_put_data(zd,wname,wdata); } return 1; } /**************************************************************************/ // Output text to a popup window. // Window is left on screen until user destroys it with [x] button or // caller closes it with "close" action. // action: open: create window with title = text and size = ww, hh // write: write text to next line in window // top: scroll back to line 1 after writing last line // close: close window now // close N: close window after N seconds // The parent argument is optional and will cause the popup window to center // on the parent window. The GtkTextView window is returned for possible // use with textwidget_set_clickfunc() etc. GtkWidget * write_popup_text(cchar *action, cchar *text, int ww, int hh, GtkWidget *parent) { int popup_text_timeout(GtkWidget **mWin); static GtkWidget *mWin = 0, *mVbox, *mScroll; static GtkWidget *mLog = 0; static PangoFontDescription *monofont = 0; int secs; zthreadcrash(); // thread usage not allowed if (! monofont) monofont = pango_font_description_from_string("monospace 9"); if (strEqu(action,"open")) { if (mWin) gtk_widget_destroy(mWin); // only one at a time if (! ww) ww = 400; if (! hh) hh = 300; mWin = gtk_window_new(GTK_WINDOW_TOPLEVEL); // create main window gtk_window_set_title(GTK_WINDOW(mWin),text); gtk_window_set_default_size(GTK_WINDOW(mWin),ww,hh); if (parent) { // parent added gtk_window_set_transient_for(GTK_WINDOW(mWin),GTK_WINDOW(parent)); gtk_window_set_position(GTK_WINDOW(mWin),GTK_WIN_POS_CENTER_ON_PARENT); } else gtk_window_set_position(GTK_WINDOW(mWin),GTK_WIN_POS_MOUSE); mVbox = gtk_box_new(VERTICAL,0); // vertical packing box gtk_container_add(GTK_CONTAINER(mWin),mVbox); // add to main window mScroll = gtk_scrolled_window_new(0,0); // scrolled window gtk_box_pack_end(GTK_BOX(mVbox),mScroll,1,1,0); // add to main window mVbox gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(mScroll), GTK_POLICY_AUTOMATIC,GTK_POLICY_AUTOMATIC); mLog = gtk_text_view_new(); // text edit window gtk_container_add(GTK_CONTAINER(mScroll),mLog); // add to scrolled window gtk_widget_override_font(mLog,monofont); // set small monospaced font G_SIGNAL(mWin,"destroy",write_popup_text,"destroypop"); // connect window destroy event gtk_widget_show_all(mWin); // show window gtk_window_present(GTK_WINDOW(mWin)); if (! parent) gtk_window_set_keep_above(GTK_WINDOW(mWin),1); zfuncs::open_popup_windows++; } if (strEqu(action,"write")) // add text to window if (mWin) wprintf(mLog," %s\n",text); if (strEqu(action,"top")) // scroll to top line if (mWin) wscroll(mLog,1); if (strnEqu(action,"close",5)) { // close window secs = atoi(action+5); if (secs < 1) { if (mWin) gtk_widget_destroy(mWin); // immediate mWin = 0; } else // after N seconds g_timeout_add_seconds(secs,(GSourceFunc) popup_text_timeout,&mWin); } if (text && strEqu(text,"destroypop")) { // "destroy" signal from [x] zfuncs::open_popup_windows--; mWin = 0; } zmainloop(); return mLog; } // private function for timeout int popup_text_timeout(GtkWidget **mWin) // v.5.6 { if (*mWin) gtk_widget_destroy(*mWin); return 0; } /**************************************************************************/ // execute a command and show the output in a scrolling popup window int popup_command(cchar *command, int ww, int hh, GtkWidget *parent) // use write_popup_text() { char *buff; int err, contx = 0; write_popup_text("open",command,ww,hh,parent); // bugfix while ((buff = command_output(contx,command))) { write_popup_text("write",buff); free(buff); } write_popup_text("top",0); // back to top of window err = command_status(contx); return err; } /**************************************************************************/ // display message box and wait for user acknowledgement // may be called from a thread (uses xterm message). void zmessageACK(GtkWidget *parent, cchar *title, cchar *format, ... ) // with title v.5.5 { va_list arglist; char message[1000]; zdialog *zd; GtkWidget *widget; if (! pthread_equal(pthread_self(),zfuncs::tid_main)) { // from a thread, no GTK allowed zpopup_message(format); return; } va_start(arglist,format); vsnprintf(message,999,format,arglist); va_end(arglist); zd = zdialog_new(title,parent,"OK",null); zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3"); zdialog_add_widget(zd,"label","lab1","hb1",message,"space=5"); zdialog_resize(zd,200,0); widget = zdialog_widget(zd,"dialog"); // make modal gtk_window_set_modal(GTK_WINDOW(widget),1); gtk_window_set_keep_above(GTK_WINDOW(widget),1); // keep on top v.5.2 gtk_window_present(GTK_WINDOW(widget)); // should not be needed (GTK bug) v.5.5 zdialog_run(zd,0,"mouse"); // v.5.5 zdialog_wait(zd); zdialog_free(zd); return; } /**************************************************************************/ // log error message to STDOUT as well as message box and user OK void zmessLogACK(GtkWidget *parent, cchar *format, ...) { va_list arglist; char message[1000]; va_start(arglist,format); vsnprintf(message,999,format,arglist); va_end(arglist); printz("%s \n",message); zmessageACK(parent,0,message); return; } /**************************************************************************/ // display message box and wait for user Yes or No response // returns 1 or 0 int zmessageYN(GtkWidget *parent, cchar *format, ... ) { va_list arglist; char message[400]; zdialog *zd; int zstat; GtkWidget *widget; zthreadcrash(); // thread usage not allowed va_start(arglist,format); vsnprintf(message,400,format,arglist); va_end(arglist); zd = zdialog_new("message",parent,ZTX("Yes"),ZTX("No"),null); zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3"); zdialog_add_widget(zd,"label","lab1","hb1",message,"space=5"); zdialog_resize(zd,200,0); widget = zdialog_widget(zd,"dialog"); // make modal gtk_window_set_modal(GTK_WINDOW(widget),1); gtk_window_set_keep_above(GTK_WINDOW(widget),1); // keep on top v.5.2 zdialog_run(zd,0,"mouse"); // v.5.5 zstat = zdialog_wait(zd); zdialog_free(zd); if (zstat == 1) return 1; return 0; } /**************************************************************************/ // display message until timeout (can be forever) or user cancel // or caller kills it with zdialog_free() zdialog * zmessage_post(GtkWidget *parent, int seconds, cchar *format, ... ) { int zmessage_post_timeout(zdialog *zd); int zmessage_post_event(zdialog *zd, cchar *event); va_list arglist; char message[400]; static zdialog *zd; zthreadcrash(); // thread usage not allowed va_start(arglist,format); vsnprintf(message,400,format,arglist); va_end(arglist); zd = zdialog_new("message",parent,ZTX("cancel"),null); zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3"); zdialog_add_widget(zd,"label","lab1","hb1",message,"space=5"); zdialog_run(zd,zmessage_post_event); if (seconds) g_timeout_add_seconds(seconds,(GSourceFunc) zmessage_post_timeout,zd); zmainloop(); // v.5.2 return zd; } int zmessage_post_timeout(zdialog *zd) { if (! zd || zd->sentinel != zdsentinel) return 0; // v.5.7 if (zd->zstat == 0) zdialog_free(zd); return 0; } int zmessage_post_event(zdialog *zd, cchar *event) { if (! zd || zd->sentinel != zdsentinel) return 0; // v.5.7 if (zd->zstat) zdialog_free(zd); return 0; } /**************************************************************************/ // get text input from a popup dialog // returned text is subject for free() // null is returned if user presses [cancel] button. char * zdialog_text(GtkWidget *parent, cchar *title, cchar *inittext) { zdialog *zd; int zstat; char *text; GtkWidget *widget; zthreadcrash(); // thread usage not allowed zd = zdialog_new(title,parent,"OK",ZTX("cancel"),null); zdialog_add_widget(zd,"frame","fred","dialog"); zdialog_add_widget(zd,"edit","edit","fred"); if (inittext) zdialog_stuff(zd,"edit",inittext); zdialog_resize(zd,200,0); widget = zdialog_widget(zd,"dialog"); // make modal gtk_window_set_modal(GTK_WINDOW(widget),1); gtk_window_set_keep_above(GTK_WINDOW(widget),1); // keep on top v.5.2 zdialog_run(zd,0,"mouse"); // v.5.5 zstat = zdialog_wait(zd); if (zstat == 1) text = (char *) zdialog_get_data(zd,"edit"); else text = 0; if (text) text = strdup(text); zdialog_free(zd); return text; } /**************************************************************************/ // Display a dialog with a message and 1-5 choice buttons. // Returns choice 1-N corresponding to button selected. // Parent window may be NULL. // List of buttons must be NULL terminated. int zdialog_choose(cchar *title, GtkWidget *parent, cchar *message, ...) { zdialog *zd; va_list arglist; int ii, zstat, Nbutts; cchar *butts[6]; GtkWidget *widget; zthreadcrash(); // thread usage not allowed va_start(arglist,message); for (ii = 0; ii < 5; ii++) { butts[ii] = va_arg(arglist,cchar *); if (! butts[ii]) break; } Nbutts = ii; if (! Nbutts) zappcrash("zdialog_choose(), no buttons"); repeat: zd = zdialog_new(title,parent,butts[0],butts[1],butts[2],butts[3],butts[4],null); zdialog_add_widget(zd,"hbox","hbmess","dialog","space=3"); zdialog_add_widget(zd,"label","labmess","hbmess",message,"space=5"); widget = zdialog_widget(zd,"dialog"); // make modal gtk_window_set_modal(GTK_WINDOW(widget),1); gtk_window_set_keep_above(GTK_WINDOW(widget),1); // keep on top v.5.2 zdialog_run(zd,0,"mouse"); // v.5.5 zstat = zdialog_wait(zd); zdialog_free(zd); if (zstat < 1) goto repeat; return zstat; } /**************************************************************************/ // functions to show popup text windows namespace poptext { char *ptext = 0; GtkWidget *pwidget = 0; char *pcurrent = 0; GdkRGBA pblack = { 0.2, 0.2, 0.2, 1 }; // v.5.6 GdkRGBA pwhite = { 0.9, 0.9, 0.9, 1 }; #define GSFNORMAL GTK_STATE_FLAG_NORMAL } // timer function to show popup window after a given time int poptext_show(char *current) { using namespace poptext; if (current != pcurrent) return 0; if (pwidget) gtk_widget_show_all(pwidget); return 0; } // timer function to kill popup window after a given time int poptext_kill(char *current) { using namespace poptext; if (current != pcurrent) return 0; if (pwidget) gtk_widget_destroy(pwidget); if (ptext) free(ptext); pwidget = 0; ptext = 0; return 0; } // kill popup window unconditionally // v.5.8 int poptext_killnow() { using namespace poptext; if (pwidget) gtk_widget_destroy(pwidget); if (ptext) free(ptext); pwidget = 0; ptext = 0; return 0; } // Show a popup text message at current mouse position + offsets. // v.5.6 // Any prior popup will be killed and replaced. // If text == null, kill without replacement. // secs1 is time to delay before showing the popup. // secs2 is time to kill the popup after it is shown. // This function returns immediately. // Get mouse via GdkEvent->device. void poptext_mouse(cchar *text, GdkDevice *mouse, int dx, int dy, float secs1, float secs2) { using namespace poptext; GtkWidget *label; int cc, mx, my; int millisec1, millisec2; zthreadcrash(); // thread usage not allowed if (pwidget) gtk_widget_destroy(pwidget); // kill prior popup if (ptext) free(ptext); pwidget = 0; ptext = 0; pcurrent++; // make current != pcurrent if (! text) return; cc = strlen(text); // construct popup window ptext = (char *) malloc(cc+1); // with caller's text strcpy(ptext,text); pwidget = gtk_window_new(GTK_WINDOW_POPUP); label = gtk_label_new(ptext); gtk_container_add(GTK_CONTAINER(pwidget),label); gtk_widget_override_background_color(pwidget,GSFNORMAL,&pblack); // window not label v.5.6 gtk_widget_override_color(label,GSFNORMAL,&pwhite); gdk_device_get_position(mouse,0,&mx,&my); // position popup window gtk_window_move(GTK_WINDOW(pwidget),mx+dx,my+dy); if (secs1 > 0) { // delayed popup display millisec1 = secs1 * 1000; g_timeout_add(millisec1,(GSourceFunc) poptext_show,pcurrent); } else gtk_widget_show_all(pwidget); // immediate display if (secs2 > 0) { // popup kill timer millisec2 = (secs1 + secs2) * 1000; g_timeout_add(millisec2,(GSourceFunc) poptext_kill,pcurrent); } return; } // Show a popup text message at the given window position. // v.5.6 // Any prior popup will be killed and replaced. // If text == null, kill without replacement. // secs1 is time to delay before showing the popup. // secs2 is time to kill the popup after it is shown (-1 = never). // This function returns immediately. void poptext_window(cchar *text, GtkWindow *pwin, int mx, int my, float secs1, float secs2) { using namespace poptext; GtkWidget *label; int cc, pmx, pmy; int millisec1, millisec2; zthreadcrash(); // thread usage not allowed if (pwidget) gtk_widget_destroy(pwidget); // kill prior popup if (ptext) free(ptext); pwidget = 0; ptext = 0; pcurrent++; // make current != pcurrent if (! text) return; cc = strlen(text); // construct popup window ptext = (char *) malloc(cc+1); // with caller's text strcpy(ptext,text); pwidget = gtk_window_new(GTK_WINDOW_POPUP); label = gtk_label_new(ptext); gtk_container_add(GTK_CONTAINER(pwidget),label); gtk_widget_override_background_color(pwidget,GSFNORMAL,&pblack); // window not label v.5.6 gtk_widget_override_color(label,GSFNORMAL,&pwhite); if (pwin) { // position relative to pwin gtk_window_get_position(pwin,&pmx,&pmy); mx += pmx; my += pmy; } gtk_window_move(GTK_WINDOW(pwidget),mx,my); if (secs1 > 0) { // delayed popup display millisec1 = secs1 * 1000; g_timeout_add(millisec1,(GSourceFunc) poptext_show,pcurrent); } else gtk_widget_show_all(pwidget); // immediate display if (secs2 > 0) { // popup kill timer millisec2 = (secs1 + secs2) * 1000; g_timeout_add(millisec2,(GSourceFunc) poptext_kill,pcurrent); } return; } // show an image file in a new popup window // returns 0 if OK, +N otherwise int popup_image(cchar *imagefile, GtkWindow *parent, int size) // v.5.8 { void popup_image_paint(GtkWidget *, cairo_t *, cchar *); GtkWidget *window, *drawarea; window = gtk_window_new(GTK_WINDOW_TOPLEVEL); // create output window gtk_window_set_default_size(GTK_WINDOW(window),size,size); drawarea = gtk_drawing_area_new(); // add drawing window gtk_container_add(GTK_CONTAINER(window),drawarea); G_SIGNAL(drawarea,"draw",popup_image_paint,imagefile); // connect paint function if (parent) gtk_window_set_transient_for(GTK_WINDOW(window),parent); gtk_window_set_position(GTK_WINDOW(window),GTK_WIN_POS_MOUSE); gtk_widget_show_all(window); return 0; } void popup_image_paint(GtkWidget *drawarea, cairo_t *cr, cchar *imagefile) { GdkPixbuf *pixbuf; GError *gerror = 0; GtkWidget *window; int ww, hh; cchar *pp; ww = gtk_widget_get_allocated_width(drawarea); // drawing window size hh = gtk_widget_get_allocated_height(drawarea); pixbuf = gdk_pixbuf_new_from_file_at_size(imagefile,ww,hh,&gerror); // load image file into pixbuf if (! pixbuf) { printz("file: %s \n %s \n",imagefile,gerror->message); return; } ww = gdk_pixbuf_get_width(pixbuf); hh = gdk_pixbuf_get_height(pixbuf); window = gtk_widget_get_parent(drawarea); gtk_window_set_default_size(GTK_WINDOW(window),ww,hh); gtk_window_resize(GTK_WINDOW(window),ww,hh); pp = strrchr(imagefile,'/'); gtk_window_set_title(GTK_WINDOW(window),pp+1); gdk_cairo_set_source_pixbuf(cr,pixbuf,0,0); // paint window cairo_paint(cr); g_object_unref(pixbuf); return; } // move the mouse pointer to given position in given window int move_pointer(GtkWidget *widget, int px, int py) // v.5.6 { GdkScreen *screen; GdkDevice *mouse; GdkWindow *window; int err, rpx, rpy; err = get_mouse_device(widget,&screen,&mouse); if (err) return 0; window = gtk_widget_get_window(widget); if (! window) return 0; gdk_window_get_root_coords(window,px,py,&rpx,&rpy); gdk_device_warp(mouse,screen,rpx,rpy); return 1; } /************************************************************************** File chooser dialog for one or more files Action: "file" select an existing file "files" select multiple existing files "save" select an existing or new file "folder" select existing folder "folders" select multiple existing folders "create folder" select existing or new folder buttx: "hidden" add button to toggle display of hidden files "quality" add button to set JPG file save quality optional, default = null Returns a list of filespecs terminated with null. Memory for returned list and returned files are subjects for free(); ***************************************************************************/ // version for 1 file only: file, save, folder, create folder // returns one filespec or null char * zgetfile(cchar *title, cchar *action, cchar *initfile, cchar *buttx) { if (! strcmpv(action,"file","save","folder","create folder",null)) zappcrash("zgetfile() call error: %s",action); char **flist = zgetfiles(title,action,initfile,buttx); if (! flist) return 0; char *file = *flist; free(flist); return file; } // version for 1 or more files // returns a list of filespecs (char **) terminated with null char ** zgetfiles(cchar *title, cchar *action, cchar *initfile, cchar *buttx) { using namespace zfuncs; void zgetfile_preview(GtkWidget *dialog, GtkWidget *pvwidget); // private functions int zgetfile_KBkey(GtkWidget *dialog, GdkEventKey *event); void zgetfile_newfolder(GtkFileChooser *dialog, void *); GtkFileChooserAction fcact = GTK_FILE_CHOOSER_ACTION_OPEN; GtkWidget *dialog; GtkWidget *pvwidget = gtk_image_new(); GSList *gslist = 0; cchar *button1 = 0, *buttxx = 0; char *pdir, *pfile; int ii, err, NF, setfname = 0; int fcstat, bcode = 0, qnum, hide = 0; char *qual, *file1, *file2, **flist = 0; struct stat fstat; zthreadcrash(); // thread usage not allowed if (strEqu(action,"file")) { fcact = GTK_FILE_CHOOSER_ACTION_OPEN; button1 = ZTX("choose file"); } else if (strEqu(action,"files")) { fcact = GTK_FILE_CHOOSER_ACTION_OPEN; button1 = ZTX("choose files"); } else if (strEqu(action,"save")) { fcact = GTK_FILE_CHOOSER_ACTION_SAVE; button1 = ZTX("save"); setfname = 1; } else if (strEqu(action,"folder")) { fcact = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER; button1 = ZTX("choose folder"); } else if (strEqu(action,"folders")) { fcact = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER; button1 = ZTX("choose folders"); } else if (strEqu(action,"create folder")) { fcact = GTK_FILE_CHOOSER_ACTION_CREATE_FOLDER; button1 = ZTX("create folder"); setfname = 1; } else zappcrash("zgetfiles() call error: %s",action); if (buttx) { if (strnEqu(buttx,"hidden",6)) { // generate text for translation buttxx = ZTX("hidden"); bcode = 103; } if (strEqu(buttx,"quality")) { buttxx = ZTX("quality"); bcode = 104; } } dialog = gtk_file_chooser_dialog_new(title, null, fcact, // create file selection dialog button1, GTK_RESPONSE_ACCEPT, ZTX("cancel"), GTK_RESPONSE_CANCEL, buttxx, bcode, null); gtk_file_chooser_set_preview_widget(GTK_FILE_CHOOSER(dialog),pvwidget); G_SIGNAL(dialog,"update-preview",zgetfile_preview,pvwidget); // create preview for selected file G_SIGNAL(dialog,"key-release-event",zgetfile_KBkey,0); // respond to F1 help key gtk_window_set_position(GTK_WINDOW(dialog),GTK_WIN_POS_MOUSE); // put dialog at mouse position gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(dialog),0); // default: no show hidden if (strEqu(action,"save")) // overwrite confirmation gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog),1); if (strEqu(action,"files") || strEqu(action,"folders")) gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog),1); // select multiple files or folders v.5.8 if (initfile) { // pre-select filespec err = stat(initfile,&fstat); if (err) { pdir = strdup(initfile); // non-existent file pfile = strrchr(pdir,'/'); if (pfile && pfile > pdir) { *pfile++ = 0; // set folder name gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),pdir); } if (setfname) // set new file name gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog),pfile); free(pdir); } else if (S_ISREG(fstat.st_mode)) // select given file gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dialog),initfile); else if (S_ISDIR(fstat.st_mode)) // select given folder gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),initfile); } gtk_widget_show_all(dialog); while (true) { fcstat = gtk_dialog_run(GTK_DIALOG(dialog)); // run dialog, get status button if (fcstat == 103) { // show/hide hidden files hide = gtk_file_chooser_get_show_hidden(GTK_FILE_CHOOSER(dialog)); hide = 1 - hide; gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(dialog),hide); } else if (fcstat == 104) { // get JPG quality parameter while (true) { qual = zdialog_text(null,ZTX("JPG quality 0-100"),JPGquality); if (! qual) break; // cancel = no change err = convSI(qual,qnum,0,100); free(qual); if (err) continue; // enforce 0-100 snprintf(JPGquality,4,"%d",qnum); break; } } else break; // some other button } if (fcstat == GTK_RESPONSE_ACCEPT) { gslist = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog)); if (! gslist) goto fcreturn; NF = g_slist_length(gslist); // no. selected files if (! NF) goto fcreturn; flist = (char **) malloc((NF+1)*sizeof(char *)); // allocate returned list for (ii = 0; ii < NF; ii++) { // process selected files file1 = (char *) g_slist_nth_data(gslist,ii); if (strlen(file1) >= maxfcc) file1 = (char *) "ridiculously long filespec"; file2 = strdup(file1); // re-allocate memory g_free(file1); flist[ii] = file2; } flist[ii] = 0; // EOL marker } fcreturn: if (gslist) g_slist_free(gslist); gtk_widget_destroy(dialog); return flist; } // zgetfile private function - get preview images for image files void zgetfile_preview(GtkWidget *dialog, GtkWidget *pvwidget) { using namespace zfuncs; GdkPixbuf *thumbnail; char *filename; filename = gtk_file_chooser_get_preview_filename(GTK_FILE_CHOOSER(dialog)); if (! filename) { gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(dialog),0); return; } thumbnail = get_thumbnail(filename,192); // use 192x192 pixels g_free(filename); if (thumbnail) { gtk_image_set_from_pixbuf(GTK_IMAGE(pvwidget),thumbnail); gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(dialog),1); g_object_unref(thumbnail); } else gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(dialog),0); return; } // zgetfile private function - send F1 key (help) to main app int zgetfile_KBkey(GtkWidget *dialog, GdkEventKey *event) { int KBkey = event->keyval; if (KBkey != GDK_KEY_F1) return 0; // v.5.6 KBstate(event,0); return 1; } /************************************************************************** print_image_file(GtkWidget *parent, cchar *imagefile) Print an image file using the printer, paper, orientation, margins, and scale set by the user. HPLIP problem: Setting paper size was made less flexible. GtkPrintSettings paper size must agree with the one in the current printer setup. This can only be set in the printer setup dialog, not in the application. Also the print size (width, height) comes from the chosen paper size and cannot be changed in the application. Print margins can be changed to effect printing a smaller or shifted image on a larger paper size. ***************************************************************************/ namespace print_image { #define MM GTK_UNIT_MM #define INCH GTK_UNIT_INCH #define PRINTOP GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG #define PORTRAIT GTK_PAGE_ORIENTATION_PORTRAIT #define LANDSCAPE GTK_PAGE_ORIENTATION_LANDSCAPE #define QUALITY GTK_PRINT_QUALITY_HIGH GtkWidget *parent = 0; GtkPageSetup *priorpagesetup = 0; GtkPageSetup *pagesetup; GtkPrintSettings *printsettings = 0; GtkPrintOperation *printop; GtkPageOrientation orientation = PORTRAIT; GdkPixbuf *pixbuf; cchar *printer = 0; int landscape = 0; // true if landscape double width = 21.0, height = 29.7; // paper size, CM (default A4 portrait) double margins[4] = { 0.5, 0.5, 0.5, 0.5 }; // margins, CM (default 0.5) v.5.5 double imagescale = 100; // image print scale, percent double pwidth, pheight; // printed image size int paper_setup(); int margins_setup(); int margins_dialog_event(zdialog *zd, cchar *event); void get_printed_image_size(); void print_page(GtkPrintOperation *, GtkPrintContext *, int page); } // user callable function to set paper, margins, scale, and then print void print_image_file(GtkWidget *pwin, cchar *imagefile) // overhauled v.5.2 { using namespace print_image; GtkPrintOperationResult printstat; GError *gerror = 0; int err; parent = pwin; // save parent window pixbuf = gdk_pixbuf_new_from_file(imagefile,&gerror); // read image file if (! pixbuf) { zmessageACK(null,0,gerror->message); return; } err = paper_setup(); // select paper if (err) return; // v.5.5 err = margins_setup(); // set margins and scale if (err) return; // v.5.5 printop = gtk_print_operation_new(); // print operation gtk_print_operation_set_default_page_setup(printop,pagesetup); gtk_print_operation_set_print_settings(printop,printsettings); gtk_print_operation_set_n_pages(printop,1); g_signal_connect(printop,"draw-page",G_CALLBACK(print_page),0); // start print printstat = gtk_print_operation_run(printop,PRINTOP,0,0); if (printstat == GTK_PRINT_OPERATION_RESULT_ERROR) { gtk_print_operation_get_error(printop,&gerror); zmessageACK(null,0,gerror->message); } g_object_unref(printop); return; } // draw the graphics for the print page // rescale with cairo void print_image::print_page(GtkPrintOperation *printop, GtkPrintContext *printcontext, int page) { using namespace print_image; cairo_t *cairocontext; double iww, ihh, pww, phh, scale; pww = gtk_print_context_get_width(printcontext); // print context size, pixels phh = gtk_print_context_get_height(printcontext); iww = gdk_pixbuf_get_width(pixbuf); // original image size ihh = gdk_pixbuf_get_height(pixbuf); scale = pww / iww; // rescale to fit page if (phh / ihh < scale) scale = phh / ihh; cairocontext = gtk_print_context_get_cairo_context(printcontext); // use cairo to rescale cairo_translate(cairocontext,0,0); cairo_scale(cairocontext,scale,scale); gdk_cairo_set_source_pixbuf(cairocontext,pixbuf,0,0); cairo_paint(cairocontext); return; } // Do a print paper format selection, after which the page width, height // and orientation are available to the caller. Units are CM. // (paper width and height are reversed for landscape orientation) int print_image::paper_setup() { using namespace zfuncs; using namespace print_image; char printsettingsfile[200], pagesetupfile[200]; snprintf(printsettingsfile,200,"%s/printsettings",zuserdir); snprintf(pagesetupfile,200,"%s/pagesetup",zuserdir); if (! printsettings) { // start with prior print settings printsettings = gtk_print_settings_new_from_file(printsettingsfile,0); if (! printsettings) printsettings = gtk_print_settings_new(); } if (! priorpagesetup) { // start with prior page setup priorpagesetup = gtk_page_setup_new_from_file(pagesetupfile,0); if (! priorpagesetup) priorpagesetup = gtk_page_setup_new(); } pagesetup = gtk_print_run_page_setup_dialog // select printer, paper, orientation (GTK_WINDOW(parent),priorpagesetup,printsettings); // user cancel cannot be detected v.5.5 g_object_unref(priorpagesetup); // save for next call priorpagesetup = pagesetup; orientation = gtk_print_settings_get_orientation(printsettings); // save orientation if (orientation == LANDSCAPE) landscape = 1; else landscape = 0; gtk_print_settings_set_quality(printsettings,QUALITY); // set high quality 300 dpi gtk_print_settings_set_resolution(printsettings,300); gtk_print_settings_to_file(printsettings,printsettingsfile,0); // save print settings to file gtk_page_setup_to_file(pagesetup,pagesetupfile,0); // save print settings to file return 0; } // Optionally set the print margins and print scale. // If canceled the margins are zero (or printer-dependent minimum) // and the scale is 100% (fitting the paper and margins). int print_image::margins_setup() { using namespace print_image; zdialog *zd; int zstat; char imagesize[100]; #define formatsize ZTX("printed image width: %.1f height: %.1f cm") /*** top bottom left right margins [___] [___] [___] [___] scale [80|-+] percent printed image width: xx.x height: xx.x cm ***/ zd = zdialog_new(ZTX("margins"),parent,ZTX("done"),ZTX("cancel"),null); zdialog_add_widget(zd,"hbox","hbmlab","dialog"); zdialog_add_widget(zd,"vbox","vbmarg","hbmlab",0,"space=5"); zdialog_add_widget(zd,"vbox","vbtop","hbmlab",0,"space=5"); zdialog_add_widget(zd,"vbox","vbbottom","hbmlab",0,"space=5"); zdialog_add_widget(zd,"vbox","vbleft","hbmlab",0,"space=5"); zdialog_add_widget(zd,"vbox","vbright","hbmlab",0,"space=5"); zdialog_add_widget(zd,"label","space","vbmarg"," "); zdialog_add_widget(zd,"label","labtop","vbtop",ZTX("top")); zdialog_add_widget(zd,"label","labbot","vbbottom",ZTX("bottom")); zdialog_add_widget(zd,"label","lableft","vbleft",ZTX("left")); zdialog_add_widget(zd,"label","labright","vbright",ZTX("right")); zdialog_add_widget(zd,"label","labmarg","vbmarg",ZTX("margins"),"space=5"); zdialog_add_widget(zd,"spin","mtop","vbtop","0|3|0.1|0"); zdialog_add_widget(zd,"spin","mbottom","vbbottom","0|3|0.1|0"); zdialog_add_widget(zd,"spin","mleft","vbleft","0|3|0.1|0"); zdialog_add_widget(zd,"spin","mright","vbright","0|3|0.1|0"); zdialog_add_widget(zd,"hbox","hbscale","dialog",0,"space=8"); zdialog_add_widget(zd,"label","labscale","hbscale",ZTX("image scale"),"space=6"); zdialog_add_widget(zd,"spin","scale","hbscale","5|100|0.1|100"); zdialog_add_widget(zd,"label","labpct","hbscale",ZTX("percent"),"space=5"); zdialog_add_widget(zd,"hbox","hbsize","dialog"); zdialog_add_widget(zd,"label","labsize","hbsize","xxx","space=6"); zdialog_stuff(zd,"mtop",margins[0]); // stuff prior page margins zdialog_stuff(zd,"mbottom",margins[1]); zdialog_stuff(zd,"mleft",margins[2]); zdialog_stuff(zd,"mright",margins[3]); zdialog_stuff(zd,"scale",imagescale); // stuff prior image scale gtk_page_setup_set_top_margin(pagesetup,10*margins[0],MM); // set page margins gtk_page_setup_set_bottom_margin(pagesetup,10*margins[1],MM); // (cm to mm units) gtk_page_setup_set_left_margin(pagesetup,10*margins[2],MM); gtk_page_setup_set_right_margin(pagesetup,10*margins[3],MM); gtk_print_settings_set_scale(printsettings,imagescale); // set image print scale % get_printed_image_size(); snprintf(imagesize,100,formatsize,pwidth,pheight); // show print size in dialog zdialog_stuff(zd,"labsize",imagesize); zdialog_run(zd,margins_dialog_event); // run dialog zstat = zdialog_wait(zd); // wait for completion if (zstat != 1) { // user canceled zdialog_free(zd); return 1; // v.5.5 } zdialog_free(zd); // kill dialog return 0; } // dialog event function // save user margin and scale changes // recompute print image size int print_image::margins_dialog_event(zdialog *zd, cchar *event) { using namespace print_image; char imagesize[100]; zdialog_fetch(zd,"mtop",margins[0]); // save print margins zdialog_fetch(zd,"mbottom",margins[1]); zdialog_fetch(zd,"mleft",margins[2]); zdialog_fetch(zd,"mright",margins[3]); zdialog_fetch(zd,"scale",imagescale); // save image print scale gtk_page_setup_set_top_margin(pagesetup,10*margins[0],MM); // set page margins gtk_page_setup_set_bottom_margin(pagesetup,10*margins[1],MM); // (cm to mm units) gtk_page_setup_set_left_margin(pagesetup,10*margins[2],MM); gtk_page_setup_set_right_margin(pagesetup,10*margins[3],MM); gtk_print_settings_set_scale(printsettings,imagescale); // set image print scale % get_printed_image_size(); snprintf(imagesize,100,formatsize,pwidth,pheight); // show print size in dialog zdialog_stuff(zd,"labsize",imagesize); return 1; } // compute printed image size based on paper size, // orientation, margins, and scale (percent) void print_image::get_printed_image_size() { using namespace print_image; double iww, ihh, pww, phh, scale; pww = 0.1 * gtk_page_setup_get_paper_width(pagesetup,MM); // get paper size phh = 0.1 * gtk_page_setup_get_paper_height(pagesetup,MM); // (mm to cm units) pww = pww - margins[2] - margins[3]; // reduce for margins phh = phh - margins[1] - margins[2]; pww = pww / 2.54 * 300; // convert to dots @ 300 dpi phh = phh / 2.54 * 300; iww = gdk_pixbuf_get_width(pixbuf); // original image size, pixels ihh = gdk_pixbuf_get_height(pixbuf); scale = pww / iww; // rescale image to fit page if (phh / ihh < scale) scale = phh / ihh; scale = scale * 0.01 * imagescale; // adjust for user scale setting pwidth = iww * scale / 300 * 2.54; // dots to cm pheight = ihh * scale / 300 * 2.54; return; } /**************************************************************************/ // connect a user callback function to a window drag-drop event void drag_drop_connect(GtkWidget *window, drag_drop_func *ufunc) { int drag_drop_connect2(GtkWidget *, void *, int, int, void *, int, int, void *); char string[] = "STRING"; GtkTargetEntry file_drop_target = { string, 0, 0 }; gtk_drag_dest_set(window, GTK_DEST_DEFAULT_ALL, &file_drop_target, 1, GDK_ACTION_COPY); G_SIGNAL(window, "drag-data-received", drag_drop_connect2, ufunc); gtk_drag_dest_add_uri_targets(window); // accept URI (file) drop return; } // private function // get dropped file, clean escapes, pass to user function // passed filespec is subject for free() int drag_drop_connect2(GtkWidget *, void *, int mpx, int mpy, void *sdata, int, int, void *ufunc) { char * drag_drop_unescape(cchar *escaped_string); drag_drop_func *ufunc2; char *text, *text2, *file, *file2; int cc; text = (char *) gtk_selection_data_get_data((GtkSelectionData *) sdata); ufunc2 = (drag_drop_func *) ufunc; if (strstr(text,"file://")) // text is a filespec { file = strdup(text+7); // get rid of junk added by GTK cc = strlen(file); while (file[cc-1] < ' ') cc--; file[cc] = 0; file2 = drag_drop_unescape(file); // clean %xx escapes from Nautilus free(file); ufunc2(mpx,mpy,file2); // pass file to user function } else { text2 = strdup(text); ufunc2(mpx,mpy,text2); } return 1; } // private function // Clean %xx escapes from strange Nautilus drag-drop file names char * drag_drop_unescape(cchar *inp) { int drag_drop_convhex(char ch); char inch, *out, *outp; int nib1, nib2; out = (char *) malloc(strlen(inp)+1); outp = out; while ((inch = *inp++)) { if (inch == '%') { nib1 = drag_drop_convhex(*inp++); nib2 = drag_drop_convhex(*inp++); *outp++ = nib1 << 4 | nib2; } else *outp++ = inch; } *outp = 0; return out; } // private function - convert character 0-F to number 0-15 int drag_drop_convhex(char ch) { if (ch >= '0' && ch <= '9') return ch - '0'; if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10; if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10; return ch; } /************************************************************************** Miscellaneous GDK/GTK functions ***************************************************************************/ // Get thumbnail image for given image file. // Returned thumbnail belongs to caller: g_object_unref() is necessary. GdkPixbuf * get_thumbnail(char *fpath, int size) // v.5.0 { using namespace zfuncs; GdkPixbuf *thumbpxb; GError *gerror = 0; int err; char *bpath; struct stat statf; zthreadcrash(); // thread usage not allowed err = stat(fpath,&statf); // fpath status info if (err) return 0; if (S_ISDIR(statf.st_mode)) { // if directory, return folder image bpath = (char *) malloc(500); *bpath = 0; // v.5.8 strncatv(bpath,499,zicondir,"/folder.png",null); thumbpxb = gdk_pixbuf_new_from_file_at_size(bpath,size,size,&gerror); free(bpath); return thumbpxb; } thumbpxb = gdk_pixbuf_new_from_file_at_size(fpath,size,size,&gerror); return thumbpxb; // return pixbuf to caller } // make a cursor from a graphic file in application's icon directory // (see initz_appfiles()). GdkCursor * zmakecursor(cchar *iconfile) { using namespace zfuncs; GError *gerror = 0; GdkPixbuf *pixbuf; GdkDisplay *display; GdkCursor *cursor = 0; char iconpath[200]; zthreadcrash(); // thread usage not allowed display = gdk_display_get_default(); *iconpath = 0; strncatv(iconpath,199,zicondir,"/",iconfile,null); pixbuf = gdk_pixbuf_new_from_file(iconpath,&gerror); if (pixbuf && display) cursor = gdk_cursor_new_from_pixbuf(display,pixbuf,0,0); else printz("*** %s \n",gerror->message); return cursor; } /************************************************************************** GdkPixbuf * gdk_pixbuf_rotate(GdkPixbuf *pixbuf, float angle, int acolor) Rotate a pixbuf through an arbitrary angle (degrees). The returned image has the same size as the original, but the pixbuf envelope is increased to accomodate the rotated original (e.g. a 100x100 pixbuf rotated 45 deg. needs a 142x142 pixbuf). Pixels added around the rotated image have all RGB values = acolor. Angle is in degrees. Positive direction is clockwise. Pixbuf must have 8 bits per channel and 3 or 4 channels. Loss of resolution is about 1/2 pixel. Speed is about 28 million pixels/sec. on 3.3 GHz CPU. (e.g. a 10 megapix image needs about 0.36 seconds) NULL is returned if the function fails for one of the following: - pixbuf not 8 bits/channel or < 3 channels - unable to create output pixbuf (lack of memory?) Algorithm: create output pixbuf big enough for rotated input pixbuf compute coefficients for affine transform loop all output pixels (px2,py2) get corresp. input pixel (px1,py1) using affine transform if outside of pixmap output pixel = black continue for 4 input pixels based at (px0,py0) = (int(px1),int(py1)) compute overlap (0 to 1) with (px1,py1) sum RGB values * overlap output aggregate RGB to pixel (px2,py2) Benchmark: rotate 7 megapixel image 10 degrees 0.31 secs. 3.3 GHz Core i5 ***/ GdkPixbuf * gdk_pixbuf_rotate(GdkPixbuf *pixbuf1, float angle, int acolor) { typedef unsigned char *pixel; // 3 RGB values, 0-255 each GdkPixbuf *pixbuf2; GdkColorspace color; int nch, nbits, alpha; int ww1, hh1, rs1, ww2, hh2, rs2; int px2, py2, px0, py0; pixel ppix1, ppix2, pix0, pix1, pix2, pix3; float px1, py1; float f0, f1, f2, f3, red, green, blue, tran = 0; float a, b, d, e, ww15, hh15, ww25, hh25; float PI = 3.141593; zthreadcrash(); // thread usage not allowed nch = gdk_pixbuf_get_n_channels(pixbuf1); nbits = gdk_pixbuf_get_bits_per_sample(pixbuf1); if (nch < 3) return 0; // must have 3+ channels (colors) if (nbits != 8) return 0; // must be 8 bits per channel color = gdk_pixbuf_get_colorspace(pixbuf1); // get input pixbuf1 attributes alpha = gdk_pixbuf_get_has_alpha(pixbuf1); ww1 = gdk_pixbuf_get_width(pixbuf1); hh1 = gdk_pixbuf_get_height(pixbuf1); rs1 = gdk_pixbuf_get_rowstride(pixbuf1); while (angle < -180) angle += 360; // normalize, -180 to +180 while (angle > 180) angle -= 360; angle = angle * PI / 180; // radians, -PI to +PI if (fabsf(angle) < 0.001) { // bugfix 0.01 >> 0.001 pixbuf2 = gdk_pixbuf_copy(pixbuf1); // angle is zero within my precision return pixbuf2; } ww2 = int(ww1*fabsf(cosf(angle)) + hh1*fabsf(sinf(angle))); // rectangle containing rotated image hh2 = int(ww1*fabsf(sinf(angle)) + hh1*fabsf(cosf(angle))); pixbuf2 = gdk_pixbuf_new(color,alpha,nbits,ww2,hh2); // create output pixbuf2 if (! pixbuf2) return 0; rs2 = gdk_pixbuf_get_rowstride(pixbuf2); ppix1 = gdk_pixbuf_get_pixels(pixbuf1); // input pixel array ppix2 = gdk_pixbuf_get_pixels(pixbuf2); // output pixel array ww15 = 0.5 * ww1; hh15 = 0.5 * hh1; ww25 = 0.5 * ww2; hh25 = 0.5 * hh2; a = cosf(angle); // affine transform coefficients b = sinf(angle); d = - sinf(angle); e = cosf(angle); for (py2 = 0; py2 < hh2; py2++) // loop through output pixels for (px2 = 0; px2 < ww2; px2++) { px1 = a * (px2 - ww25) + b * (py2 - hh25) + ww15; // (px1,py1) = corresponding py1 = d * (px2 - ww25) + e * (py2 - hh25) + hh15; // point within input pixels px0 = int(px1); // pixel containing (px1,py1) py0 = int(py1); if (px1 < 0 || px0 >= ww1-1 || py1 < 0 || py0 >= hh1-1) { // if outside input pixel array pix2 = ppix2 + py2 * rs2 + px2 * nch; // output is acolor pix2[0] = pix2[1] = pix2[2] = acolor; continue; } pix0 = ppix1 + py0 * rs1 + px0 * nch; // 4 input pixels based at (px0,py0) pix1 = pix0 + rs1; pix2 = pix0 + nch; pix3 = pix0 + rs1 + nch; f0 = (px0+1 - px1) * (py0+1 - py1); // overlap of (px1,py1) f1 = (px0+1 - px1) * (py1 - py0); // in each of the 4 pixels f2 = (px1 - px0) * (py0+1 - py1); f3 = (px1 - px0) * (py1 - py0); red = f0 * pix0[0] + f1 * pix1[0] + f2 * pix2[0] + f3 * pix3[0]; // sum the weighted inputs green = f0 * pix0[1] + f1 * pix1[1] + f2 * pix2[1] + f3 * pix3[1]; blue = f0 * pix0[2] + f1 * pix1[2] + f2 * pix2[2] + f3 * pix3[2]; if (alpha) tran = f0 * pix0[3] + f1 * pix1[3] + f2 * pix2[3] + f3 * pix3[3]; // 4th color = alpha if (red == acolor && green == acolor && blue == acolor) { // avoid acolor in image if (blue == 0) blue = 1; else blue--; } pix2 = ppix2 + py2 * rs2 + px2 * nch; // output pixel pix2[0] = int(red); pix2[1] = int(green); pix2[2] = int(blue); if (alpha) pix2[3] = int(tran); // bugfix } return pixbuf2; } /************************************************************************** parameter management functions int initParmList(int maxparms) int loadParms() int loadParms(char *filename) int saveParms() int saveParms(char *filename) int setParm(cchar *parmname, double parmval) double getParm(cchar *parmname) char * getParm(int Nth) int listParms(GtkWidget *textWin) int editParms(GtkWidget *textWin, int addp) initParms call before any other functions to initialize empty list loadParms load parameters from a file - file selection dialog loadParms load parameters from a file - filename argument saveParms save parameters to a file - file selection dialog saveParms save parameters to a file - filename argument setParm sets the value of a parameter (created if not already defined) getParm returns the value of a parameter by name (NAN if non-existent) getParm returns the name of the Nth parameter (0 based), or null listParms list all parameter names and values in text window editParms edit the parameters with a GUI loadParms and saveParms use zgetfile() selection dialog loadParms returns the count of parameters loaded saveParms returns the count saved editParms() starts a dialog which shows all parameters and allows them to be edited. The dialog has buttons to allow loading parameters from a file or saving them to a file. If the argument textWin is non-zero, a button is added which lists all parameters to the window. If the argument addp is non-zero, a button is added to allow new parameters to be defined. ***************************************************************************/ struct t_parmlist { // parameter list in memory int max; // max parameter count int count; // actual parameter count char **name; // pointer to names (list of char *) double *value; // pointer to values (list of double) } parmlist; int parmlistvalid = 0; // flag char zparmfile[200]; // last used parm file // initialize parameter list - must be called first int initParmlist(int max) { using namespace zfuncs; int err; if (! parmlistvalid) { // start with default parms file err = locale_filespec("user","parameters",zparmfile); if (err) return 0; } if (parmlistvalid) { // delete old parms delete [] parmlist.name; delete [] parmlist.value; } parmlist.max = max; parmlist.count = 0; char **names = new char*[max]; // allocate max pointers for names double *values = new double[max]; // allocate max doubles for values parmlist.name = names; parmlist.value = values; parmlistvalid = 1; return 0; } // Load user parameters if the file exists, else initialize the // user parameters file from default application parameters. int initz_userParms() { if (! parmlistvalid) zappcrash("parmlistvalid = 0"); int np = loadParms("parameters"); if (! np) { saveParms("parameters"); zmessageACK(0,0,ZTX("Initial parameters file created. \n" "Inspect and revise if necessary.")); } return np; } // load parameters from a file, with file selection dialog int loadParms() { char *pfile; int np; if (! parmlistvalid) zappcrash("parmlistvalid = 0"); pfile = zgetfile(ZTX("load parameters from a file"),"file",zparmfile,"hidden"); if (! pfile) return 0; np = loadParms(pfile); free(pfile); return np; } // load parameters from a file // returns no. parameters loaded int loadParms(cchar *pfile) { FILE *fid; int Nth, np1, np2 = 0, err; char buff[100], *fgs, *pp; cchar *pname, *pvalue; double dvalue; if (! parmlistvalid) zappcrash("parmlistvalid = 0"); if (! pfile) pfile = zparmfile; if (*pfile != '/') { // if parm file name only, pp = (char *) strrchr(zparmfile,'/'); // make complete absolute path if (pp) strcpy(pp+1,pfile); // in same directory as prior pfile = zparmfile; } fid = fopen(pfile,"r"); if (! fid) return 0; // bad file strncpy0(zparmfile,pfile,200); // set current parm file while (true) // read file { fgs = fgets_trim(buff,99,fid,1); if (! fgs) break; // EOF pp = strchr(buff,'#'); // eliminate comments if (pp) *pp = 0; Nth = 1; // parse parm name, value pname = strField(buff,' ',Nth++); if (! pname) continue; pvalue = strField(buff,' ',Nth); if (! pvalue) continue; err = convSD(pvalue,dvalue); if (err) continue; np1 = setParm(pname,dvalue); // set the parameter if (! np1) continue; np2++; } fclose(fid); // close file return np2; // return parameter count } // save parameters to a file, with file selection dialog int saveParms() { char *pfile; int np; if (! parmlistvalid) zappcrash("parmlistvalid = 0"); pfile = zgetfile(ZTX("save parameters to a file"),"save",zparmfile,"hidden"); if (! pfile) return 0; np = saveParms(pfile); free(pfile); return np; } // save parameters to a file int saveParms(cchar *pfile) { FILE *fid; int np; char *pp; if (! parmlistvalid) zappcrash("parmlistvalid = 0"); if (*pfile != '/') { // if parm file name only, pp = (char *) strrchr(zparmfile,'/'); // make complete absolute path if (pp) strcpy(pp+1,pfile); // in same directory as prior pfile = zparmfile; } fid = fopen(pfile,"w"); if (! fid) { zmessageACK(null,null,ZTX("cannot open file %s"),pfile); return 0; } strncpy0(zparmfile,pfile,200); for (np = 0; np < parmlist.count; np++) fprintf(fid," \"%s\" %.12g \n",parmlist.name[np],parmlist.value[np]); fclose(fid); return np; } // create a new paramater or change value of existing parameter int setParm(cchar *parmname, double parmval) { int ii, cc; char *ppname; if (! parmlistvalid) zappcrash("parmlistvalid = 0"); for (ii = 0; ii < parmlist.count; ii++) if (strEqu(parmlist.name[ii],parmname)) break; if (ii == parmlist.max) return 0; if (ii == parmlist.count) { parmlist.count++; cc = strlen(parmname); ppname = new char[cc+1]; strTrim(ppname,parmname); parmlist.name[ii] = ppname; } parmlist.value[ii] = parmval; return parmlist.count; } // get parameter value from parameter name double getParm(cchar *parmname) { if (! parmlistvalid) zappcrash("parmlistvalid = 0"); for (int ii = 0; ii < parmlist.count; ii++) { if (strNeq(parmlist.name[ii],parmname)) continue; return parmlist.value[ii]; } return NAN; } // get Nth parameter name (zero-based) char * getParm(int Nth) { if (! parmlistvalid) zappcrash("parmlistvalid = 0"); if (Nth >= parmlist.count) return null; return parmlist.name[Nth]; } // list parameters in supplied text entry window int listParms(GtkWidget *textWin) { int ii; cchar *pname; double pvalue; for (ii = 0; ii < parmlist.count; ii++) { pname = getParm(ii); pvalue = getParm(pname); wprintf(textWin," %s %.12g \n",pname,pvalue); } return parmlist.count; } // edit parameters with a GUI // textWin != null enables button to list parameters in window // addp != 0 enables button to add new parameters // return: 0 if cancel, else parameter count int editParms(GtkWidget *textWin, int addp) { GtkWidget *peDialog, *peLabel[100], *peEdit[100], *peHbox[100]; GtkWidget *vbox; char ptemp[20], *pname; cchar *pchval; double pvalue; int ii, err, iie = -1, zstat, floaded = 0; int bcancel=1, bapply=2, bload=3, bsave=4, blist=5, baddp=6; if (! parmlistvalid) zappcrash("parmlistvalid = 0"); zthreadcrash(); // thread usage not allowed build_dialog: // build parameter edit dialog if (parmlist.count > 100) zappcrash("more than 100 parameters"); if (textWin && addp) peDialog = gtk_dialog_new_with_buttons (ZTX("edit parameters"), null, (GtkDialogFlags) 0, // non-modal ZTX("load\nfile"),bload, ZTX("save\nfile"),bsave, ZTX("list\nall"),blist, ZTX("add\nnew"),baddp, ZTX("cancel"),bcancel, ZTX("apply"),bapply, null); else if (textWin) peDialog = gtk_dialog_new_with_buttons (ZTX("edit parameters"), null, (GtkDialogFlags) 0, ZTX("load\nfile"),bload, ZTX("save\nfile"),bsave, ZTX("list\nall"),blist, ZTX("cancel"),bcancel, ZTX("apply"),bapply, null); else if (addp) peDialog = gtk_dialog_new_with_buttons (ZTX("edit parameters"), null, (GtkDialogFlags) 0, ZTX("load\nfile"),bload, ZTX("save\nfile"),bsave, ZTX("add\nnew"),baddp, ZTX("cancel"),bcancel, ZTX("apply"),bapply, null); else peDialog = gtk_dialog_new_with_buttons (ZTX("edit parameters"), null, (GtkDialogFlags) 0, ZTX("load\nfile"),bload, ZTX("save\nfile"),bsave, ZTX("cancel"),bcancel, ZTX("apply"),bapply, null); gtk_window_set_position(GTK_WINDOW(peDialog),GTK_WIN_POS_MOUSE); for (ii = 0; ii < parmlist.count; ii++) // labels and edit boxes side by side { // (parm names and parm values) peLabel[ii] = gtk_label_new(parmlist.name[ii]); gtk_misc_set_alignment(GTK_MISC(peLabel[ii]),1,0.5); gtk_label_set_width_chars(GTK_LABEL(peLabel[ii]),30); peEdit[ii] = gtk_entry_new(); gtk_entry_set_width_chars(GTK_ENTRY(peEdit[ii]),12); sprintf(ptemp,"%.12g",parmlist.value[ii]); gtk_entry_set_text(GTK_ENTRY(peEdit[ii]),ptemp); peHbox[ii] = gtk_box_new(HORIZONTAL,0); gtk_box_pack_start(GTK_BOX(peHbox[ii]),peLabel[ii],0,0,5); gtk_box_pack_start(GTK_BOX(peHbox[ii]),peEdit[ii],0,0,5); vbox = gtk_dialog_get_content_area(GTK_DIALOG(peDialog)); gtk_box_pack_start(GTK_BOX(vbox),peHbox[ii],1,1,2); } run_dialog: // display dialog and get inputs if (iie > -1) { gtk_editable_select_region(GTK_EDITABLE(peEdit[iie]),0,-1); // focus on new or bad parameter gtk_widget_grab_focus(peEdit[iie]); iie = -1; } gtk_widget_show_all(peDialog); zstat = gtk_dialog_run(GTK_DIALOG(peDialog)); if (zstat <= bcancel) // kill, cancel { if (floaded) { zstat = zmessageYN(null,ZTX("apply?")); // if file loaded, clarify if (! zstat) { gtk_widget_destroy(peDialog); return 0; } zstat = bapply; } } if (zstat == bload) // load from file { loadParms(); gtk_widget_destroy(peDialog); floaded = 1; goto build_dialog; } for (ii = 0; ii < parmlist.count; ii++) // capture inputs and check if OK { pchval = gtk_entry_get_text(GTK_ENTRY(peEdit[ii])); err = convSD(pchval,pvalue); if (err && iie < 0) iie = ii; // remember 1st error } if (iie >= 0) goto run_dialog; // re-get bad input if (zstat == bapply) // apply new values { for (ii = 0; ii < parmlist.count; ii++) // capture inputs and save them { pchval = gtk_entry_get_text(GTK_ENTRY(peEdit[ii])); err = convSD(pchval,parmlist.value[ii]); } gtk_widget_destroy(peDialog); // done return parmlist.count; } if (zstat == bsave) // save to file { for (ii = 0; ii < parmlist.count; ii++) // apply new values { pchval = gtk_entry_get_text(GTK_ENTRY(peEdit[ii])); err = convSD(pchval,parmlist.value[ii]); } saveParms(); floaded = 0; goto run_dialog; } if (zstat == blist) // list parameters { listParms(textWin); goto run_dialog; } if (zstat == baddp) // add parameter { // main dialog must be non-modal pname = zdialog_text(null,ZTX("add parameter"),ZTX("(new parm name)")); if (! pname) goto run_dialog; setParm(pname,0.0); free(pname); floaded = 1; iie = parmlist.count - 1; // focus on new parm gtk_widget_destroy(peDialog); goto build_dialog; } gtk_widget_destroy(peDialog); // unknown status return 0; } /************************************************************************** xstring class (dynamic length string) xstring(int cc = 0) default constructor xstring(cchar * ) string constructor xstring(const xstring &) copy constructor ~xstring() destructor operator cchar * () const { return xpp; } conversion operator (cchar *) xstring operator= (const xstring &) operator = xstring operator= (cchar *) operator = friend xstring operator+ (const xstring &, const xstring &) operator + (catenate) friend xstring operator+ (const xstring &, cchar *) operator + friend xstring operator+ (cchar *, const xstring &) operator + void insert(int pos, cchar *string, int cc = 0) insert substring at pos (expand) void overlay(int pos, cchar *string, int cc = 0) replace substring (possibly expand) static void getStats(int & tcount2, int & tmem2) get statistics void validate() const verify integrity int getcc() const { return xcc; } return string length ***************************************************************************/ #define wmiv 1648734981 int xstring::tcount = 0; // initz. static members int xstring::tmem = 0; xstring::xstring(int cc) // new xstring(cc) { wmi = wmiv; xmem = (cc & 0x7ffffff8) + 8; // mod 8 length xpp = new char[xmem]; // allocate if (! xpp) zappcrash("xstring NEW failure",null); tcount++; // incr. object count tmem += xmem; // incr. allocated memory xcc = 0; // string cc = 0 *xpp = 0; // string = null } xstring::xstring(cchar *string) // new xstring("initial string") { wmi = wmiv; xcc = 0; if (string) xcc = strlen(string); // string length xmem = (xcc & 0x7ffffff8) + 8; // mod 8 length xpp = new char[xmem]; // allocate if (! xpp) zappcrash("xstring NEW failure",null); tcount++; // incr. object count tmem += xmem; // incr. allocated memory *xpp = 0; if (xcc) strcpy(xpp,string); // copy string } xstring::xstring(const xstring & xstr) // new xstring2(xstring1) { wmi = wmiv; xmem = xstr.xmem; // allocate same length xcc = xstr.xcc; xpp = new char[xmem]; if (! xpp) zappcrash("xstring NEW failure",null); tcount++; // incr. object count tmem += xmem; // incr. allocated memory strcpy(xpp,xstr.xpp); // copy string } xstring::~xstring() // delete xstring { validate(); delete[] xpp; // release allocated memory xpp = 0; tcount--; // decr. object count tmem -= xmem; // decr. allocated memory if (tcount < 0) zappcrash("xstring count < 0",null); if (tmem < 0) zappcrash("xstring memory < 0",null); if (tcount == 0 && tmem > 0) zappcrash("xstring memory leak",null); } xstring xstring::operator= (const xstring & xstr) // xstring2 = xstring1 { validate(); xstr.validate(); if (this == &xstr) return *this; xcc = xstr.xcc; if (xmem < xcc+1) { delete[] xpp; // expand memory if needed tmem -= xmem; xmem = (xcc & 0x7ffffff8) + 8; // mod 8 length xpp = new char[xmem]; if (! xpp) zappcrash("xstring NEW failure",null); tmem += xmem; } strcpy(xpp,xstr.xpp); // copy string return *this; } xstring xstring::operator= (cchar *str) // xstring = "some string" { validate(); xcc = 0; *xpp = 0; if (str) xcc = strlen(str); if (xmem < xcc+1) { delete[] xpp; // expand memory if needed tmem -= xmem; xmem = (xcc & 0x7ffffff8) + 8; // mod 8 length xpp = new char[xmem]; if (! xpp) zappcrash("xstring NEW failure",null); tmem += xmem; } if (xcc) strcpy(xpp,str); // copy string return *this; } xstring operator+ (const xstring & x1, const xstring & x2) // xstring1 + xstring2 { x1.validate(); x2.validate(); xstring temp(x1.xcc + x2.xcc); // build temp xstring strcpy(temp.xpp,x1.xpp); // with both input strings strcpy(temp.xpp + x1.xcc, x2.xpp); temp.xcc = x1.xcc + x2.xcc; temp.validate(); return temp; } xstring operator+ (const xstring & x1, cchar *s2) // xstring + "some string" { x1.validate(); int cc2 = 0; if (s2) cc2 = strlen(s2); xstring temp(x1.xcc + cc2); // build temp xstring strcpy(temp.xpp,x1.xpp); // with both input strings if (s2) strcpy(temp.xpp + x1.xcc, s2); temp.xcc = x1.xcc + cc2; temp.validate(); return temp; } xstring operator+ (cchar *s1, const xstring & x2) // "some string" + xstring { x2.validate(); int cc1 = 0; if (s1) cc1 = strlen(s1); xstring temp(cc1 + x2.xcc); // build temp xstring if (s1) strcpy(temp.xpp,s1); // with both input strings strcpy(temp.xpp + cc1, x2.xpp); temp.xcc = cc1 + x2.xcc; temp.validate(); return temp; } void xstring::insert(int pos, cchar *string, int cc) // insert cc chars from string at pos { // pad if pos > xcc or cc > string validate(); int scc = strlen(string); if (! cc) cc = scc; int pad = pos - xcc; if (pad < 0) pad = 0; if (xmem < xcc + cc + pad + 1) // allocate more memory if needed { int newmem = xcc + cc + pad; newmem = (newmem & 0x7ffffff8) + 8; // mod 8 length char * xpp2 = new char[newmem]; if (! xpp2) zappcrash("xstring NEW failure",null); strcpy(xpp2,xpp); // copy to new space delete[] xpp; xpp = xpp2; tmem += newmem - xmem; xmem = newmem; } if (pad) memset(xpp+xcc,' ',pad); // add blanks up to pos for (int ii = xcc + pad; ii >= pos; ii--) // make hole for inserted string *(xpp+ii+cc) = *(xpp+ii); if (cc > scc) memset(xpp+pos+scc,' ',cc-scc); // blank pad if cc > string if (cc < scc) scc = cc; strncpy(xpp+pos,string,scc); // insert string, without null xcc += cc + pad; // set new length xpp[xcc] = 0; validate(); } void xstring::overlay(int pos, cchar *string, int cc) // overlay substring { validate(); int scc = strlen(string); if (! cc) cc = scc; if (xmem < pos + cc + 1) // allocate more memory if needed { int newmem = pos + cc; newmem = (newmem & 0x7ffffff8) + 8; // mod 8 length char * xpp2 = new char[newmem]; if (! xpp2) zappcrash("xstring NEW failure",null); strcpy(xpp2,xpp); // copy to new space delete[] xpp; xpp = xpp2; tmem += newmem - xmem; xmem = newmem; } if (pos > xcc) memset(xpp+xcc,' ',pos-xcc); // add blanks up to pos if (cc > scc) memset(xpp+pos+scc,' ',cc-scc); // blank pad if cc > string if (cc < scc) scc = cc; strncpy(xpp+pos,string,scc); // insert string, without null if (pos + cc > xcc) xcc = pos + cc; // set new length xpp[xcc] = 0; validate(); } void xstring::getStats(int & tcount2, int & tmem2) // get statistics { tcount2 = tcount; tmem2 = tmem; } void xstring::validate() const // validate integrity { if (wmi != wmiv) zappcrash("xstring bad wmi",null); if (xmem < xcc+1) zappcrash("xstring xmem < xcc+1",null); if (xcc != (int) strlen(xpp)) zappcrash("xstring xcc != strlen(xpp)",null); } /************************************************************************** Vxstring class (array or vector of xstring) Vxstring(int = 0); constructor ~Vxstring(); destructor Vxstring(const Vxstring &); copy constructor Vxstring operator= (const Vxstring &); operator = xstring & operator[] (int); operator [] const xstring & operator[] (int) const; operator [] (const) int search(cchar *string); find element in unsorted Vxstring int bsearch(cchar *string); find element in sorted Vxstring int sort(int nkeys, int keys[][3]); sort elements by designated subfields int sort(int pos = 0, int cc = 0); sort elements by 1 subfield (cc 0 = all) int getCount() const { return nd; } get current count Sort with keys: keys[N][0] = key position (0 based) of key N keys[N][1] = key length keys[N][2] = sort type: 1/2 = ascending/descending, 3/4 = same, ignoring case ***************************************************************************/ Vxstring::Vxstring(int ii) // constructor { pdata = 0; nd = ii; if (nd) pdata = new xstring[nd]; if (nd && !pdata) zappcrash("Vxstring NEW fail",null); } Vxstring::~Vxstring() // destructor { if (nd) delete[] pdata; pdata = 0; nd = 0; } Vxstring::Vxstring(const Vxstring & pold) // copy constructor { pdata = 0; nd = pold.nd; // set size if (nd) pdata = new xstring[nd]; // allocate memory if (nd && !pdata) zappcrash("Vxstring NEW fail"); for (int ii = 0; ii < nd; ii++) pdata[ii] = pold[ii]; // copy defined elements } Vxstring Vxstring::operator= (const Vxstring & vdstr) // operator = { if (nd) delete[] pdata; // delete old memory pdata = 0; nd = vdstr.nd; if (nd) pdata = new xstring[nd]; // allocate new memory if (nd && !pdata) zappcrash("Vxstring NEW fail",null); for (int ii = 0; ii < nd; ii++) pdata[ii] = vdstr.pdata[ii]; // copy elements return *this; } xstring & Vxstring::operator[] (int ii) // operator [] { static xstring xnull(0); if (ii < nd) return pdata[ii]; // return reference zappcrash("Vxstring index invalid %d %d",nd,ii,null); return xnull; } const xstring & Vxstring::operator[] (int ii) const // operator [] { static xstring xnull(0); if (ii < nd) return pdata[ii]; // return reference zappcrash("Vxstring index invalid %d %d",nd,ii,null); return xnull; } int Vxstring::search(cchar *string) // find element in unsorted Vxstring { for (int ii = 0; ii < nd; ii++) if (strEqu(pdata[ii],string)) return ii; return -1; } int Vxstring::bsearch(cchar *string) // find element in sorted Vxstring { // (binary search) int nn, ii, jj, kk, rkk; nn = nd; if (! nn) return 0; // empty list ii = nn / 2; // next element to search jj = (ii + 1) / 2; // next increment nn--; // last element rkk = 0; while (1) { kk = strcmp(pdata[ii],string); // check element if (kk > 0) { ii -= jj; // too high, go down if (ii < 0) return -1; } else if (kk < 0) { ii += jj; // too low, go up if (ii > nn) return -1; } else if (kk == 0) return ii; // matched jj = jj / 2; // reduce increment if (jj == 0) { jj = 1; // step by 1 element if (! rkk) rkk = kk; // save direction else { if (rkk > 0) { if (kk < 0) return -1; } // if change direction, fail else if (kk > 0) return -1; } } } } static int VDsortKeys[10][3], VDsortNK; int Vxstring::sort(int NK, int keys[][3]) // sort elements by subfields { // key[ii][0] = position int NR, RL, ii; // [1] = length HeapSortUcomp VDsortComp; // [2] = 1/2 = ascending/desc. // = 3/4 = + ignore case NR = nd; if (NR < 2) return 1; RL = sizeof(xstring); if (NK < 1) zappcrash("Vxstring::sort, bad NK",null); if (NK > 10) zappcrash("Vxstring::sort, bad NK",null); VDsortNK = NK; for (ii = 0; ii < NK; ii++) { VDsortKeys[ii][0] = keys[ii][0]; VDsortKeys[ii][1] = keys[ii][1]; VDsortKeys[ii][2] = keys[ii][2]; } HeapSort((char *) pdata,RL,NR,VDsortComp); return 1; } int VDsortComp(cchar *r1, cchar *r2) { xstring *d1, *d2; cchar *p1, *p2; int ii, stat, kpos, ktype, kleng; d1 = (xstring *) r1; d2 = (xstring *) r2; p1 = *d1; p2 = *d2; for (ii = 0; ii < VDsortNK; ii++) // compare each key { kpos = VDsortKeys[ii][0]; kleng = VDsortKeys[ii][1]; ktype = VDsortKeys[ii][2]; if (ktype == 1) { stat = strncmp(p1+kpos,p2+kpos,kleng); if (stat) return stat; continue; } else if (ktype == 2) { stat = strncmp(p1+kpos,p2+kpos,kleng); if (stat) return -stat; continue; } else if (ktype == 3) { stat = strncasecmp(p1+kpos,p2+kpos,kleng); if (stat) return stat; continue; } else if (ktype == 4) { stat = strncasecmp(p1+kpos,p2+kpos,kleng); if (stat) return -stat; continue; } zappcrash("Vxstring::sort, bad KEYS sort type",null); } return 0; } int Vxstring::sort(int pos, int cc) // sort elements ascending { int key[3]; if (! cc) cc = 999999; key[0] = pos; key[1] = cc; key[2] = 1; sort(1,&key); return 1; } /************************************************************************** Hash Table class HashTab(int cc, int cap); constructor ~HashTab(); destructor int Add(cchar *string); add a new string int Del(cchar *string); delete a string int Find(cchar *string); find a string int GetCount() { return count; } get string count int GetNext(int & first, char *string); get first/next string int Dump(); dump hash table to std. output constructor: cc = string length of table entries, cap = table capacity cap should be set 30% higher than needed to reduce collisions and improve speed Benchmark: 0.056 usec. to find 19 char string in a table of 100,000 which is 80% full. 3.3 GHz Core i5 ***************************************************************************/ // static members (robust for tables up to 60% full) int HashTab::trys1 = 100; // Add() tries int HashTab::trys2 = 200; // Find() tries HashTab::HashTab(int _cc, int _cap) // constructor { cc = 4 * (_cc + 4) / 4; // + 1 + mod 4 length cap = _cap; int len = cc * cap; table = new char [len]; if (! table) zappcrash("HashTab() new %d fail",len,null); memset(table,0,len); } HashTab::~HashTab() // destructor { delete [] table; table = 0; } // Add a new string to table int HashTab::Add(cchar *string) { int pos, fpos, trys; pos = strHash(string,cap); // get random position pos = pos * cc; for (trys = 0, fpos = -1; trys < trys1; trys++, pos += cc) // find next free slot at/after position { if (pos >= cap * cc) pos = 0; // last position wraps to 1st if (! table[pos]) // empty slot: string not found { if (fpos != -1) pos = fpos; // use prior deleted slot if there strncpy(table+pos,string,cc); // insert new string table[pos+cc-1] = 0; // insure null terminator return (pos/cc); // return rel. table entry } if (table[pos] == -1) // deleted slot { if (fpos == -1) fpos = pos; // remember 1st one found continue; } if (strEqu(string,table+pos)) return -2; // string already present } return -3; // table full (trys1 exceeded) } // Delete a string from table int HashTab::Del(cchar *string) { int pos, trys; pos = strHash(string,cap); // get random position pos = pos * cc; for (trys = 0; trys < trys2; trys++, pos += cc) // search for string at/after position { if (pos >= cap * cc) pos = 0; // last position wraps to 1st if (! table[pos]) return -1; // empty slot, string not found if (strEqu(string,table+pos)) // string found { table[pos] = -1; // delete table entry return (pos/cc); // return rel. table entry } } zappcrash("HashTab::Del() bug",null); // exceed trys2, must not happen return 0; // (table too full to function) } // Find a table entry. int HashTab::Find(cchar *string) { int pos, trys; pos = strHash(string,cap); // get random position pos = pos * cc; for (trys = 0; trys < trys2; trys++, pos += cc) // search for string at/after position { if (pos >= cap * cc) pos = 0; // last position wraps to 1st if (! table[pos]) return -1; // empty slot, string not found if (strEqu(string,table+pos)) return (pos/cc); // string found, return rel. entry } zappcrash("HashTab::Find() bug",null); // cannot happen return 0; } // return first or next table entry int HashTab::GetNext(int & ftf, char *string) { static int pos; if (ftf) // initial call { pos = 0; ftf = 0; } while (pos < (cap * cc)) { if ((table[pos] == 0) || (table[pos] == -1)) // empty or deleted slot { pos += cc; continue; } strcpy(string,table+pos); // return string pos += cc; return 1; } return -4; // EOF } int HashTab::Dump() { int ii, pos; for (ii = 0; ii < cap; ii++) { pos = ii * cc; if (table[pos] && table[pos] != -1) printz("%d, %s \n", ii, table + pos); if (table[pos] == -1) printz("%d, deleted \n", pos); } return 1; } /************************************************************************** class for queue of dynamic strings Queue(int cap); create queue with capacity ~Queue(); destroy queue int getCount(); get current entry count int push(const xstring *entry, double secs); add new entry with max. wait time xstring *pop1(); get 1st entry (oldest) xstring *popN(); get Nth entry (newest) constructor: cap is queue capacity push: secs is max. time to wait if queue is full. This makes sense if the queue is being pop'd from another thread. Use zero otherwise. Execution time: 0.48 microsecs per push + pop on queue with 100 slots kept full. (2.67 GHz Intel Core i7) ***************************************************************************/ Queue::Queue(int cap) // constructor { int err; err = mutex_init(&qmutex, 0); // create mutex = queue lock if (err) zappcrash("Queue(), mutex init fail",null); qcap = cap; // queue capacity ent1 = entN = qcount = 0; // state = empty vd = new Vxstring(qcap); // create vector of xstring's if (! vd) zappcrash("Queue(), NEW fail %d",cap,null); strcpy(wmi,"queue"); return; } Queue::~Queue() // destructor { if (strNeq(wmi,"queue")) zappcrash("~Queue wmi fail",null); wmi[0] = 0; mutex_destroy(&qmutex); // destroy mutex qcount = qcap = ent1 = entN = -1; delete vd; vd = 0; return; } void Queue::lock() // lock queue (private) { int err; err = mutex_lock(&qmutex); // reserve mutex or suspend if (err) zappcrash("Queue mutex lock fail",null); return; } void Queue::unlock() // unlock queue (private) { int err; err = mutex_unlock(&qmutex); // release mutex if (err) zappcrash("Queue mutex unlock fail",null); return; } int Queue::getCount() // get current entry count { if (strNeq(wmi,"queue")) zappcrash("Queue getCount wmi fail",null); return qcount; } int Queue::push(const xstring *newEnt, double wait) // add entry to queue, with max. wait { double elaps = 0.0; int count; if (strNeq(wmi,"queue")) zappcrash("Queue::push wmi fail",null); lock(); // lock queue while (qcount == qcap) { // queue full unlock(); // unlock queue if (elaps >= wait) return -1; // too long, return -1 status usleep(1000); // sleep in 1 millisec. steps elaps += 0.001; // until queue not full lock(); // lock queue } (* vd)[entN] = *newEnt; // copy new entry into queue entN++; // incr. end pointer if (entN == qcap) entN = 0; qcount++; // incr. queue count count = qcount; unlock(); // unlock queue return count; // return curr. queue count } xstring *Queue::pop1() // get 1st (oldest) entry and remove { xstring *entry; if (strNeq(wmi,"queue")) zappcrash("Queue::pop1 wmi fail",null); lock(); // lock queue if (qcount == 0) entry = 0; // queue empty else { entry = &(* vd)[ent1]; // get first entry ent1++; // index pointer to next if (ent1 == qcap) ent1 = 0; qcount--; // decr. queue count } unlock(); // unlock queue return entry; } xstring *Queue::popN() // get last (newest) entry and remove { xstring *entry; if (strNeq(wmi,"queue")) zappcrash("Queue::popN wmi fail",null); lock(); // lock queue if (qcount == 0) entry = 0; // queue empty else { if (entN == 0) entN = qcap; // index pointer to prior entN--; qcount--; // decr. queue count entry = &(* vd)[entN]; // get last entry } unlock(); // unlock queue return entry; } /************************************************************************** Tree class, tree-structured data storage without limits Store any amount of data at any depth within a tree-structure with named nodes. Data can be found using an ordered list of node names or node numbers. Tree(cchar *name); create Tree ~Tree(); destroy Tree int put(void *data, int dd, char *nodes[], int nn); put data by node names[] int put(void *data, int dd, int nodes[], int nn); put data by node numbers[] int get(void *data, int dd, char *nodes[], int nn); get data by node names[] int get(void *data, int dd, int nodes[], int nn); get data by node numbers[] A Tree can also be thought of as an N-dimensional array with the cells or nodes having both names and numbers. Data can be stored and retrieved with a list of node names or numbers. The nodes are created as needed. Nodes are sparse: those with no data do not exist. Node numbers are created when data is stored by node numbers. Node numbers are also added when data is stored by node names: the numbers are assigned sequentially from zero at each level in the tree. nodes array of node names or numbers nn no. of nodes used for a put() or get() call dd data length to put, or the max. data length to get (i.e. the space available) put() returns 1 if successful and crashes with a message if not (out of memory) get() returns the length of the data retrieved (<= dd) or 0 if not found there is no assumption that the data is character data and no null is appended data returned has the same length as the data stored (if dd arg is big enough) example: char *snodes[10]; // up to 10 node names (max tree depth) int knodes[10]; // up to 10 node numbers char mydata[20]; // data length up to 20 Tree *mytree = new Tree("myname"); // create Tree snodes[0] = "name1"; snodes[1] = "name2"; mytree->put("string1",8,snodes,2); // put "string1" at ["name1","name2"] snodes[1] = "name3"; mytree->put("string22",9,snodes,2); // put "string22" at ["name1","name3"] snodes[1] = "name2"; mytree->get(mydata,20,snodes,2); // get data at ["name1","name2"] ("string1") knodes[0] = 0; knodes[1] = 0; mytree->get(mydata,20,knodes,2); // get data at [0,0] ("string1") knodes[1] = 1; mytree->get(mydata,20,knodes,2); // get data at [0,1] ("string22") When data was stored at ["name1","name2"] these node names were created along with the corresponding node numbers [0,0]. When data was stored at ["name1","name3"] a new node "name3" was created under the existing node "name1", and assigned the numbers [0,1]. Benchmark Execution times: 2.67 GHz Intel Core i7 Tree with 1 million nodes and average depth of 8 levels (peak 15 levels) put() all data by node names: 2.1 secs get() all data by node names: 1.5 secs put() all data by node numbers: 2.0 secs get() all data by node numbers: 0.72 secs Internal code conventions: - caller level is node 0, next level is node 1, etc. - node names and numbers in calls to get() and put() refer to next levels - number of levels = 1+nn, where nn is max. in calls to put(...nodes[], nn) ***************************************************************************/ #define wmid 1374602859 // integrity check key // constructor Tree::Tree(cchar *name) { wmi = wmid; tname = 0; tmem = 0; tdata = 0; nsub = 0; psub = 0; if (name) { int cc = strlen(name); tname = new char[cc+1]; if (! tname) zappcrash("Tree, no memory",null); strcpy(tname,name); } } // destructor Tree::~Tree() { if (wmi != wmid) zappcrash("not a Tree",null); if (tname) delete [] tname; tname = 0; if (tmem) free(tdata); tmem = 0; tdata = 0; for (int ii = 0; ii < nsub; ii++) delete psub[ii]; if (psub) free(psub); nsub = 0; psub = 0; } // put data by node names[] int Tree::put(void *data, int dd, char *nodes[], int nn) { Tree *tnode; if (wmi != wmid) zappcrash("not a Tree",null); tnode = make(nodes,nn); if (tnode->tdata) free(tnode->tdata); tnode->tdata = new char[dd]; if (! tnode->tdata) zappcrash("Tree, no memory",null); tnode->tmem = dd; memmove(tnode->tdata,data,dd); return 1; } // put data by node numbers[] int Tree::put(void *data, int dd, int nodes[], int nn) { Tree *tnode; if (wmi != wmid) zappcrash("not a Tree",null); tnode = make(nodes,nn); if (tnode->tdata) free(tnode->tdata); tnode->tdata = new char[dd]; if (! tnode->tdata) zappcrash("Tree, no memory",null); tnode->tmem = dd; memmove(tnode->tdata,data,dd); return 1; } // get data by node names[] int Tree::get(void *data, int dd, char *nodes[], int nn) { Tree *tnode = find(nodes,nn); if (! tnode) return 0; if (! tnode->tmem) return 0; if (dd > tnode->tmem) dd = tnode->tmem; memmove(data,tnode->tdata,dd); return dd; } // get data by node numbers[] int Tree::get(void *data, int dd, int nodes[], int nn) { Tree *tnode = find(nodes,nn); if (! tnode) return 0; if (! tnode->tmem) return 0; if (dd > tnode->tmem) dd = tnode->tmem; memmove(data,tnode->tdata,dd); return dd; } // find a given node by names[] Tree * Tree::find(char *nodes[], int nn) { int ii; for (ii = 0; ii < nsub; ii++) if (psub[ii]->tname && strEqu(nodes[0],psub[ii]->tname)) break; if (ii == nsub) return 0; if (nn == 1) return psub[ii]; return psub[ii]->find(&nodes[1],nn-1); } // find a given node by numbers[] Tree * Tree::find(int nodes[], int nn) { int ii = nodes[0]; if (ii >= nsub) return 0; if (! psub[ii]) return 0; if (nn == 1) return psub[ii]; return psub[ii]->find(&nodes[1],nn-1); } // find or create a given node by names[] Tree * Tree::make(char *nodes[], int nn) { int ii; Tree **psub2; for (ii = 0; ii < nsub; ii++) if (psub[ii]->tname && strEqu(nodes[0],psub[ii]->tname)) break; if (ii == nsub) { psub2 = new Tree * [nsub+1]; if (! psub2) zappcrash("Tree, no memory",null); for (ii = 0; ii < nsub; ii++) psub2[ii] = psub[ii]; delete [] psub; psub = psub2; nsub++; psub[ii] = new Tree(nodes[0]); if (! psub[ii]) zappcrash("Tree, no memory",null); } if (nn == 1) return psub[ii]; return psub[ii]->make(&nodes[1],nn-1); } // find or create a given node by numbers[] Tree * Tree::make(int nodes[], int nn) { Tree **psub2; int ii, jj; ii = nodes[0]; if ((ii < nsub) && psub[ii]) { if (nn == 1) return psub[ii]; return psub[ii]->make(&nodes[1],nn-1); } if (ii >= nsub) { psub2 = new Tree * [ii+1]; if (psub2 == null) zappcrash("Tree, no memory",null); for (jj = 0; jj < nsub; jj++) psub2[jj] = psub[jj]; for (jj = nsub; jj < ii; jj++) psub2[jj] = 0; delete [] psub; psub = psub2; nsub = ii + 1; } psub[ii] = new Tree("noname"); if (! psub[ii]) zappcrash("Tree, no memory",null); if (nn == 1) return psub[ii]; return psub[ii]->make(&nodes[1],nn-1); } // dump tree data to stdout (call with level 0) void Tree::dump(int level) { cchar *name; if (! tname) name = "noname"; else name = tname; printz("%*s level: %d name: %s subs: %d mem: %d \n", level*2,"",level,name,nsub,tmem); for (int ii = 0; ii < nsub; ii++) if (psub[ii]) psub[ii]->dump(level+1); } // get node counts and total data per level // level 0 + nn more levels, as given in calls to put(...nodes[],nn) // caller must initialize counters to zero void Tree::stats(int nn[], int nd[]) { nn[0] += 1; nd[0] += tmem; for (int ii = 0; ii < nsub; ii++) if (psub[ii]) psub[ii]->stats(&nn[1],&nd[1]); } ukopp-4.9/Makefile0000644000175000017500000000406312333076512012631 0ustar micomico# ukopp makefile PROGRAM = ukopp VERSION = 4.9 SOURCE = $(PROGRAM)-$(VERSION).cc # defaults for parameters that may be pre-defined CXXFLAGS ?= -O2 -Wall -ggdb LDFLAGS ?= -rdynamic -lpthread PREFIX ?= /usr # target install directories BINDIR = $(PREFIX)/bin SHAREDIR = $(PREFIX)/share/$(PROGRAM) DATADIR = $(SHAREDIR)/data ICONDIR = $(SHAREDIR)/icons DOCDIR = $(PREFIX)/share/doc/$(PROGRAM) MANDIR = $(PREFIX)/share/man/man1 MENUFILE = $(PREFIX)/share/applications/$(PROGRAM).desktop CFLAGS = $(CXXFLAGS) -c `pkg-config --cflags gtk+-3.0` LIBS = `pkg-config --libs gtk+-3.0` -lpthread $(PROGRAM): $(PROGRAM).o zfuncs.o $(CXX) $(LDFLAGS) $(PROGRAM).o zfuncs.o $(LIBS) -o $(PROGRAM) $(PROGRAM).o: $(SOURCE) $(CXX) $(CFLAGS) -o $(PROGRAM).o $(SOURCE) zfuncs.o: zfuncs.cc zfuncs.h $(CXX) $(CFLAGS) zfuncs.cc \ -D PREFIX=\"$(PREFIX)\" -D DOCDIR=\"$(DOCDIR)\" install: $(PROGRAM) rm -f $(DESTDIR)$(BINDIR)/$(PROGRAM) rm -f -R $(DESTDIR)$(SHAREDIR) rm -f -R $(DESTDIR)$(DOCDIR) rm -f $(DESTDIR)$(MANDIR)/$(PROGRAM).1.gz xdg-desktop-menu uninstall $(DESTDIR)$(MENUFILE) rm -f $(DESTDIR)$(MENUFILE) mkdir -p $(DESTDIR)$(BINDIR) mkdir -p $(DESTDIR)$(DATADIR) mkdir -p $(DESTDIR)$(ICONDIR) mkdir -p $(DESTDIR)$(DOCDIR) mkdir -p $(DESTDIR)$(MANDIR) mkdir -p $(DESTDIR)$(PREFIX)/share/applications cp -f $(PROGRAM) $(DESTDIR)$(BINDIR) cp -f -R data/* $(DESTDIR)$(DATADIR) cp -f -R icons/* $(DESTDIR)$(ICONDIR) cp -f -R doc/* $(DESTDIR)$(DOCDIR) gzip -f -9 $(DESTDIR)$(DOCDIR)/changelog # man page cp -f doc/$(PROGRAM).man $(PROGRAM).1 gzip -f -9 $(PROGRAM).1 cp $(PROGRAM).1.gz $(DESTDIR)$(MANDIR) rm -f $(PROGRAM).1.gz # menu (desktop) file cp -f desktop $(DESTDIR)$(MENUFILE) chmod +x $(DESTDIR)$(MENUFILE) xdg-desktop-menu install --novendor $(DESTDIR)$(MENUFILE) uninstall: rm -f $(DESTDIR)$(BINDIR)/$(PROGRAM) rm -f -R $(DESTDIR)$(SHAREDIR) rm -f -R $(DESTDIR)$(DOCDIR) rm -f $(DESTDIR)$(MANDIR)/$(PROGRAM).1.gz xdg-desktop-menu uninstall $(DESTDIR)$(MENUFILE) rm -f $(DESTDIR)$(MENUFILE) clean: rm -f $(PROGRAM) rm -f *.o ukopp-4.9/ukopp-4.9.cc0000644000175000017500000053533412333076511013157 0ustar micomico/************************************************************************** ukopp - disk to disk backup and restore program Copyright 2007-2014 Michael Cornelison source URL: kornelix.com contact: kornelix@posteo.de This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ***************************************************************************/ #include #include #include "zfuncs.h" #define ukopp_title "ukopp v.4.9" // version #define ukopp_license "GNU General Public License v.3" // parameters and limits #define normfont "monospace 8" #define boldfont "monospace bold 8" #define BIOCC 512*1024 // read and write I/O buffer size #define maxnx 200 // max include/exclude in job file #define maxfs 500000 // max disk files, 500K v.4.4 #define MODTIMETOLR 2.0 // tolerance for "equal" mod times v.4.1 #define nano 0.000000001 // nanosecond #define mega (1024*1024) // computer million #define VSEP1 " (" // file version appendage format: #define VSEP2 ")" // /xxxx.../filename (nnn) #define RSEP1 " (" // file retention appendage format: #define RSEP2 ")" // /xxxx.../filename (nn,nn) // special control files in backup directory #define BD_UKOPPDIRK "/ukopp-data" // directory for special files #define BD_POOPFILE "/ukopp-data/poopfile" // file owner & permissions file #define BD_JOBFILE "/ukopp-data/jobfile" // backup job file #define BD_DATETIME "/ukopp-data/datetime" // backup date-time file // GTK GUI widgets GtkWidget *mWin, *mVbox, *mScroll, *mLog; // main window GtkWidget *fc_widget; // file-chooser dialog widget GtkWidget *editwidget; // edit box in file selection dialogs GdkCursor *watchcursor; GdkWindow *mLogwin; PangoFontDescription *monofont; // fixed-width font // file scope variables int main_argc; // command line args char **main_argv; int killFlag; // tell function to quit int pauseFlag; // tell function to pause/resume int menuLock; // menu lock flag int Fgui; // flag, GUI mode or not int clrun; // flag, command line 'run' command char TFbakfiles[100]; // /home/user/.ukopp/xxx temp. files char TFjobfile[100], TFpoopfile[100]; char TFdatetime[100], TFformatscript[100]; // disk devices and mount points char diskdev[100][40]; // /dev/xxx char diskdesc[100][60]; // device description char diskmp[100][60]; // mount point, /media/xxxx int Ndisk, maxdisk = 99; // max. disks / partitions int devMounted = 0; // backup device mounted status int ukoppMounted = 0; // device was mounted by me int ukoppMpoint = 0; // mount point was made by me char mountdev[40]; // current mount data char mountdirk[200]; // backup job data char BJfilespec[maxfcc]; // backup job file int BJnnx; // filespec count, 0...maxnx int BJrtype[maxnx]; // 1/2/3 = comment/include/exclude char *BJfspec[maxnx]; // filespec (wild) int BJretND[maxnx]; // retention days int BJretNV[maxnx]; // retention versions int BJfiles[maxnx]; // count of matching disk files double BJbytes[maxnx]; // matching files byte count int BJvmode; // 0/1/2/3 = none/incr/full/comp char BJdev[40] = ""; // backup device (maybe) char BJdirk[200] = ""; // backup target directory int BJdcc; // target directory cc int BJvalid = 0; // backup job valid flag int BJedited = 0; // job edited and not saved const char *vertype[4] = { "none","incremental","full","compare" }; // verify types // disk files specified in backup job struct dfrec { // disk file record char *file; // file: /directory.../filename double size; // byte count double mtime; // mod time int err; // fstat() status int jindx; // index to job data BJfspec[] etc. int bindx; // index to backup files Brec[] int finc; // included in curr. backup char disp; // status: new mod unch }; int Dnf; // actual file count < maxfs double Dbytes; // disk files, total bytes dfrec Drec[maxfs]; // disk file data records // backup files (copies at backup location) struct bfrec { // backup file record char *file; // file: /directory.../filename double size; // byte count double mtime; // mod time int err; // file fstat() status int retND; // retention days int retNV; // retention versions int lover, hiver; // range of previous versions int nexpv; // no. expired versions int finc; // included in curr. backup char disp; // file status: del mod unch }; int Bnf; // actual file count < maxfs double Bbytes; // backup files, total bytes bfrec Brec[maxfs]; // backup file data records // backup file statistics: int Cfiles; // curr. version file count double Cbytes; // and total bytes int Vfiles; // prior version file count double Vbytes; // and total bytes int Pfiles; // expired prior versions double Pbytes; // and total bytes int fverrs, fcerrs; // file verify and compare errors // disk::backup comparison data int nnew, ndel, nmod, nunc; // new, del, mod, unch file counts int Mfiles; // new + mod + del file count double Mbytes; // new + mod files, total bytes // restore job data char RJfrom[300]; // restore copy-from: /directory/.../ char RJto[300]; // restore copy-to: /directory/.../ int RJnnx; // filespec count, 0...maxnx int RJrtype[maxnx]; // record type: include/exclude char *RJfspec[maxnx]; // filespec of include/exclude int RJvalid; // restore job valid flag // restore file data struct rfrec { // restore file record char *file; // restore filespec: /directory.../file int finc; // flag, file restore was done }; rfrec Rrec[maxfs]; // restore file data records int Rnf; // actual file count < maxfs // ukopp functions int initfunc(void *data); // GTK init function void buttonfunc(GtkWidget *, cchar *menu); // process toolbar button event void menufunc(GtkWidget *, cchar *menu); // process menu select event int getroot(cchar *); // get root privileges int quit_ukopp(cchar *); // exit application int clearScreen(cchar *); // clear logging window int signalFunc(cchar *); // kill/pause/resume curr. function int checkKillPause(); // test flags: killFlag and pauseFlag int BDpoop(); // get all devices and mount points int chooseTarget(cchar *); // choose device and mount point int BJfileOpen(cchar *); // job file open dialog int BJfileSave(cchar *); // job file save dialog int BJload(cchar *fspec); // backup job data <<< file int BJstore(cchar *fspec); // backup job data >>> file int BJlist(cchar *); // backup job >>> log window int BJedit(cchar *); // backup job edit dialog cchar * parseNXrec(cchar *, int &, char *&, int &, int &); // parse include/exclude record cchar * parseTarget(cchar *); // parse target record cchar * parseVerify(cchar *); // parse verify record int Backup(cchar *); // backup function int Synch(cchar *); // synchronize function int Verify(cchar *); // verify functions int Report(cchar *); // report functions int RJedit(cchar *); // restore job edit dialog int RJlist(cchar *); // list backup files to be restored int Restore(cchar *); // file restore function int Format(cchar *); // format disk function int helpFunc(cchar *); // help function int mount(cchar *); // mount target device int unmount(cchar *); // unmount target device int saveScreen(cchar *); // save logging window to file int writeDT(); // write date-time to temp file int synch_poop(const char *mode); // synch owner and permissions data int dGetFiles(); // generate backup files from job data int bGetFiles(); // get backup file list int rGetFiles(); // generate file list from restore job int setFileDisps(); // set file disps: new del mod unch int SortFileList(char *recs, int RL, int NR, char sort); // sort file list in memory int filecomp(cchar *file1, cchar *file2); // compare files, directories first int BJreset(); // reset backup job file data int RJreset(); // reset restore job data int dFilesReset(); // reset disk file data and free memory int bFilesReset(); // reset backup file data, free memory int rFilesReset(); // reset restore file data, free memory cchar * copyFile(cchar *file1, cchar *file2, int mpf); // copy backup file << >> disk file cchar * checkFile(cchar *file, int compf, double &bcc); // validate file and return length cchar * setnextVersion(bfrec &rec); // backup file: assign next version cchar * purgeVersions(bfrec &rec, int fkeep); // backup file: delete expired vers. cchar * deleteFile(cchar *file); // delete backup file int setFileVersion(char *file, int vers); // (re)set filespec version in memory int do_shell(cchar *pname, cchar *command); // do shell command and echo outputs // ukopp menu table struct menuent { char menu1[20], menu2[40]; // top-menu, sub-menu int lock; // lock funcs: no run parallel int (*mfunc)(cchar *); // processing function }; #define nmenu 40 struct menuent menus[nmenu] = { // top-menu sub-menu lock menu-function { "button", "root", 1, getroot }, { "button", "edit job", 1, BJedit }, { "button", "target", 1, chooseTarget }, { "button", "clear", 0, clearScreen }, { "button", "run job", 1, Backup }, { "button", "mount", 1, mount }, { "button", "unmount", 1, unmount }, { "button", "pause", 0, signalFunc }, { "button", "resume", 0, signalFunc }, { "button", "kill job", 0, signalFunc }, { "button", "quit", 0, quit_ukopp }, { "File", "open job", 1, BJfileOpen }, { "File", "edit job", 1, BJedit }, { "File", "list job", 0, BJlist }, { "File", "save job", 0, BJfileSave }, { "File", "save job as", 0, BJfileSave }, { "File", "run job", 1, Backup }, { "File", "quit", 0, quit_ukopp }, { "Backup", "backup only", 1, Backup }, { "Backup", "synchronize", 1, Synch }, { "Verify", "incremental", 1, Verify }, { "Verify", "full", 1, Verify }, { "Verify", "compare", 1, Verify }, { "Report", "get disk files", 1, Report }, { "Report", "diffs summary", 1, Report }, { "Report", "diffs by directory", 1, Report }, { "Report", "diffs by file status", 1, Report }, { "Report", "diffs by file", 1, Report }, { "Report", "file versions", 1, Report }, { "Report", "expired versions", 1, Report }, { "Report", "list disk files", 1, Report }, { "Report", "list backup files", 1, Report }, { "Report", "find files", 1, Report }, { "Report", "save screen", 0, saveScreen }, { "Restore", "setup restore job", 1, RJedit }, { "Restore", "list restore files", 1, RJlist }, { "Restore", "restore files", 1, Restore }, { "Format", "format device", 1, Format }, { "Help", "about", 0, helpFunc }, { "Help", "contents", 0, helpFunc } }; // ukopp main program int main(int argc, char *argv[]) { GtkWidget *mbar, *tbar; GtkWidget *mFile, *mBackup, *mVerify, *mReport, *mRestore; GtkWidget *mFormat, *mHelp; int ii; zinitapp("ukopp",null); // setup app directories clrun = 0; // no command line run command *BJfilespec = 0; // no backup job file Fgui = 1; // assume GUI mode main_argc = argc; // save command line arguments main_argv = argv; for (ii = 1; ii < argc; ii++) // process command line { if (strEqu(argv[ii],"-nogui")) Fgui = 0; // command line mode, no GUI else if (strEqu(argv[ii],"-job") && argc > ii+1) // -job jobfile (load only) strcpy(BJfilespec,argv[++ii]); else if (strEqu(argv[ii],"-run") && argc > ii+1) // -run jobfile (load and run) { strcpy(BJfilespec,argv[++ii]); clrun++; } else strcpy(BJfilespec,argv[ii]); // assume a job file and load it } if (! Fgui) { // no GUI v.3.6 mLog = mWin = 0; // outputs go to STDOUT initfunc(0); // run job if (devMounted && ukoppMounted) unmount(0); return 0; // exit } gtk_init(&argc, &argv); // GTK command line options mWin = gtk_window_new(GTK_WINDOW_TOPLEVEL); // create main window gtk_window_set_title(GTK_WINDOW(mWin),ukopp_title); gtk_window_set_position(GTK_WINDOW(mWin),GTK_WIN_POS_CENTER); gtk_window_set_default_size(GTK_WINDOW(mWin),800,500); mVbox = gtk_box_new(VERTICAL,0); // vertical packing box gtk_container_add(GTK_CONTAINER(mWin),mVbox); // add to main window mScroll = gtk_scrolled_window_new(0,0); // scrolled window gtk_box_pack_end(GTK_BOX(mVbox),mScroll,1,1,0); // add to main window mVbox mLog = gtk_text_view_new(); // text edit window gtk_container_add(GTK_CONTAINER(mScroll),mLog); // add to scrolled window monofont = pango_font_description_from_string(normfont); // set fixed pitch font gtk_widget_override_font(mLog,monofont); mbar = create_menubar(mVbox); // create menu bar mFile = add_menubar_item(mbar,"File",menufunc); // add menu bar items add_submenu_item(mFile,"open job",menufunc); add_submenu_item(mFile,"edit job",menufunc); add_submenu_item(mFile,"list job",menufunc); add_submenu_item(mFile,"save job",menufunc); add_submenu_item(mFile,"save job as",menufunc); add_submenu_item(mFile,"run job",menufunc); add_submenu_item(mFile,"quit",menufunc); mBackup = add_menubar_item(mbar,"Backup",menufunc); add_submenu_item(mBackup,"backup only",menufunc); add_submenu_item(mBackup,"synchronize",menufunc); mVerify = add_menubar_item(mbar,"Verify",menufunc); add_submenu_item(mVerify,"incremental",menufunc); add_submenu_item(mVerify,"full",menufunc); add_submenu_item(mVerify,"compare",menufunc); mReport = add_menubar_item(mbar,"Report",menufunc); add_submenu_item(mReport,"get disk files",menufunc); add_submenu_item(mReport,"diffs summary",menufunc); add_submenu_item(mReport,"diffs by directory",menufunc); add_submenu_item(mReport,"diffs by file status",menufunc); add_submenu_item(mReport,"diffs by file",menufunc); add_submenu_item(mReport,"file versions",menufunc); add_submenu_item(mReport,"expired versions",menufunc); add_submenu_item(mReport,"list disk files",menufunc); add_submenu_item(mReport,"list backup files",menufunc); add_submenu_item(mReport,"find files",menufunc); add_submenu_item(mReport,"save screen",menufunc); mRestore = add_menubar_item(mbar,"Restore",menufunc); add_submenu_item(mRestore,"setup restore job",menufunc); add_submenu_item(mRestore,"list restore files",menufunc); add_submenu_item(mRestore,"restore files",menufunc); mFormat = add_menubar_item(mbar,"Format",menufunc); add_submenu_item(mFormat,"format device",menufunc); mHelp = add_menubar_item(mbar,"Help",menufunc); add_submenu_item(mHelp,"about",menufunc); add_submenu_item(mHelp,"contents",menufunc); tbar = create_toolbar(mVbox); // create toolbar and buttons gtk_toolbar_set_style(GTK_TOOLBAR(tbar),GTK_TOOLBAR_BOTH); // v.4.5 if (getuid() > 0) add_toolbar_button(tbar,"root","get root privileges","root.png",buttonfunc); else add_toolbar_button(tbar,"root","you have root privileges","root.png",buttonfunc); add_toolbar_button(tbar,"target","select backup device or directory","target.png",buttonfunc); add_toolbar_button(tbar,"mount","mount target device","mount.png",buttonfunc); add_toolbar_button(tbar,"unmount","unmount target device","unmount.png",buttonfunc); add_toolbar_button(tbar,"edit job","edit backup job","edit.png",buttonfunc); add_toolbar_button(tbar,"run job","run backup job","run.png",buttonfunc); add_toolbar_button(tbar,"pause","pause running job","media-pause.png",buttonfunc); add_toolbar_button(tbar,"resume","resume running job","media-play.png",buttonfunc); add_toolbar_button(tbar,"kill job","kill running job","stop.png",buttonfunc); add_toolbar_button(tbar,"clear","clear screen","clear.png",buttonfunc); add_toolbar_button(tbar,"quit","quit ukopp","quit.png",buttonfunc); gtk_widget_show_all(mWin); // show all widgets G_SIGNAL(mWin,"destroy",quit_ukopp,0); // connect window destroy event G_SIGNAL(mWin,"delete_event",quit_ukopp,0); watchcursor = gdk_cursor_new(GDK_WATCH); // v.4.1 mLogwin = gtk_text_view_get_window(GTK_TEXT_VIEW(mLog), // GDK window for mLog GTK_TEXT_WINDOW_TEXT); g_timeout_add(0,initfunc,0); // setup initial call from gtk_main() gtk_main(); // process window events return 0; } // initial function called from gtk_main() at startup int initfunc(void *data) { int ii; const char *home, *appdirk; time_t datetime; datetime = time(0); wprintf(mLog,"ukopp %s \n",ctime(&datetime)); // v.4.1 if (getuid() == 0) // v.4.1 wprintx(mLog,0,"you have root privileges \n",boldfont); else { menufunc(null,"Help"); // show version and license menufunc(null,"about"); } appdirk = get_zuserdir(); sprintf(TFbakfiles,"%s/bakfiles",appdirk); // make temp file names sprintf(TFpoopfile,"%s/poopfile",appdirk); sprintf(TFjobfile,"%s/jobfile",appdirk); sprintf(TFdatetime,"%s/datetime",appdirk); sprintf(TFformatscript,"%s/formatscript.sh",appdirk); menuLock = killFlag = pauseFlag = 0; // initialize controls BJnnx = 4; // default backup job data for (ii = 0; ii < BJnnx; ii++) BJfspec[ii] = (char *) malloc(60); home = getenv("HOME"); // get "/home/username" if (! home) home = "/root"; strcpy(BJfspec[0],"# default backup job"); // comment sprintf(BJfspec[1],"%s/*",home); // /home/username/* sprintf(BJfspec[2],"%s/*/Trash/*",home); // /home/username/*/Trash/* sprintf(BJfspec[3],"%s/.thumbnails/*",home); // /home/username/.thumbnails/* BJrtype[0] = 1; // comment BJrtype[1] = 2; // include BJrtype[2] = 3; // exclude BJrtype[3] = 3; // exclude BJretND[1] = BJretNV[1] = 0; // no retention specs v.3.5 BJvmode = 0; // no verify BJvalid = 0; // not validated strcpy(BJdev,""); // backup target device (maybe) strcpy(BJdirk,"/unknown"); // backup target directory, cc BJdcc = strlen(BJdirk); strcpy(RJfrom,"/home/"); // file restore copy-from location strcpy(RJto,"/home/"); // file restore copy-to location RJnnx = 0; // no. restore include/exclude recs RJvalid = 0; // not validated BDpoop(); // find devices and mount points if (*BJfilespec) BJload(BJfilespec); // load command line job file else snprintf(BJfilespec,maxfcc,"%s/ukopp.job",get_zuserdir()); // or set default job file if (clrun) { menufunc(null,"File"); // run command line job file menufunc(null,"run job"); } return 0; } // process toolbar button events (simulate menu selection) void buttonfunc(GtkWidget *, cchar *button) { char button2[20], *pp; strncpy0(button2,button,19); pp = strchr(button2,'\n'); // replace \n with blank if (pp) *pp = ' '; menufunc(0,"button"); menufunc(0,button2); return; } // process menu selection event void menufunc(GtkWidget *, cchar *menu) // revised for change in GTK behavior v.4.9 { // starting with Ubuntu 14.04 int ii; char command[100]; for (ii = 0; ii < nmenu; ii++) // search menu table if (strEqu(menu,menus[ii].menu2)) break; // mark sub-menu selection if (ii == nmenu) return; // a top menu, ignore if (menuLock && menus[ii].lock) { // no lock funcs can run parallel zmessageACK(mWin,0,"a blocking function is active"); return; } if (! menuLock) killFlag = pauseFlag = 0; // reset controls snprintf(command,99,"\n""command: %s \n",menu); wprintx(mLog,0,command,boldfont); if (menus[ii].lock) ++menuLock; menus[ii].mfunc(menu); // call menu function if (menus[ii].lock) --menuLock; return; } // get root privileges if password is OK int getroot(cchar * menu) // v.3.8 { if (getuid() == 0) // v.4.1 wprintx(mLog,0,"\nyou have root privileges \n",boldfont); else beroot(main_argc-1,main_argv+1); // does not return return 0; } // quit ukopp int quit_ukopp(cchar *menu) { int yn; char logfile[200]; if (devMounted && ukoppMounted) unmount(0); // v.3.5.2 if (BJedited && Fgui) { // v.4.0 yn = zmessageYN(mWin,"job file modified, QUIT anyway?"); if (! yn) return 1; BJedited = 0; } if (mLog) { sprintf(logfile,"%s/ukopp.log2",get_zuserdir()); // dump window to log file wfiledump(mLog,logfile); // v.3.9 } gtk_main_quit(); // tell gtk_main() to quit return 0; } // clear logging window int clearScreen(cchar *menu) { wclear(mLog); return 0; } // kill/pause/resume current function - called from menu function int signalFunc(cchar *menu) { if (strEqu(menu,"kill job")) { if (! menuLock) { wprintf(mLog,"\n""ready \n"); return 0; } if (killFlag) { wprintf(mLog," *** waiting for function to quit \n"); return 0; } wprintf(mLog," *** KILL current function \n"); pauseFlag = 0; killFlag = 1; return 0; } if (strEqu(menu,"pause")) { pauseFlag = 1; return 0; } if (strEqu(menu,"resume")) { pauseFlag = 0; return 0; } else zappcrash("signalFunc: %s",menu); return 0; } // check kill and pause flags // called periodically from long-running functions int checkKillPause() { while (pauseFlag) // idle loop while paused { zsleep(0.1); zmainloop(); // process menus } zmainloop(); // keep menus working v.4.0 if (! killFlag) return 0; // keep running return 1; // die now and reset killFlag } // find all disk devices and mount points via Linux utilities int BDpoop() // v.3.3 new udevinfo format { int ii, jj, contx = 0, pii, pjj, err; int diskf, filsysf, usbf, Nth, Nmounted; char *buff, diskdev1[40], diskdesc1[60], work[100]; cchar *pp1, *pp2; Ndisk = diskf = filsysf = usbf = 0; err = system("udevadm --version >/dev/null 2>&1"); // keep up with dynamic Linux v.3.4 if (! err) strcpy(work,"udevadm info -e"); // new Linux command else strcpy(work,"udevinfo -e"); // old Linux command while ((buff = command_output(contx,work))) { if (strnEqu(buff,"P: ",3)) { // start new device if (diskf && filsysf) { // if last device = formatted disk strncpy0(diskdev[Ndisk],diskdev1,39); // save /dev/devid strncpy0(diskdesc[Ndisk],diskdesc1,59); // save description if (usbf) strcat(diskdesc[Ndisk]," (USB)"); // note if USB device strcpy(diskmp[Ndisk],"(not mounted)"); // mount point TBD Ndisk++; if (Ndisk == maxdisk) { wprintf(mLog," *** exceeded %d devices \n",maxdisk); break; } } diskf = filsysf = usbf = 0; // clear new device flags } if (strnEqu(buff,"N: ",3)) { strcpy(diskdev1,"/dev/"); strncat(diskdev1,buff+3,14); // save /dev/devid } if (strnEqu(buff,"E: ",3)) { pp1 = strstr(buff,"ID_TYPE=disk"); if (pp1) diskf = 1; // device is a disk pp1 = strstr(buff,"ID_FS_TYPE="); if (pp1) filsysf = 1; // device has a file system pp1 = strstr(buff,"ID_BUS=usb"); if (pp1) usbf = 1; // device is a USB device pp1 = strstr(buff,"ID_MODEL="); if (pp1) strncpy0(diskdesc1,pp1+9,59); // save description } } if (! Ndisk) { wprintf(mLog," no devices found \n"); return 0; } contx = Nmounted = 0; while ((buff = command_output(contx,"cat /proc/mounts"))) // get mounted disk info v.3.2 { if (strnNeq(buff,"/dev/",5)) continue; // not a /dev/xxx record Nth = 1; pp1 = strField(buff,' ',Nth++); // parse /dev/xxx /media/xxx pp2 = strField(buff,' ',Nth++); for (ii = 0; ii < Ndisk; ii++) // look for matching device { if (strNeq(pp1,diskdev[ii])) continue; strncpy0(diskmp[ii],pp2,59); // copy its mount point strTrim(diskmp[ii]); Nmounted++; break; } } #define swap(name,ii,jj) { \ strcpy(work,name[ii]); \ strcpy(name[ii],name[jj]); \ strcpy(name[jj],work); } for (ii = 0; ii < Ndisk; ii++) // sort USB and mounted devices for (jj = ii + 1; jj < Ndisk; jj++) // to the top of the list { pii = pjj = 0; if (strstr(diskdesc[ii],"(USB)")) pii += 2; if (! strEqu(diskmp[ii],"(not mounted)")) pii += 1; if (strstr(diskdesc[jj],"(USB)")) pjj += 2; if (! strEqu(diskmp[jj],"(not mounted)")) pjj += 1; if (pjj > pii) { swap(diskdev,jj,ii); swap(diskmp,jj,ii); swap(diskdesc,jj,ii); } } return Nmounted; } // choose backup device or enter a target directory // update backup job target device and directory int chooseTarget(cchar *) // overhauled v.3.2 { int ii, zstat; char text[300]; zdialog *zd; cchar *instruct = "Select target device or directory"; const char *errmess = 0; BDpoop(); // refresh available devices zd = zdialog_new("Choose Backup Target",mWin,"OK","cancel",null); zdialog_add_widget(zd,"vbox","vb1","dialog",0,"space=10"); zdialog_add_widget(zd,"label","lab1","vb1",instruct); // select backup device ... zdialog_add_widget(zd,"comboE","target","vb1",BJdirk); // [_______________________][v] for (ii = 0; ii < Ndisk; ii++) { // load combo box with device poop strcpy(text,diskdev[ii]); // /dev/xxx /media/xxx (description) strncatv(text,299," ",diskmp[ii]," (",diskdesc[ii],")",null); zdialog_cb_app(zd,"target",text); } zdialog_resize(zd,300,0); // v.4.1 zdialog_run(zd,0,"mouse"); // run dialog posn v.4.1 zstat = zdialog_wait(zd); if (zstat != 1) { zdialog_free(zd); return 0; } zdialog_fetch(zd,"target",text,299); // get device or target directory zdialog_free(zd); // kill dialog errmess = parseTarget(text); // parse selected device, directory wprintf(mLog," new target: %s %s \n",BJdev,BJdirk); if (errmess) wprintf(mLog," *** %s \n",errmess); BJedited++; // v.4.1 return 0; } // job file open dialog - get backup job data from a file // return 1 if OK, else 0 int BJfileOpen(cchar *menu) { char *file; file = zgetfile("open backup job","file",BJfilespec,"hidden"); // get file from user if (file) { strncpy0(BJfilespec,file,maxfcc-2); free(file); BJload(BJfilespec); // load job file, set BJvalid } return 0; } // job file save dialog - save backup job data to a file // return 1 if OK, else 0 int BJfileSave(cchar *menu) { char *file; int yn; if (! BJvalid && Fgui) { yn = zmessageYN(mWin,"backup job has errors, save anyway?"); // v.3.5 if (! yn) return 0; } if (strEqu(menu,"save job")) { BJstore(BJfilespec); return 0; } file = zgetfile("save backup job","save",BJfilespec,"hidden"); if (file) { strncpy0(BJfilespec,file,maxfcc-2); free(file); BJstore(BJfilespec); } return 0; } // backup job data <<< jobfile int BJload(cchar *jobfile) { FILE *fid; char *pp, *fspec, buff[1000]; const char *errmess, *jobname; int rtype, days, vers, nerrs = 0; snprintf(buff,999,"\n""loading job file: %s \n",jobfile); wprintx(mLog,0,buff,boldfont); fid = fopen(jobfile,"r"); // open job file if (! fid) { wprintf(mLog," *** cannot open job file: %s \n",jobfile); return 0; } BJreset(); // reset all job data while (true) { pp = fgets_trim(buff,999,fid,1); // read next job record if (! pp) break; // EOF wprintf(mLog," %s \n",buff); // output if (strnEqu(pp,"target",6)) { errmess = parseTarget(buff); // target /dev/xxx /xxxxxxx if (errmess) wprintf(mLog," *** %s \n",errmess); continue; } if (strnEqu(pp,"verify",6)) { errmess = parseVerify(buff); // verify xxxxxx if (errmess) wprintf(mLog," *** %s \n",errmess); if (errmess) nerrs++; continue; } errmess = parseNXrec(buff,rtype,fspec,days,vers); // comment/include/exclude if (errmess) wprintf(mLog," *** %s \n",errmess); if (errmess) nerrs++; BJfspec[BJnnx] = fspec; BJrtype[BJnnx] = rtype; BJretND[BJnnx] = days; BJretNV[BJnnx] = vers; BJnnx++; if (BJnnx == maxnx) { wprintf(mLog," *** max job records exceeded \n"); nerrs++; break; } } fclose(fid); // close file if (nerrs == 0) { BJvalid = 1; // job valid if no errors jobname = strrchr(BJfilespec,'/') + 1; snprintf(buff,100,"%s %s",ukopp_title,jobname); // put job name in window title v.4.0 if (Fgui) gtk_window_set_title(GTK_WINDOW(mWin),buff); BJedited = 0; } return 1; } // backup job data >>> jobfile // return 1 if OK, else 0 int BJstore(cchar *jobfile) { FILE *fid; char buff[100]; cchar *jobname; fid = fopen(jobfile,"w"); // open file if (! fid) { wprintf(mLog," *** cannot open job file: %s \n",jobfile); return 0; } for (int ii = 0; ii < BJnnx; ii++) { if (BJrtype[ii] == 1) fprintf(fid,"%s \n",BJfspec[ii]); // comment if (BJrtype[ii] == 2) { if (BJretND[ii] + BJretNV[ii] > 0) // include /filespec (nd,nv) fprintf(fid,"include %s%s%d,%d%s\n", BJfspec[ii],RSEP1,BJretND[ii],BJretNV[ii],RSEP2); else fprintf(fid,"include %s\n",BJfspec[ii]); } if (BJrtype[ii] == 3) // exclude /filespec fprintf(fid,"exclude %s\n",BJfspec[ii]); } fprintf(fid,"verify %s \n",vertype[BJvmode]); // verify xxxx fprintf(fid,"target %s %s \n",BJdev,BJdirk); // target /dev/xxx /xxxxxxx fclose(fid); if (strNeq(jobfile,TFjobfile)) { // if not job file in temp storage, jobname = strrchr(jobfile,'/') + 1; snprintf(buff,100,"%s %s",ukopp_title,jobname); // put job name in window title v.4.0 if (Fgui) gtk_window_set_title(GTK_WINDOW(mWin),buff); BJedited = 0; } return 1; } // list backup job data to log window int BJlist(cchar *menu) { wprintf(mLog,"\n backup job file: %s \n",BJfilespec); // job file v.21 for (int ii = 0; ii < BJnnx; ii++) { if (BJrtype[ii] == 1) // comment wprintf(mLog," %s \n",BJfspec[ii]); if (BJrtype[ii] == 2) { // include /filespec (nd,nv) if (BJretND[ii] + BJretNV[ii] > 0) wprintf(mLog," include %s%s%d days, %d vers%s \n", BJfspec[ii],RSEP1,BJretND[ii],BJretNV[ii],RSEP2); else wprintf(mLog," include %s \n",BJfspec[ii]); } if (BJrtype[ii] == 3) // exclude /filespec wprintf(mLog," exclude %s \n",BJfspec[ii]); } wprintf(mLog," verify %s \n",vertype[BJvmode]); // verify xxxx wprintf(mLog," target %s %s \n",BJdev,BJdirk); // target /dev/xxx /xxxxxx return 0; } // edit dialog for backup job data int BJedit_fchooser(cchar *dirk); zdialog *BJedit_fchooser_zd = 0; char orgBJdev[40]; // v.4.1 char orgBJdirk[200]; int BJedit(cchar *menu) { int BJedit_dialog_event(zdialog *zd, const char *event); zdialog *zd; char text[300]; zd = zdialog_new("Edit Backup Job",mWin,"browse","clear","done","cancel",null); zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=4"); // target: /dev/xxx /xxxxx [choose] zdialog_add_widget(zd,"label","labtarg","hb1","backup target: "); // v.3.5 zdialog_add_widget(zd,"label","target","hb1","/dev/xxx /xxxxxx"); zdialog_add_widget(zd,"button","choosetarg","hb1","choose target"); zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=4"); // verify: (o) none (o) incr (o) ... zdialog_add_widget(zd,"label","labverify","hb2","verify method: "); // v.3.5 zdialog_add_widget(zd,"radio","vnone","hb2","none"); zdialog_add_widget(zd,"radio","vincr","hb2","incremental","space=10"); zdialog_add_widget(zd,"radio","vfull","hb2","full","space=10"); zdialog_add_widget(zd,"radio","vcomp","hb2","compare","space=10"); zdialog_add_widget(zd,"hsep","sep2","dialog"); // edit box for job recs zdialog_add_widget(zd,"label","labinex","dialog","Include / Exclude"); zdialog_add_widget(zd,"frame","frminex","dialog",0,"expand"); zdialog_add_widget(zd,"scrwin","scrwinex","frminex"); zdialog_add_widget(zd,"edit","edinex","scrwinex"); snprintf(text,299,"%s %s",BJdev,BJdirk); // stuff current target v.3.5 zdialog_stuff(zd,"target",text); strncpy0(orgBJdev,BJdev,40); // save in case of cancel v.4.1 strncpy0(orgBJdirk,BJdirk,200); zdialog_stuff(zd,"vnone",0); // stuff verify mode v.3.5 zdialog_stuff(zd,"vincr",0); zdialog_stuff(zd,"vfull",0); zdialog_stuff(zd,"vcomp",0); if (BJvmode == 0) zdialog_stuff(zd,"vnone",1); if (BJvmode == 1) zdialog_stuff(zd,"vincr",1); if (BJvmode == 2) zdialog_stuff(zd,"vfull",1); if (BJvmode == 3) zdialog_stuff(zd,"vcomp",1); editwidget = zdialog_widget(zd,"edinex"); wclear(editwidget); // stuff include/exclude recs for (int ii = 0; ii < BJnnx; ii++) { if (BJrtype[ii] == 1) // comment wprintf(editwidget,"%s\n",BJfspec[ii]); if (BJrtype[ii] == 2) { // include /filespec (nd,nv) if (BJretND[ii] + BJretNV[ii] > 0) wprintf(editwidget,"include %s%s%d,%d%s\n", BJfspec[ii],RSEP1,BJretND[ii],BJretNV[ii],RSEP2); else wprintf(editwidget,"include %s\n",BJfspec[ii]); } if (BJrtype[ii] == 3) // exclude /filespec wprintf(editwidget,"exclude %s\n",BJfspec[ii]); } zdialog_resize(zd,400,400); zdialog_run(zd,BJedit_dialog_event,"40/10"); // run dialog posn v.4.1 zdialog_wait(zd); // wait for completion return 0; } // job edit dialog event function int BJedit_dialog_event(zdialog *zd, const char *event) { int rtype, days, vers, nerrs = 0; char *pp, *fspec, text[300]; cchar *errmess = 0, *jobname; int zstat, nn, ftf = 1; if (strEqu(event,"choosetarg")) { // set new target device, directory chooseTarget(0); snprintf(text,299,"%s %s",BJdev,BJdirk); zdialog_stuff(zd,"target",text); return 0; } zstat = zd->zstat; // zdialog complete? if (! zstat) { BJedited++; // no, manual edit was done v.4.1 return 0; } zd->zstat = 0; // dialog may continue if (zstat == 2) { wclear(editwidget); // clear include/exclude recs BJedited++; // v.4.1 return 0; } if (zstat == 1) { // browse, do file-chooser dialog if (! BJedit_fchooser_zd) BJedit_fchooser("/home"); return 0; } if (BJedit_fchooser_zd) // kill file chooser dialog if active zdialog_free(BJedit_fchooser_zd); if (zstat != 3) { // cancel or kill zdialog_free(zd); strcpy(BJdev,orgBJdev); // restore original target v.4.1 strcpy(BJdirk,orgBJdirk); BJedited = 0; return 0; } if (! BJedited) { // done zdialog_free(zd); // no edits made v.4.1 return 0; } BJreset(); // reset job data zdialog_fetch(zd,"target",text,299); // get device or target directory wprintf(mLog," target: %s \n",text); errmess = parseTarget(text); // v.3.5 if (errmess) wprintf(mLog," *** %s \n",errmess); BJvmode = 0; zdialog_fetch(zd,"vincr",nn); // get verify mode v.3.5 if (nn) BJvmode = 1; zdialog_fetch(zd,"vfull",nn); if (nn) BJvmode = 2; zdialog_fetch(zd,"vcomp",nn); if (nn) BJvmode = 3; for (BJnnx = 0; BJnnx < maxnx; ) // get include/exclude records { pp = wscanf(editwidget,ftf); if (! pp) break; errmess = parseNXrec(pp,rtype,fspec,days,vers); if (errmess) { wprintf(mLog,"%s \n *** %s \n",pp,errmess); nerrs++; } BJfspec[BJnnx] = fspec; BJrtype[BJnnx] = rtype; BJretND[BJnnx] = days; BJretNV[BJnnx] = vers; BJnnx++; // v.4.6 } if (nerrs == 0) BJvalid = 1; // valid job if no errors jobname = strrchr(BJfilespec,'/') + 1; snprintf(text,100,"%s %s (*)",ukopp_title,jobname); // (*) in title for edited job v.4.0 gtk_window_set_title(GTK_WINDOW(mWin),text); zdialog_free(zd); // destroy dialog return 0; } // file chooser dialog for backup job edit int BJedit_fchooser(cchar *dirk) // v.3.5 { int BJedit_fchooser_event(zdialog *zd, const char *event); BJedit_fchooser_zd = zdialog_new("Choose Files for Backup",mWin,"Done",null); zdialog *zd = BJedit_fchooser_zd; zdialog_add_widget(zd,"frame","fr1","dialog",0,"expand"); zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5"); zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5"); zdialog_add_widget(zd,"label","space","hb1",0,"expand"); zdialog_add_widget(zd,"button","incl","hb1","include","space=5"); zdialog_add_widget(zd,"button","excl","hb1","exclude","space=5"); zdialog_add_widget(zd,"check","showhf","hb1","Show hidden","space=10"); zdialog_add_widget(zd,"label","space","hb2",0,"expand"); zdialog_add_widget(zd,"label","lab1","hb2","Retain old files: Days: "); zdialog_add_widget(zd,"spin","days","hb2","0|9999|1|0"); zdialog_add_widget(zd,"label","lab2","hb2"," Versions: "); zdialog_add_widget(zd,"spin","vers","hb2","0|9999|1|0"); zdialog_add_widget(zd,"label","space","hb2",0,"space=5"); fc_widget = gtk_file_chooser_widget_new(GTK_FILE_CHOOSER_ACTION_OPEN); GtkWidget *frame = zdialog_widget(zd,"fr1"); gtk_container_add(GTK_CONTAINER(frame),fc_widget); gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(fc_widget),dirk); gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(fc_widget),1); gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(fc_widget),1); zdialog_stuff(zd,"showhf",1); zdialog_resize(zd,550,500); zdialog_run(zd,BJedit_fchooser_event); zdialog_wait(zd); zdialog_free(zd); BJedit_fchooser_zd = 0; return 0; } int BJedit_fchooser_event(zdialog *zd, const char *event) { GSList *flist = 0; struct stat64 filestat; char *file1, *file2; int ii, err, showhf, days, vers; if (strEqu(event,"showhf")) // show/hide hidden files { zdialog_fetch(zd,"showhf",showhf); gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(fc_widget),showhf); } if (strEqu(event,"incl") || strEqu(event,"excl")) // include or exclude { flist = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(fc_widget)); for (ii = 0; ; ii++) // process selected files { file1 = (char *) g_slist_nth_data(flist,ii); if (! file1) break; file2 = strdupz(file1,2); // extra space for wildcard g_free(file1); err = lstat64(file2,&filestat); if (err) { wprintf(mLog," *** error: %s file: %s \n",strerror(errno),file2); continue; } if (S_ISDIR(filestat.st_mode)) strcat(file2,"/*"); // if directory, append wildcard zdialog_fetch(zd,"days",days); // get corresp. retention specs zdialog_fetch(zd,"vers",vers); // from dialog if (strEqu(event,"incl")) { // include /filespec (dd,vv) v.3.5 if (days || vers) wprintf(editwidget,"include %s%s%d,%d%s\n",file2,RSEP1,days,vers,RSEP2); else wprintf(editwidget,"include %s""\n",file2); } if (strEqu(event,"excl")) wprintf(editwidget,"exclude %s""\n",file2); free(file2); BJedited++; } gtk_file_chooser_unselect_all(GTK_FILE_CHOOSER(fc_widget)); g_slist_free(flist); } return 0; } // parse and validate a comment/include/exclude record // filespec* means a /path.../filename with wildcards // # comment (or blank line) // include filespec* [ (days,vers) ] // v.3.5 // exclude filespec* cchar * parseNXrec(const char *jobrec, int &rtype, char *&fspec, int &days, int &vers) { int nn, Nth = 1; const char *pp1, *pp2; rtype = days = vers = -1; fspec = null; pp1 = strField(jobrec,' ',Nth++); if (! pp1 || *pp1 == '#') { // comment or blank line rtype = 1; if (pp1) fspec = strdup(pp1); else fspec = strdup(""); return 0; } if (strEqu(pp1,"include")) { // include /filespec (nd,nv) rtype = 2; pp1 = jobrec + 7; while(*pp1 == ' ') pp1++; pp2 = strstr(pp1,RSEP1); if (! pp2) { // assume no (nd,nv) fspec = strdupz(pp1,4); // v.4.1 days = vers = 0; } else { // parse (nd,nv) v.3.5 nn = sscanf(pp2,RSEP1" %d , %d "RSEP2,&days,&vers); if (nn != 2 || days < 0 || days > 9999 || vers < 0 || vers > 9999) return "invalid retention spec, use \" (nn,nn)\" "; fspec = strdupz(pp1,4); // v.4.1 fspec[pp2-pp1] = 0; } strTrim2(fspec); // strip trailing blanks v.4.0 pp1 = fspec; if (*pp1 == '"') { pp1++; // allow quoted filespec v.4.1 pp2 = pp1 + strlen(pp1); if (pp2[-1] != '"') return "filespec closing quote missing"; } if (*pp1 != '/') return "filespec missing /topdir/"; pp1 = strchr(pp1+1,'/'); if (!pp1) return "filespec missing /topdir/"; pp2 = strchr(fspec,'*'); if (pp2 && pp2 < pp1) return "wildcards in /topdir/ not allowed"; pp2 = strchr(fspec,'?'); if (pp2 && pp2 < pp1) return "wildcards in /topdir/ not allowed"; return 0; } if (strEqu(pp1,"exclude")) { // exclude /filespec rtype = 3; pp1 = jobrec + 7; while(*pp1 == ' ') pp1++; fspec = strdupz(pp1,4); // v.4.1 strTrim2(fspec); // strip trailing blanks v.4.0 pp1 = fspec; if (*pp1 == '"') { // allow quoted filespec v.4.1 pp2 = pp1 + strlen(pp1); if (pp2[-1] != '"') return "filespec closing quote missing"; } return 0; } return "unrecognized record type"; } // parse a verify record: verify xxxxx cchar * parseVerify(const char *text) // v.3.5 { const char *pp; BJvmode = 0; pp = strField(text,' ',1); if (! pp || strNeq(pp,"verify")) return "bad verify record"; pp = strField(text,' ',2); if (! pp) return "missing verify type"; BJvmode = -1; if (strEqu(pp,"none")) BJvmode = 0; if (strnEqu(pp,"incr",4)) BJvmode = 1; if (strEqu(pp,"full")) BJvmode = 2; if (strnEqu(pp,"comp",4)) BJvmode = 3; if (BJvmode >= 0) return 0; BJvmode = 0; return "bad verify mode"; } // parse a target record and set backup device and directory accordingly // format: [ target ] [ /dev/xxx ] [ /directory ] cchar * parseTarget(const char *text) // more robust v.3.5 { int ii, err, cc, yn, Nth = 1; int direxists = 0, dirempty = 0; char ch; const char *pp; DIR *dirf; struct dirent *ppd; struct stat dstat; bFilesReset(); // no files at backup location *BJdev = *BJdirk = BJdcc = 0; // reset target poop pp = strField(text,' ',Nth++); if (pp && strEqu(pp,"target")) // skip "target" pp = strField(text,' ',Nth++); if (pp && strnEqu(pp,"/dev/",5)) { strncpy0(BJdev,pp,39); // have /dev/xxxx pp = strField(text,' ',Nth++); } if (pp && *pp == '/') { strncpy0(BJdirk,pp,199); // have /directory/... BJdcc = strlen(BJdirk); } if (! *BJdev && ! *BJdirk) return "no backup target specified"; BDpoop(); // refresh known device data if (*BJdev) { // if device is specified for (ii = 0; ii < Ndisk; ii++) if (strEqu(BJdev,diskdev[ii])) break; // look for device if (ii == Ndisk) return "target device not found"; } if (*BJdev && ! *BJdirk) { // get mount point for device for (ii = 0; ii < Ndisk; ii++) if (strEqu(BJdev,diskdev[ii])) break; if (ii < Ndisk && *diskmp[ii] == '/') strcpy(BJdirk,diskmp[ii]); } if (! *BJdev && *BJdirk) { // get device for mount point for (ii = 0; ii < Ndisk; ii++) if (strEqu(BJdirk,diskmp[ii])) break; if (ii < Ndisk) strcpy(BJdev,diskdev[ii]); } if (*BJdev && ! *BJdirk) { // if no directory specified, strcpy(BJdirk,"/media"); // set a default for device strcpy(BJdirk+6,BJdev+4); // e.g. /media/sdf1 } BJdcc = strlen(BJdirk); // set target directory cc err = stat(BJdirk,&dstat); // determine if directory if (! err && S_ISDIR(dstat.st_mode)) direxists = 1; // exists in file system if (direxists) { // determine if directory is empty dirempty = 1; dirf = opendir(BJdirk); if (dirf) { while (true) { ppd = readdir(dirf); if (! ppd) break; if (ppd->d_name[0] == '.') continue; dirempty = 0; break; } closedir(dirf); } } if (direxists) { // directory exists if (*BJdev) { // if device is specified, for (ii = 0; ii < Ndisk; ii++) // find where it is mounted if (strEqu(BJdev,diskdev[ii])) break; if (ii == Ndisk || *diskmp[ii] != '/') { // device not mounted if (dirempty) { wprintf(mLog,"target is valid and not mounted \n"); // mount to existing empty return 0; // directory is allowed } else return "target directory is not an empty directory"; // directory is not empty } else { // device is mounted cc = strlen(diskmp[ii]); if (! strnEqu(diskmp[ii],BJdirk,cc)) return "target directory not on target device"; // somewhere else ch = BJdirk[cc]; if (ch && ch != '/') return "target directory not on target device"; devMounted = 1; // device mounted at directory strcpy(mountdev,BJdev); // save for later unmount() strcpy(mountdirk,BJdirk); wprintf(mLog,"target is valid and mounted \n"); return 0; } } else { // device not specified wprintf(mLog,"target directory is valid \n"); return 0; } } else { // directory does not exist if (*BJdev) { wprintf(mLog,"target is valid and not mounted \n"); // can be created at mount time return 0; } else { yn = zmessageYN(mWin,"target directory does not exist\n" // offer to create if missing "create target directory?"); // v.4.1 if (! yn) return "target directory does not exist"; err = mkdir(BJdirk,0751); if (err) return strerror(errno); else return "target directory created"; } } } // Delete backup files exceeding age and version limits. // Copy new and modified disk files to backup location. int Backup(cchar *menu) { char message[100]; int vmode = 0, terrs = 0, ii, jj, yn; int upvers = 0, deleted = 0; char disp, *dfile = 0; const char *errmess = 0; double bsecs, bbytes, bspeed; double time0; if (! BJvalid) { if (Fgui) zmessageACK(mWin,0,"backup job has errors (open or edit)"); else wprintf(mLog,"backup job has errors \n"); return 0; } if (! mount(0)) return 0; // validate and mount target v.3.2 Report("diffs summary"); // refresh all file data, report diffs if (Fgui) { yn = zmessageYN(mWin,"backup target: %s %s \n" "%d files (%s) will be copied to \n" "(or deleted from) backup media \n" "continue?", BJdev,BJdirk,Mfiles,formatKBMB(Mbytes,3)); // confirm backup target v.23 if (! yn) return 0; } snprintf(message,99,"\n""begin backup \n"); wprintx(mLog,0,message,boldfont); wprintf(mLog," files: %d bytes: %s \n",Mfiles,formatKBMB(Mbytes,3)); // files and bytes to copy if (Mfiles == 0) { wprintf(mLog," *** nothing to back-up \n"); return 0; } wprintf(mLog," using backup directory: %s %s \n",BJdev,BJdirk); if (strEqu(menu,"backup only")) vmode = 0; // backup command, no auto verify if (strEqu(menu,"run job")) vmode = BJvmode; wprintf(mLog," assign new version numbers to modified backup files \n" " and purge expired versions from backup location \n\n"); gdk_window_set_cursor(mLogwin,watchcursor); // busy cursor v.4.1 for (ii = 0; ii < Bnf; ii++) // scan files at backup location { disp = Brec[ii].disp; dfile = Brec[ii].file; errmess = null; if (disp == 'm' || disp == 'd') { // modified or deleted, errmess = setnextVersion(Brec[ii]); // rename to next version number Brec[ii].err = -1; // mark file gone if (disp =='m') upvers++; // update counts if (disp =='d') deleted++; } if (! errmess) errmess = purgeVersions(Brec[ii],1); // purge expired file versions if (errmess) { wprintf(mLog," %s \n *** %s \n",dfile,errmess); // log error v.4.3 terrs++; if (terrs > 100) goto backup_fail; } } wprintf(mLog," %d backup files were assigned new versions \n",upvers); wprintf(mLog," %d backup files were deleted \n",deleted); wprintf(mLog," %d expired versions (%s) were purged \n\n",Pfiles,formatKBMB(Pbytes,3)); Pbytes = Pfiles = 0; start_timer(time0); // start timer bbytes = Mbytes; BJstore(TFjobfile); // copy job file to temp file writeDT(); // create date-time temp file wprintf(mLog,-2," %s \n",BD_JOBFILE); errmess = copyFile(TFjobfile,BD_JOBFILE,2); // copy job file to backup location if (errmess) goto backup_fail; wprintf(mLog,-2," %s \n",BD_DATETIME); errmess = copyFile(TFdatetime,BD_DATETIME,2); // copy date-time file if (errmess) goto backup_fail; wprintf(mLog," copying new and modified files from disk to backup location \n\n"); for (ii = 0; ii < Dnf; ii++) // scan all disk files { disp = Drec[ii].disp; dfile = Drec[ii].file; Drec[ii].finc = 0; // not included yet if (disp == 'n' || disp == 'm') // new or modified file { wprintf(mLog," %s \n",dfile); errmess = copyFile(dfile,dfile,2); // copy disk file to backup v.4.3 if (errmess) { Drec[ii].err = 1; // copy failed wprintf(mLog," *** %s \n",errmess); // log error v.4.3 terrs++; if (terrs > 100) goto backup_fail; } else { // copy OK Drec[ii].finc = 1; // set included file flag } if (checkKillPause()) goto backup_fail; // killed by user } jj = Drec[ii].bindx; // purge last version now if (jj >= 0) purgeVersions(Brec[jj],0); // bugfix v.4.1 } if (terrs) wprintf(mLog," *** %d files had backup errors \n",terrs); synch_poop("backup"); // synch owner and permissions data bsecs = get_timer(time0); // output perf. statistics wprintf(mLog," backup time: %.1f secs \n",bsecs); bspeed = bbytes/mega/bsecs; wprintf(mLog," backup speed: %.2f MB/sec \n",bspeed); wprintf(mLog," backup complete \n"); if (vmode) // do verify if requested { wprintf(mLog,"\n"); sleep(2); if (vmode == 1) Verify("incr"); else if (vmode == 2) Verify("full"); else if (vmode == 3) Verify("comp"); sprintf(message," %d files had backup errors \n",terrs); // repeat backup status v.4.5 wprintx(mLog,0,message,boldfont); terrs = fverrs + fcerrs; sprintf(message," %d files had verify errors \n",terrs); // add verify status v.4.5 wprintx(mLog,0,message,boldfont); } wprintf(mLog," ready \n"); if (ukoppMounted) unmount(0); // leave unmounted gdk_window_set_cursor(mLogwin,0); // normal cursor return 0; backup_fail: if (terrs > 100) wprintf(mLog," too many errors, giving up \n"); else if (errmess) wprintf(mLog," %s \n",errmess); wprintx(mLog,0," *** BACKUP FAILED \n",boldfont); bFilesReset(); killFlag = 0; gdk_window_set_cursor(mLogwin,0); // normal cursor return 0; } // synchronize disk and backup files // v.25 // bi-directional copy of new and newer files int Synch(cchar *menu) { int ii, yn, dii, bii, comp; char disp, *dfile = 0; time_t btime, dtime; const char *errmess = 0; if (! BJvalid) { wprintf(mLog," *** job data has errors \n"); return 0; } if (! mount(0)) return 0; // validate and mount target v.3.2 if (Fgui) { yn = zmessageYN(mWin,"backup target: %s %s \n continue?",BJdev,BJdirk); // confirm backup target if (! yn) return 0; } wprintf(mLog," using backup directory: %s %s \n",BJdev,BJdirk); dGetFiles(); // get disk files of backup job if (bGetFiles() < 0) goto synch_exit; // get files in backup location setFileDisps(); // compare and set dispositions wprintf(mLog,"\n begin synchronize \n"); gdk_window_set_cursor(mLogwin,watchcursor); // busy cursor v.4.1 BJstore(TFjobfile); // copy job file to temp file writeDT(); // create date-time temp file wprintf(mLog,-2," %s \n",BD_JOBFILE); errmess = copyFile(TFjobfile,BD_JOBFILE,2); // copy job file to backup location if (errmess) goto synch_exit; wprintf(mLog,-2," %s \n",BD_DATETIME); errmess = copyFile(TFdatetime,BD_DATETIME,2); // copy date-time file if (errmess) goto synch_exit; for (ii = 0; ii < Dnf; ii++) // copy new disk files >> backup loc. { disp = Drec[ii].disp; dfile = Drec[ii].file; if (disp != 'n') continue; wprintf(mLog," disk >> backup: %s \n",dfile); errmess = copyFile(dfile,dfile,2); if (errmess) wprintf(mLog," *** %s \n",errmess); else Drec[ii].finc = 1; if (checkKillPause()) goto synch_exit; // killed by user } for (ii = 0; ii < Bnf; ii++) // copy new backup files >> disk { // (aka "deleted" disk files) disp = Brec[ii].disp; dfile = Brec[ii].file; if (disp != 'd') continue; wprintf(mLog," backup >> disk: %s \n",dfile); errmess = copyFile(dfile,dfile,1); if (errmess) wprintf(mLog," *** %s \n",errmess); else Brec[ii].finc = 1; if (checkKillPause()) goto synch_exit; } dii = bii = 0; while ((dii < Dnf) || (bii < Bnf)) // scan disk and backup files parallel { if ((dii < Dnf) && (bii == Bnf)) comp = -1; else if ((dii == Dnf) && (bii < Bnf)) comp = +1; else comp = strcmp(Drec[dii].file, Brec[bii].file); if (comp < 0) { dii++; continue; } // next disk file if (comp > 0) { bii++; continue; } // next backup file disp = Drec[dii].disp; dfile = Drec[dii].file; if (disp == 'm') // screen for modified status { btime = int(Brec[bii].mtime); dtime = int(Drec[dii].mtime); if (btime > dtime) { // copy newer backup file >> disk wprintf(mLog," backup >> disk: %s \n",dfile); errmess = copyFile(dfile,dfile,1); if (errmess) wprintf(mLog," *** %s \n",errmess); else Brec[bii].finc = 1; } else { // copy newer disk file >> backup wprintf(mLog," disk >> backup: %s \n",dfile); errmess = copyFile(dfile,dfile,2); if (errmess) wprintf(mLog," *** %s \n",errmess); else Drec[dii].finc = 1; } } dii++; // next disk and backup files bii++; if (checkKillPause()) goto synch_exit; // killed by user } errmess = null; synch_poop("synch"); // synch owner and permissions data Verify("incremental"); // verify all files copied synch_exit: if (errmess) wprintf(mLog," *** %s \n",errmess); wprintf(mLog," ready \n"); // v.3.6 killFlag = 0; gdk_window_set_cursor(mLogwin,0); // normal cursor return 0; } // verify integrity of backup files int Verify(cchar *menu) { int ii, vers, comp, vfiles; int dfiles1 = 0, dfiles2 = 0; char filespec[maxfcc]; const char *errmess = 0; double secs, dcc1, vbytes, vspeed; double mtime, diff; double time0; struct stat64 filestat; vfiles = fverrs = fcerrs = 0; vbytes = 0.0; if (! mount(0)) return 0; // validate and mount target v.3.2 start_timer(time0); gdk_window_set_cursor(mLogwin,watchcursor); // busy cursor v.4.1 if (strnEqu(menu,"incremental",4)) // verify new/modified files only { wprintx(mLog,0,"\n""Verify files copied in prior backup or synch \n",boldfont); for (ii = 0; ii < Dnf; ii++) // scan disk file list { if (! Drec[ii].finc) continue; // file included in last backup strncpy0(filespec,Drec[ii].file,maxfcc-1); wprintf(mLog," %s \n",filespec); // output filespec errmess = checkFile(filespec,1,dcc1); // compare disk/backup files, get length if (errmess) { wprintf(mLog," *** %s \n\n",errmess); // log and count errors if (strstr(errmess,"compare")) fcerrs++; // backup - disk compare failure else fverrs++; } vfiles++; // count files and bytes vbytes += dcc1; if (fverrs + fcerrs > 100) goto verify_exit; // v.3.7 if (checkKillPause()) goto verify_exit; // killed by user } for (ii = 0; ii < Bnf; ii++) // scan backup file list v.25 { if (! Brec[ii].finc) continue; // file included in last backup strncpy0(filespec,Brec[ii].file,maxfcc-1); wprintf(mLog," %s \n",filespec); // output filespec errmess = checkFile(filespec,1,dcc1); // compare disk/backup files, get length if (errmess) { wprintf(mLog," *** %s \n\n",errmess); // log and count errors if (strstr(errmess,"compare")) fcerrs++; // backup - disk compare failure else fverrs++; } vfiles++; // count files and bytes vbytes += dcc1; if (fverrs + fcerrs > 100) goto verify_exit; // v.3.7 if (checkKillPause()) goto verify_exit; // killed by user } } if (strEqu(menu,"full")) // verify all files are readable { wprintx(mLog,0,"\n""Read and verify ALL backup files \n\n",boldfont); bGetFiles(); // get all files at backup location wprintf(mLog," %d backup files \n",Bnf); if (! Bnf) goto verify_exit; for (ii = 0; ii < Bnf; ii++) // scan backup file list { strncpy0(filespec,Brec[ii].file,maxfcc-10); // /directory.../filename if (Brec[ii].err == 0) { // check current file wprintf(mLog,-2," %s \n",filespec); errmess = checkFile(filespec,0,dcc1); // verify file, get length if (errmess) { wprintf(mLog,-1," *** %s \n",errmess); // log and count error wprintf(mLog,"\n"); fverrs++; } vfiles++; // count files and bytes vbytes += dcc1; } if (Brec[ii].lover) for (vers = Brec[ii].lover; vers <= Brec[ii].hiver; vers++) // check previous versions { setFileVersion(filespec,vers); // append version if > 0 wprintf(mLog,-2," %s \n",filespec); errmess = checkFile(filespec,0,dcc1); // verify file, get length if (errmess) { wprintf(mLog,-1," *** %s \n",errmess); // log and count error wprintf(mLog,"\n"); fverrs++; } vfiles++; // count files and bytes vbytes += dcc1; } if (fverrs + fcerrs > 100) goto verify_exit; // v.3.7 if (checkKillPause()) goto verify_exit; // killed by user } } if (strnEqu(menu,"compare",4)) // compare backup files to disk files { wprintx(mLog,0,"\n Read and verify ALL backup files. \n",boldfont); wprintf(mLog," Compare to correspending disk files (if present). \n\n"); bGetFiles(); // get all files at backup location wprintf(mLog," %d backup files \n",Bnf); if (! Bnf) goto verify_exit; for (ii = 0; ii < Bnf; ii++) // scan backup file list { strncpy0(filespec,Brec[ii].file,maxfcc-10); // /directory.../filename if (Brec[ii].err == 0) { // check current file comp = 0; if (lstat64(filespec,&filestat) == 0) { // corresponding disk file exists mtime = filestat.st_mtime + filestat.st_mtim.tv_nsec * nano; diff = fabs(mtime - Brec[ii].mtime); // compare disk and backup mod times if (diff < MODTIMETOLR) comp = 1; // equal within file system resolution dfiles1++; // count matching disk names dfiles2 += comp; // count matching mod times } wprintf(mLog,-2," %s \n",filespec); errmess = checkFile(filespec,comp,dcc1); // verify, get length, compare disk if (errmess) { wprintf(mLog,-1," *** %s \n",errmess); // log and count error wprintf(mLog,"\n"); if (strstr(errmess,"compare")) fcerrs++; // backup - disk compare failure else fverrs++; } vfiles++; // count files and bytes vbytes += dcc1; } if (Brec[ii].lover) for (vers = Brec[ii].lover; vers <= Brec[ii].hiver; vers++) // check previous versions { setFileVersion(filespec,vers); // append version if > 0 wprintf(mLog,-2," %s \n",filespec); errmess = checkFile(filespec,0,dcc1); // verify file, get length if (errmess) { wprintf(mLog,-1," *** %s \n",errmess); // log and count error wprintf(mLog,"\n"); fverrs++; } vfiles++; // count files and bytes vbytes += dcc1; } if (checkKillPause()) goto verify_exit; // killed by user if (fverrs + fcerrs > 100) goto verify_exit; // v.3.7 } } wprintf(mLog," backup files: %d (%s) \n",vfiles,formatKBMB(vbytes,3)); wprintf(mLog," backup file read errors: %d \n",fverrs); if (strnEqu(menu,"incremental",4)) wprintf(mLog," compare failures: %d \n",fcerrs); if (strnEqu(menu,"compare",4)) { wprintf(mLog," matching disk names: %d mod times: %d \n",dfiles1,dfiles2); wprintf(mLog," compare failures: %d \n",fcerrs); } secs = get_timer(time0); wprintf(mLog," verify time: %.1f secs \n",secs); vspeed = vbytes/mega/secs; wprintf(mLog," verify speed: %.2f MB/sec \n",vspeed); verify_exit: if (fverrs + fcerrs) wprintx(mLog,0," *** THERE WERE ERRORS *** \n",boldfont); else wprintx(mLog,0," NO ERRORS \n",boldfont); // v.3.9 wprintf(mLog," ready \n"); // v.3.6 killFlag = 0; gdk_window_set_cursor(mLogwin,0); // normal cursor return 0; } // various kinds of reports int Report(cchar *menu) { char *fspec1; char fspec2[200], bfile[maxfcc]; char *pslash, *pdirk, ppdirk[maxfcc]; char header[100]; int ii, kfiles, knew, kdel, kmod; int dii, bii, comp, err; double nbytes, mb1, mb2, fage; int vers, lover, hiver, nexpv; int age, loage, hiage; struct tm tmdt; time_t btime, dtime; char bmod[20], dmod[20]; const char *copy; struct stat64 filestat; // get all disk files in backup job // report file and byte counts per include and exclude record if (strEqu(menu, "get disk files")) { dGetFiles(); // get all files on disk wprintx(mLog,0,"\n"" files bytes filespec retention (days, vers) \n",boldfont); for (ii = 0; ii < BJnnx; ii++) { // formatted report if (BJfspec[ii]) { if (BJfiles[ii]) { if (BJrtype[ii] == 2) // include: add retention v.4.0 wprintf(mLog," %6d %9s %s (%d, %d) \n", BJfiles[ii],formatKBMB(BJbytes[ii],3),BJfspec[ii],BJretND[ii],BJretNV[ii]); else wprintf(mLog," %6d %9s %s \n",BJfiles[ii],formatKBMB(BJbytes[ii],3),BJfspec[ii]); } else if (BJrtype[ii] > 1) { wprintx(mLog,0," NO FILES",boldfont); wprintf(mLog," %s \n",BJfspec[ii]); } else wprintf(mLog," %s \n",BJfspec[ii]); } } wprintf(mLog," %6d %9s TOTALS \n", Dnf, formatKBMB(Dbytes,3)); goto report_exit; } // report disk / backup differences: new, modified, and deleted files if (strEqu(menu, "diffs summary")) { dGetFiles(); if (bGetFiles() < 0) goto report_exit; setFileDisps(); wprintf(mLog,"\n disk files: %d backup files: %d \n",Dnf,Bnf); wprintx(mLog,0,"\n Differences between files on disk and backup files: \n",boldfont); wprintf(mLog," %6d disk files not found on backup (new files) \n",nnew); wprintf(mLog," %6d files with different data (modified files) \n",nmod); wprintf(mLog," %6d backup files not found on disk (deleted files) \n",ndel); wprintf(mLog," %6d files with identical data (unchanged files) \n",nunc); wprintf(mLog," Total differences: %d files (%s new + modified) \n\n",Mfiles,formatKBMB(Mbytes,3)); goto report_exit; } // report disk / backup differences per directory level if (strEqu(menu, "diffs by directory")) { dGetFiles(); if (bGetFiles() < 0) goto report_exit; setFileDisps(); SortFileList((char *) Drec, sizeof(dfrec), Dnf, 'D'); // re-sort, directories first SortFileList((char *) Brec, sizeof(bfrec), Bnf, 'D'); wprintx(mLog,0,"\n differences by directory \n",boldfont); wprintx(mLog,0," new mod del bytes directory \n",boldfont); nbytes = kfiles = knew = kmod = kdel = 0; dii = bii = 0; while ((dii < Dnf) || (bii < Bnf)) // scan disk and backup files parallel { if ((dii < Dnf) && (bii == Bnf)) comp = -1; else if ((dii == Dnf) && (bii < Bnf)) comp = +1; else comp = filecomp(Drec[dii].file, Brec[bii].file); if (comp > 0) pdirk = Brec[bii].file; // get disk or backup file else pdirk = Drec[dii].file; pslash = (char *) strrchr(pdirk,'/'); // isolate directory if (pslash) *pslash = 0; if (strNeq(pdirk,ppdirk)) { // if directory changed, output if (kfiles > 0) // totals from prior directory wprintf(mLog," %5d %5d %5d %8s %s \n", knew,kmod,kdel,formatKBMB(nbytes,3),ppdirk); nbytes = kfiles = knew = kmod = kdel = 0; // reset totals strcpy(ppdirk,pdirk); // start new directory } if (pslash) *pslash = '/'; if (comp < 0) { // unmatched disk file: new knew++; // count new files kfiles++; nbytes += Drec[dii].size; dii++; } else if (comp > 0) { // unmatched backup file if (Brec[bii].disp == 'd') { kdel++; // count deleted files kfiles++; } bii++; } else if (comp == 0) { // file present on disk and backup if (Drec[dii].disp == 'm') kmod++; // count modified files if (Drec[dii].disp == 'n') knew++; // count new files (backup disp is 'v') if (Drec[dii].disp != 'u') { kfiles++; // count unless unchanged nbytes += Drec[dii].size; } dii++; bii++; } } if (kfiles > 0) wprintf(mLog," %5d %5d %5d %s %s \n", // totals from last directory knew,kmod,kdel,formatKBMB(nbytes,3),ppdirk); SortFileList((char *) Drec, sizeof(dfrec), Dnf, 'A'); // restore straight ascii sort SortFileList((char *) Brec, sizeof(bfrec), Bnf, 'A'); goto report_exit; } // report disk / backup differences by file status and directory if (strEqu(menu, "diffs by file status")) // new v.4.1 { dGetFiles(); if (bGetFiles() < 0) goto report_exit; setFileDisps(); SortFileList((char *) Drec, sizeof(dfrec), Dnf, 'D'); // re-sort, directories first SortFileList((char *) Brec, sizeof(bfrec), Bnf, 'D'); wprintx(mLog,0,"\n new files by directory \n",boldfont); // report new files wprintx(mLog,0," files bytes directory \n",boldfont); nbytes = knew = 0; dii = bii = 0; while ((dii < Dnf) || (bii < Bnf)) // scan disk and backup files parallel { if ((dii < Dnf) && (bii == Bnf)) comp = -1; else if ((dii == Dnf) && (bii < Bnf)) comp = +1; else comp = filecomp(Drec[dii].file, Brec[bii].file); if (comp > 0) pdirk = Brec[bii].file; // get disk or backup file else pdirk = Drec[dii].file; pslash = (char *) strrchr(pdirk,'/'); // isolate directory if (pslash) *pslash = 0; if (strNeq(pdirk,ppdirk)) { // if directory changed, output if (knew > 0) // totals from prior directory wprintf(mLog," %6d %8s %s \n", knew,formatKBMB(nbytes,3),ppdirk); nbytes = knew = 0; // reset totals strcpy(ppdirk,pdirk); // start new directory } if (pslash) *pslash = '/'; if (comp < 0) { // unmatched disk file: new knew++; // count new files nbytes += Drec[dii].size; dii++; } else if (comp > 0) // unmatched backup file: deleted bii++; else if (comp == 0) { // file present on disk and backup if (Drec[dii].disp == 'n') { // count new files (backup disp is 'v') knew++; nbytes += Drec[dii].size; } dii++; bii++; } } if (knew > 0) wprintf(mLog," %6d %8s %s \n", // totals from last directory knew,formatKBMB(nbytes,3),ppdirk); wprintx(mLog,0,"\n modified files by directory \n",boldfont); // report modified files wprintx(mLog,0," files bytes directory \n",boldfont); nbytes = kmod = 0; dii = bii = 0; while ((dii < Dnf) || (bii < Bnf)) // scan disk and backup files parallel { if ((dii < Dnf) && (bii == Bnf)) comp = -1; else if ((dii == Dnf) && (bii < Bnf)) comp = +1; else comp = filecomp(Drec[dii].file, Brec[bii].file); if (comp > 0) pdirk = Brec[bii].file; // get disk or backup file else pdirk = Drec[dii].file; pslash = (char *) strrchr(pdirk,'/'); // isolate directory if (pslash) *pslash = 0; if (strNeq(pdirk,ppdirk)) { // if directory changed, output if (kmod > 0) // totals from prior directory wprintf(mLog," %6d %8s %s \n", kmod,formatKBMB(nbytes,3),ppdirk); nbytes = kmod = 0; // reset totals strcpy(ppdirk,pdirk); // start new directory } if (pslash) *pslash = '/'; if (comp < 0) // unmatched disk file: new dii++; else if (comp > 0) // unmatched backup file: deleted bii++; else if (comp == 0) { // file present on disk and backup if (Drec[dii].disp == 'm') { // count modified files kmod++; nbytes += Drec[dii].size; } dii++; bii++; } } if (kmod > 0) wprintf(mLog," %6d %8s %s \n", // totals from last directory kmod,formatKBMB(nbytes,3),ppdirk); wprintx(mLog,0,"\n deleted files by directory \n",boldfont); // report deleted files wprintx(mLog,0," files bytes directory \n",boldfont); nbytes = kdel = 0; dii = bii = 0; while ((dii < Dnf) || (bii < Bnf)) // scan disk and backup files parallel { if ((dii < Dnf) && (bii == Bnf)) comp = -1; else if ((dii == Dnf) && (bii < Bnf)) comp = +1; else comp = filecomp(Drec[dii].file, Brec[bii].file); if (comp > 0) pdirk = Brec[bii].file; // get disk or backup file else pdirk = Drec[dii].file; pslash = (char *) strrchr(pdirk,'/'); // isolate directory if (pslash) *pslash = 0; if (strNeq(pdirk,ppdirk)) { // if directory changed, output if (kdel > 0) // totals from prior directory wprintf(mLog," %6d %8s %s \n", kdel,formatKBMB(nbytes,3),ppdirk); nbytes = kdel = 0; // reset totals strcpy(ppdirk,pdirk); // start new directory } if (pslash) *pslash = '/'; if (comp < 0) // unmatched disk file: new dii++; else if (comp > 0) { // unmatched backup file: deleted if (Brec[bii].disp == 'd') { kdel++; // count deleted files nbytes += Brec[bii].size; } bii++; } else if (comp == 0) { // file present on disk and backup dii++; bii++; } } if (kdel > 0) wprintf(mLog," %6d %8s %s \n", // totals from last directory kdel,formatKBMB(nbytes,3),ppdirk); SortFileList((char *) Drec, sizeof(dfrec), Dnf, 'A'); // restore straight ascii sort SortFileList((char *) Brec, sizeof(bfrec), Bnf, 'A'); goto report_exit; } // report disk / backup differences by file if (strEqu(menu, "diffs by file")) { dGetFiles(); if (bGetFiles() < 0) goto report_exit; setFileDisps(); wprintx(mLog,0,"\n Detailed list of disk:backup differences: \n",boldfont); snprintf(header,99,"\n %d disk files not found on backup \n",nnew); wprintx(mLog,0,header,boldfont); for (ii = 0; ii < Dnf; ii++) { if (Drec[ii].disp != 'n') continue; wprintf(mLog," %s \n",Drec[ii].file); } snprintf(header,99,"\n %d backup files not found on disk \n",ndel); wprintx(mLog,0,header,boldfont); for (ii = 0; ii < Bnf; ii++) { if (Brec[ii].disp != 'd') continue; wprintf(mLog," %s \n",Brec[ii].file); } snprintf(header,99,"\n %d files with different data \n",nmod); wprintx(mLog,0,header,boldfont); wprintx(mLog,0," backup mod date copy disk mod date filespec \n",boldfont); dii = bii = 0; while ((dii < Dnf) || (bii < Bnf)) // scan disk and backup files parallel { // revised v.25 if ((dii < Dnf) && (bii == Bnf)) comp = -1; else if ((dii == Dnf) && (bii < Bnf)) comp = +1; else comp = strcmp(Drec[dii].file, Brec[bii].file); if (comp < 0) { dii++; continue; } // next disk file if (comp > 0) { bii++; continue; } // next backup file if (Drec[dii].disp == 'm') // screen for modified status { btime = int(Brec[bii].mtime); // mod time on backup dtime = int(Drec[dii].mtime); // mod time on disk copy = "<<<<"; // copy direction, disk to backup if (btime > dtime) copy = "!!!!"; // flag if backup to disk tmdt = *localtime(&btime); snprintf(bmod,19,"%4d.%02d.%02d-%02d:%02d",tmdt.tm_year+1900, tmdt.tm_mon+1,tmdt.tm_mday,tmdt.tm_hour,tmdt.tm_min); tmdt = *localtime(&dtime); snprintf(dmod,19,"%4d.%02d.%02d-%02d:%02d",tmdt.tm_year+1900, tmdt.tm_mon+1,tmdt.tm_mday,tmdt.tm_hour,tmdt.tm_min); wprintf(mLog," %s %s %s %s \n",bmod,copy,dmod,Drec[dii].file); } dii++; // next disk and backup files bii++; } goto report_exit; } // report versions and expired versions per file if (strEqu(menu, "file versions")) { Report("diffs summary"); if (Bnf < 1) goto report_exit; wprintx(mLog,0,"\n lover hiver nxver loage hiage bytes expired filespec \n",boldfont); for (ii = 0; ii < Bnf; ii++) { lover = Brec[ii].lover; hiver = Brec[ii].hiver; nexpv = Brec[ii].nexpv; if (! lover) continue; strcpy(bfile,BJdirk); strcat(bfile,Brec[ii].file); loage = hiage = -1; mb1 = mb2 = 0.0; for (vers = lover; vers <= hiver; vers++) // loop file versions { setFileVersion(bfile,vers); err = lstat64(bfile,&filestat); // check file exists on backup if (err) continue; fage = (time(0)-filestat.st_mtime)/24.0/3600.0; // file age in days age = fage; // remove fraction if (loage < 0) loage = hiage = age; if (age < loage) loage = age; if (age > hiage) hiage = age; mb1 += filestat.st_size; // accumulate total bytes if (vers < lover + nexpv) // v.4.0 mb2 += filestat.st_size; // and expired version bytes } wprintf(mLog," %5d %5d %5d %5d %5d %8s %8s %s \n", lover,hiver,nexpv,loage,hiage,formatKBMB(mb1,3),formatKBMB(mb2,3),Brec[ii].file); } goto report_exit; } // report expired file versions (will be purged) if (strEqu(menu, "expired versions")) { Report("diffs summary"); if (Bnf < 1) goto report_exit; wprintx(mLog,0,"\n expired files (will purge from backup location) \n",boldfont); wprintx(mLog,0,"\n vers age bytes filespec \n",boldfont); for (ii = 0; ii < Bnf; ii++) { lover = Brec[ii].lover; hiver = Brec[ii].hiver; nexpv = Brec[ii].nexpv; if (! nexpv) continue; strcpy(bfile,BJdirk); strcat(bfile,Brec[ii].file); mb1 = 0.0; for (vers = lover; vers < lover + nexpv; vers++) // loop expired file versions v.4.0 { setFileVersion(bfile,vers); err = lstat64(bfile,&filestat); // check file exists on backup if (err) continue; fage = (time(0)-filestat.st_mtime)/24.0/3600.0; // age in days, size in MB age = fage; mb1 = filestat.st_size; wprintf(mLog," %5d %5d %8s %s \n",vers,age,formatKBMB(mb1,3),Brec[ii].file); } } goto report_exit; } // list all files in backup job set if (strEqu(menu, "list disk files")) { wprintx(mLog,0," List all files in backup file set: \n",boldfont); dGetFiles(); wprintf(mLog," %d files found \n",Dnf); for (ii = 0; ii < Dnf; ii++) wprintf(mLog," %s \n",Drec[ii].file); goto report_exit; } // list all files on backup if (strEqu(menu, "list backup files")) { wprintx(mLog,0," List all files at backup location \n",boldfont); if (bGetFiles() < 0) goto report_exit; for (ii = 0; ii < Bnf; ii++) { if (Brec[ii].lover) wprintf(mLog," %s (+ vers %d-%d) \n", Brec[ii].file, Brec[ii].lover, Brec[ii].hiver); else wprintf(mLog," %s \n",Brec[ii].file); } goto report_exit; } // search disk and backup file list for match with wild search pattern if (strEqu(menu, "find files")) { wprintx(mLog,0," Find files matching wildcard pattern \n",boldfont); dGetFiles(); bGetFiles(); if (!(Dnf + Bnf)) goto report_exit; fspec1 = zdialog_text(mWin,"enter (wildcard) filespec:","/dir*/file* "); if (! fspec1) goto report_exit; strncpy0(fspec2,fspec1,199); free(fspec1); strTrim(fspec2); if (! *fspec2) goto report_exit; wprintf(mLog,"\n matching disk files: \n"); for (ii = 0; ii < Dnf; ii++) if (MatchWild(fspec2,Drec[ii].file) == 0) wprintf(mLog," %s \n",Drec[ii].file); wprintf(mLog,"\n matching backup files: \n"); for (ii = 0; ii < Bnf; ii++) { if (MatchWild(fspec2,Brec[ii].file) == 0) { if (Brec[ii].hiver) wprintf(mLog," %s (+ vers %d-%d) \n", Brec[ii].file, Brec[ii].lover, Brec[ii].hiver); else wprintf(mLog," %s \n",Brec[ii].file); } } goto report_exit; } report_exit: wprintf(mLog," ready \n"); // v.3.6 return 0; } // edit dialog for file restore int RJedit_fchooser(cchar *dirk); zdialog *RJedit_fchooser_zd = 0; int RJedit(cchar *menu) { int RJedit_dialog_event(zdialog *zd, cchar *event); zdialog *zd; wprintf(mLog,"\n Restore files from backup \n"); if (bGetFiles() < 0) return 0; // get files in backup location wprintf(mLog," %d backup files found \n",Bnf); if (! Bnf) return 0; zd = zdialog_new("restore files from backup",mWin,"browse","done","cancel",null); zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=10"); zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog"); zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|space=5|expand"); zdialog_add_widget(zd,"label","labfrom","vb1","copy-from backup"); // copy-from backup [_____________] zdialog_add_widget(zd,"label","labto","vb1","copy-to disk"); // copy-to disk [_____________] zdialog_add_widget(zd,"entry","entfrom","vb2",RJfrom); zdialog_add_widget(zd,"entry","entto","vb2",RJto); zdialog_add_widget(zd,"hsep","hsep1","dialog"); zdialog_add_widget(zd,"label","labf","dialog","files to restore"); // files to restore zdialog_add_widget(zd,"frame","framef","dialog",0,"expand"); // scrolling edit window zdialog_add_widget(zd,"scrwin","scrf","framef"); zdialog_add_widget(zd,"edit","editf","scrf"); editwidget = zdialog_widget(zd,"editf"); for (int ii = 0; ii < RJnnx; ii++) // get restore include/exclude recs, { // pack into file selection edit box if (RJrtype[ii] == 2) wprintf(editwidget,"include %s\n",RJfspec[ii]); if (RJrtype[ii] == 3) wprintf(editwidget,"exclude %s\n",RJfspec[ii]); } zdialog_resize(zd,400,400); zdialog_run(zd,RJedit_dialog_event,"20/20"); // run dialog posn v.4.1 zdialog_wait(zd); return 0; } // dialog completion function // get restore job data from dialog widgets and validate int RJedit_dialog_event(zdialog *zd, cchar *event) { DIR *pdirk; char *pp, *fspec, rdirk[300]; int ftf = 1, cc, rtype, nerrs = 0; int zstat, days, vers; const char *errmess = 0; zstat = zd->zstat; if (! zstat) return 0; // wait for dialog end zd->zstat = 0; // this dialog continues if (RJedit_fchooser_zd) // kill file chooser dialog if active zdialog_free(RJedit_fchooser_zd); if (zstat == 1) { // browse button, file-chooser dialog zdialog_fetch(zd,"entfrom",RJfrom,299); // copy-from location /dirk/xxx/.../ strTrim(RJfrom); strcpy(rdirk,BJdirk); // start at /media/xxx/dirk/xxx/ strncat(rdirk,RJfrom,299); RJedit_fchooser(rdirk); // do file chooser dialog return 0; } if (zstat != 1 && zstat != 2) { // cancel or destroy zdialog_free(zd); return 0; } RJreset(); // edit done, reset job data zdialog_fetch(zd,"entfrom",RJfrom,299); // copy-from location /dirk/xxx/.../ strTrim(RJfrom); strcpy(rdirk,BJdirk); // validate copy-from location strncat(rdirk,RJfrom,299); // /media/xxx/dirk/... pdirk = opendir(rdirk); if (! pdirk) { zmessageACK(0,0,"invalid copy-from location"); // v.4.1 nerrs++; } else closedir(pdirk); cc = strlen(RJfrom); // insure '/' at end if (RJfrom[cc-1] != '/') strcat(RJfrom,"/"); zdialog_fetch(zd,"entto",RJto,299); // copy-to location /dirk/yyy/.../ strTrim(RJto); pdirk = opendir(RJto); // validate copy-to location if (! pdirk) { zmessageACK(0,0,"invalid copy-to location"); // v.4.1 nerrs++; } else closedir(pdirk); cc = strlen(RJto); // insure '/' at end if (RJto[cc-1] != '/') strcat(RJto,"/"); for (RJnnx = 0; RJnnx < maxnx; RJnnx++) // include/exclude recs from edit box { pp = wscanf(editwidget,ftf); // next record from edit widget if (! pp) break; wprintf(mLog," %s \n",pp); errmess = parseNXrec(pp,rtype,fspec,days,vers); // validate include/exclude rec. if (errmess) { zmessageACK(0,0,"%s \n %s",pp,errmess); // v.4.1 nerrs++; } RJrtype[RJnnx] = rtype; // save job record RJfspec[RJnnx] = fspec; } if (RJnnx == maxnx) { zmessageACK(0,0,"max job records exceeded"); nerrs++; } if (nerrs == 0) RJvalid = 1; if (RJvalid) { // all OK rGetFiles(); // get files to restore zdialog_free(zd); // destroy dialog } else zd->zstat = 0; // errors, keep dialog open v.4.1 return 0; } // file chooser dialog for restore job edit int RJedit_fchooser(cchar *dirk) // v.3.5 { int RJedit_fchooser_event(zdialog *zd, const char *event); RJedit_fchooser_zd = zdialog_new("Choose Files to Restore",mWin,"Done",null); zdialog *zd = RJedit_fchooser_zd; zdialog_add_widget(zd,"frame","fr1","dialog",0,"expand"); zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5"); zdialog_add_widget(zd,"label","space","hb1",0,"expand"); zdialog_add_widget(zd,"check","hidden","hb1","show hidden","space=5"); zdialog_add_widget(zd,"button","incl","hb1","include","space=5"); zdialog_add_widget(zd,"button","excl","hb1","exclude","space=5"); fc_widget = gtk_file_chooser_widget_new(GTK_FILE_CHOOSER_ACTION_OPEN); GtkWidget *frame = zdialog_widget(zd,"fr1"); gtk_container_add(GTK_CONTAINER(frame),fc_widget); gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(fc_widget),dirk); gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(fc_widget),1); zdialog_resize(zd,550,500); zdialog_run(zd,RJedit_fchooser_event,"50/50"); // posn v.4.1 zdialog_wait(zd); zdialog_free(zd); RJedit_fchooser_zd = 0; return 0; } int RJedit_fchooser_event(zdialog *zd, const char *event) { GSList *flist = 0; struct stat64 filestat; char *file1, *file2, rdirk[300]; int ii, rdcc, err; if (strEqu(event,"hidden")) { // show/hide hidden files v.3.7.1 zdialog_fetch(zd,"hidden",ii); gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(fc_widget),ii); } if (strEqu(event,"incl") || strEqu(event,"excl")) // include or exclude { strcpy(rdirk,BJdirk); // copy-from location v.3.9 strncat(rdirk,RJfrom,299); // /media/xxx/dirk/... rdcc = strlen(rdirk); flist = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(fc_widget)); for (ii = 0; ; ii++) // process selected files { file1 = (char *) g_slist_nth_data(flist,ii); if (! file1) break; if (! strnEqu(rdirk,file1,rdcc)) { // check file in backup location v.3.9 zmessageACK(0,0,"not in copy-from directory: \n %s",file1); // v.4.1 continue; } err = lstat64(file1,&filestat); if (err) { zmessageACK(0,0,"error: %s file: \n %s",strerror(errno),file1); continue; } file2 = strdupz(file1,2); // extra space for wildcard g_free(file1); if (S_ISDIR(filestat.st_mode)) strcat(file2,"/*"); // if directory, append wildcard if (strEqu(event,"incl")) wprintf(editwidget,"include %s""\n",file2 + BJdcc); // omit backup mount point if (strEqu(event,"excl")) wprintf(editwidget,"exclude %s""\n",file2 + BJdcc); free(file2); } gtk_file_chooser_unselect_all(GTK_FILE_CHOOSER(fc_widget)); g_slist_free(flist); } return 0; } // list and validate backup files to be restored int RJlist(cchar *menu) { int cc1, cc2, errs = 0; char *file1, file2[maxfcc]; if (! RJvalid) wprintf(mLog," *** restore job has errors \n"); if (! Rnf) goto rjlist_exit; wprintf(mLog,"\n copy %d files from backup: %s \n",Rnf, RJfrom); wprintf(mLog," to directory: %s \n",RJto); wprintf(mLog,"\n resulting files will be the following: \n"); cc1 = strlen(RJfrom); // from: /dirk/xxx/.../ cc2 = strlen(RJto); // to: /dirk/yyy/.../ for (int ii = 0; ii < Rnf; ii++) { file1 = Rrec[ii].file; if (! strnEqu(file1,RJfrom,cc1)) { wprintf(mLog," *** not within copy-from: %s \n",file1); errs++; continue; } strcpy(file2,RJto); strcpy(file2+cc2,file1+cc1); wprintf(mLog," %s \n",file2); } if (errs) { wprintf(mLog," *** %d errors \n",errs); RJvalid = 0; } rjlist_exit: wprintf(mLog," ready \n"); // v.3.6 return 0; } // restore files based on data from restore dialog int Restore(cchar *menu) { int ii, nn, ccf; char dfile[maxfcc]; const char *errmess = 0; if (! RJvalid || ! Rnf) { wprintf(mLog," *** restore job has errors \n"); goto restore_exit; } nn = zmessageYN(mWin,"Restore %d files from: %s%s \n to: %s \n" "Proceed with file restore ?",Rnf,BJdirk,RJfrom,RJto); if (! nn) goto restore_exit; snprintf(dfile,maxfcc-2,"\n""begin restore of %d files to: %s \n",Rnf,RJto); wprintx(mLog,0,dfile,boldfont); gdk_window_set_cursor(mLogwin,watchcursor); // busy cursor v.4.1 ccf = strlen(RJfrom); // from: /media/xxx/filespec for (ii = 0; ii < Rnf; ii++) { strcpy(dfile,RJto); // to: /destination/filespec strcat(dfile,Rrec[ii].file + ccf); wprintf(mLog," %s \n",dfile); errmess = copyFile(Rrec[ii].file,dfile,1); if (errmess) wprintf(mLog," *** %s \n",errmess); else Rrec[ii].finc = 1; if (checkKillPause()) goto restore_exit; } synch_poop("restore"); // synch owner and permissions data restore_exit: wprintf(mLog," ready \n"); // v.3.6 killFlag = 0; gdk_window_set_cursor(mLogwin,0); // normal cursor return 0; } // format disk backup device with vfat or ext2 file system // uses existing partitions only - no changes to partition table // v.3.3.1 int Format(cchar *menu) { int ii, jj, zstat, yn, contx = 0; char text[200], device[20], filesys[20], label[20], *crec; zdialog *zd; FILE *fid; wprintf(mLog,"\n Format a backup device \n"); zd = zdialog_new("format backup device",mWin,"start","cancel",null); zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=10"); zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog"); zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|expand"); // backup device [________][v] zdialog_add_widget(zd,"label","labdev","vb1"," backup device"); // device label [________] zdialog_add_widget(zd,"comboE","entdev","vb2"); // file system [________][v] zdialog_add_widget(zd,"label","lablab","vb1"," device label"); zdialog_add_widget(zd,"entry","entlab","vb2","ukopp"); zdialog_add_widget(zd,"label","labfs","vb1"," file system"); zdialog_add_widget(zd,"comboE","entfs","vb2","ext2"); unmount(0); // unmount mounted device BDpoop(); // refresh available devices for (ii = 0; ii < Ndisk; ii++) // load combo box with device { strcpy(text,diskdev[ii]); // /dev/xxxx description strncatv(text,199," ",diskdesc[ii],null); zdialog_cb_app(zd,"entdev",text); } zdialog_cb_app(zd,"entfs","ext2"); // load combo box with file systems zdialog_cb_app(zd,"entfs","vfat"); zdialog_resize(zd,300,0); zdialog_run(zd); // run dialog zstat = zdialog_wait(zd); zdialog_free(zd); if (zstat != 1) return 0; zdialog_fetch(zd,"entdev",device,19); // get chosen device and file system zdialog_fetch(zd,"entfs",filesys,19); zdialog_fetch(zd,"entlab",label,19); for (ii = 1; device[ii] > ' '; ii++); // strip off device description if (ii > 19) ii = 19; device[ii] = 0; yn = zmessageYN(mWin,"device: %s label: %s file sys: %s \n" "WARNING: all data will be lost! \n" "Proceed with formatting?",device,label,filesys); if (! yn) goto format_exit; wprintf(mLog," formatting %s with file system %s \n",device,filesys); fid = fopen(TFformatscript,"w"); if (! fid) { wprintf(mLog," *** cannot create format script file \n"); goto format_exit; } gdk_window_set_cursor(mLogwin,watchcursor); // busy cursor v.4.1 fprintf(fid,"umount %s \n",device); // unmount /dev/xxxx fprintf(fid,"sleep 2 \n"); if (*filesys == 'v') fprintf(fid,"mkfs -t vfat -F 32 -n %s %s \n",label,device); // make vfat file system if (*filesys == 'e') fprintf(fid,"mkfs -t ext2 -L %s %s \n",label,device); // or ext2 file system fprintf(fid,"exit 0 \n"); fclose(fid); chmod(TFformatscript,0744); while ((crec = command_output(contx,TFformatscript))) // v.3.3.1 { zsleep(0.1); // throttle a little for (ii = jj = 0; crec[jj]; jj++) { // get rid of weird characters if (crec[jj] < ' ') continue; // in mkfs output crec[ii] = crec[jj]; ii++; } crec[ii] = 0; wprintf(mLog," format: %s \n",crec); // print command output } format_exit: wprintf(mLog," ready \n"); // v.3.6 gdk_window_set_cursor(mLogwin,0); // normal cursor return 0; } // display help/about or help/contents int helpFunc(cchar *menu) { if (strEqu(menu,"about")) { wprintf(mLog," %s \n",ukopp_title); wprintf(mLog," free software: %s \n",ukopp_license); } if (strEqu(menu,"contents")) showz_userguide(); return 0; } // Mount target device. Return 1 if success, else 0. // menu caller: menu arg is present // internal caller: menu arg is 0 int mount(cchar *menu) // more error checking v.3.5 { int ii, err, cc; char ch, work[300]; const char *errmess; struct stat statb; bFilesReset(); // clear file data at backup location BDpoop(); // refresh device data snprintf(work,299,"%s %s",BJdev,BJdirk); errmess = parseTarget(work); // target device and directory if (errmess) { // in conflict with current wprintf(mLog," *** %s \n",errmess); // mount status return 0; } for (ii = 0; ii < Ndisk; ii++) // see if device is mounted if (strEqu(BJdev,diskdev[ii])) break; if (ii < Ndisk && *diskmp[ii] == '/') { // yes cc = strlen(diskmp[ii]); if (strnEqu(diskmp[ii],BJdirk,cc)) { ch = BJdirk[cc]; if (! ch || ch == '/') { devMounted = 1; // target directory is on device if (menu) wprintf(mLog," already mounted \n"); return 1; } } wprintf(mLog," *** target directory not on device \n"); } err = stat(BJdirk,&statb); // directory exists? if (err && *BJdev) { // device but no directory snprintf(work,299,"mkdir -p %s",BJdirk); // create mount point err = do_shell("mkdir",work); if (err) return 0; ukoppMpoint++; // remember created by me } if (! err && ! *BJdev) return 1; // no device, directory OK, use it snprintf(work,299,"mount -noatime %s %s",BJdev,BJdirk); // mount device at target directory err = do_shell("mount",work); if (err) return 0; ukoppMounted++; // remember mounted by me devMounted = 1; strcpy(mountdev,BJdev); // save mount poop strcpy(mountdirk,BJdirk); return 1; } // unmount target device int unmount(cchar *menu) // revised v.3.5.1 { int err; char work[200]; struct stat statb; bFilesReset(); // no files at backup location sleep(1); if (*mountdirk) snprintf(work,199,"umount %s",mountdirk); // unmount unconditionally else snprintf(work,199,"umount %s",BJdev); do_shell("umount",work); sleep(1); err = stat(mountdirk,&statb); // remove directory if (! err && ukoppMpoint) { // only if it exists snprintf(work,199,"rmdir %s",mountdirk); // and created by me do_shell("rmdir",work); } devMounted = ukoppMounted = ukoppMpoint = 0; *mountdev = *mountdirk = 0; BDpoop(); // refresh device data return 0; } // save logging window as text file int saveScreen(cchar *menu) { wfilesave(mLog); return 0; } // backup helper function // write date and time to temp file int writeDT() { time_t dt1; char *dt2; FILE *fid; int cc; time(&dt1); dt2 = ctime(&dt1); // get string date-time cc = strlen(dt2); if (cc && (dt2[cc-1] == '\n')) dt2[cc-1] = 0; // save without trailing \n fid = fopen(TFdatetime,"w"); if (! fid) zappcrash("cannot open scratch file %s",TFdatetime); fprintf(fid,"%s \n",dt2); fclose(fid); return 0; } // synchronize owner and permissions data using poopfile at backup location v.26 // - for files copied backup >> disk, set owner and permissions from poopfile // - refresh poopfile data from disk files // mode is "backup" "restore" or "synch" int synch_poop(const char *mode) { int ii, err, nn, uid, gid, perms; int cc, ccf, cct; char file[maxfcc], file2[maxfcc]; char dirk[maxfcc], pdirk[maxfcc]; char *pp, poopfile[100]; const char *errmess = 0; FILE *fid; struct stat64 dstat; if (strEqu(mode,"synch")) // set poop for updated disk files { strcpy(poopfile,BJdirk); strcat(poopfile,BD_POOPFILE); fid = fopen(poopfile,"r"); // open poopfile if (! fid) { wprintf(mLog," *** no owner/permissions file: %s \n",poopfile); return 0; } ii = 0; while (true) // read poopfile records { nn = fscanf(fid,"%d:%d %o %[^\n]",&uid,&gid,&perms,file); // uid, gid, perms, file or directory if (nn == EOF) break; if (nn != 4) continue; cc = strlen(file); while (ii < Bnf) // match poopfile file or directory { // to backup files copied to disk nn = strncmp(Brec[ii].file,file,cc); // (logic assumes ascii sort) if (nn >= 0) break; ii++; } if (ii == Bnf) break; // EOL if (nn > 0) continue; // file not in backup file list if (Brec[ii].finc == 0) continue; // file not copied to disk wprintf(mLog," set owner/perms %d:%d %04o %s \n",uid,gid,perms,file); err = chown(file,uid,gid); if (err) wprintf(mLog," *** error: %s \n",strerror(errno)); err = chmod(file,perms); if (err) wprintf(mLog," *** error: %s \n",strerror(errno)); } fclose(fid); } if (strEqu(mode,"restore")) // set poop for restored disk files { strcpy(poopfile,BJdirk); strcat(poopfile,BD_POOPFILE); fid = fopen(poopfile,"r"); if (! fid) { wprintf(mLog," *** no owner/permissions file: %s \n",poopfile); return 0; } ccf = strlen(RJfrom); cct = strlen(RJto); ii = 0; while (true) { nn = fscanf(fid,"%d:%d %o %[^\n]",&uid,&gid,&perms,file); if (nn == EOF) break; if (nn != 4) continue; cc = strlen(file); if (cc <= ccf) continue; while (ii < Rnf) { nn = strncmp(Rrec[ii].file,file,cc); if (nn >= 0) break; ii++; } if (ii == Rnf) break; if (nn > 0) continue; if (Rrec[ii].finc == 0) continue; strcpy(file2,RJto); // offset restore 'from' and 'to' paths strcpy(file2 + cct, file + ccf); wprintf(mLog," set owner/perms %d:%d %04o %s \n",uid,gid,perms,file2); err = chown(file2,uid,gid); if (err) wprintf(mLog," *** error: %s \n",strerror(errno)); err = chmod(file2,perms); if (err) wprintf(mLog," *** error: %s \n",strerror(errno)); } fclose(fid); } if (strEqu(mode,"backup") || strEqu(mode,"synch")) // make new poop file from disk files { fid = fopen(TFpoopfile,"w"); if (! fid) zappcrash("cannot open temp file %s",TFpoopfile); *pdirk = 0; // no prior directory for (ii = 0; ii < Dnf; ii++) { strcpy(dirk,Drec[ii].file); // next file on disk pp = dirk; while (true) // set directory owner & permissions { pp = strchr(pp+1,'/'); // next (last) directory level if (! pp) break; cc = pp - dirk + 1; // cc incl. '/' if (strncmp(dirk,pdirk,cc) == 0) continue; // matches prior, skip *pp = 0; // terminate this directory level err = lstat64(dirk,&dstat); // get owner and permissions v.3.0 if (err) { wprintf(mLog," *** error: %s file: %s \n",strerror(errno),dirk); break; } dstat.st_mode = dstat.st_mode & 0777; fprintf(fid,"%4d:%4d %3o %s/\n", // output uid:gid perms directory/ dstat.st_uid, dstat.st_gid, dstat.st_mode, dirk); *pp = '/'; // restore '/' } strcpy(pdirk,dirk); // prior = this directory strcpy(file,Drec[ii].file); // disk file, again err = lstat64(file,&dstat); // get owner and permissions v.3.0 if (err) { wprintf(mLog," *** error: %s file: %s \n",strerror(errno),file); continue; } dstat.st_mode = dstat.st_mode & 0777; fprintf(fid,"%4d:%4d %3o %s\n", // output uid:gid perms file dstat.st_uid, dstat.st_gid, dstat.st_mode, file); } fclose(fid); errmess = copyFile(TFpoopfile,BD_POOPFILE,2); // copy file owner/permissions file if (errmess) wprintf(mLog," *** poopfile error: %s \n",errmess); } return 0; } // get all disk files specified by include/exclude records // save in Drec[] array int dGetFiles() { const char *fsp, *psep2; char *fspec, *pp, *psep1; int ftf, wstat, cc, err, dups; int rtype, ii, jj, st, nfiles; int fcc, vers; double nbytes; struct stat64 filestat; dFilesReset(); wprintx(mLog,0,"\n""generating backup file set \n",boldfont); for (ii = 0; ii < BJnnx; ii++) // process include/exclude recs { BJfiles[ii] = 0; // initz. include/exclude rec stats BJbytes[ii] = 0.0; if (! BJfspec[ii]) continue; // v.4.6 if (*BJfspec[ii] == '"') { // v.4.1 fspec = strdup(BJfspec[ii]+1); // unquote quoted filespec pp = strrchr(fspec,'"'); if (pp) *pp = 0; } else fspec = strdup(BJfspec[ii]); rtype = BJrtype[ii]; if (rtype == 2 || rtype == 3) // include or exclude filespec { err = stat64(fspec,&filestat); if (! err && S_ISDIR(filestat.st_mode)) { // if directory, append /* v.4.1 cc = strlen(fspec) - 1; // (BJfspec has the extra length) if (fspec[cc] != '/') cc++; strcpy(fspec+cc,"/*"); } } if (rtype == 2) // include filespec { ftf = 1; while (1) { fsp = SearchWild(fspec,ftf); // find matching files if (! fsp) break; Drec[Dnf].file = strdup(fsp); err = lstat64(fsp,&filestat); // check accessibility if (! err) { Drec[Dnf].err = 0; if (! S_ISREG(filestat.st_mode) && // reg. files + symlinks only v.3.0 ! S_ISLNK(filestat.st_mode)) continue; } else Drec[Dnf].err = errno; // save file error status fcc = strlen(fsp); psep1 = (char *) strstr(fsp+fcc-10,VSEP1); // look for file version v.3.2 if (psep1) { // (char *) fix gcc error v.3.4.1 vers = 0; st = convSI(psep1+2,vers,&psep2); // if format not valid, take if (st < 2) vers = 1; // as non-versioned file if (strNeq(psep2,VSEP2)) vers = 0; if (*(psep2+1)) vers = 0; // VSEP2 must be at end if (vers) { wprintf(mLog," *** omit versioned file: %s \n",fsp); continue; } } Drec[Dnf].jindx = ii; // save pointer to include record Drec[Dnf].size = filestat.st_size; // save file size Drec[Dnf].mtime = filestat.st_mtime // save last mod time + filestat.st_mtim.tv_nsec * nano; // (nanosec resolution) if (err) Drec[Dnf].size = Drec[Dnf].mtime = 0; // inaccessible file Drec[Dnf].finc = 0; // not copied yet Drec[Dnf].bindx = -1; // no link to backup record yet v.4.0 BJfiles[ii]++; // count included files and bytes BJbytes[ii] += Drec[Dnf].size; if (++Dnf == maxfs) { wprintf(mLog," *** max files exceeded \n"); goto errret; // v.4.4 } } } if (rtype == 3) // exclude filespec { for (jj = 0; jj < Dnf; jj++) // check all included files (SO FAR) { if (! Drec[jj].file) continue; wstat = MatchWild(fspec,Drec[jj].file); if (wstat != 0) continue; BJfiles[ii]--; // un-count excluded file and bytes BJbytes[ii] -= Drec[jj].size; free(Drec[jj].file); // clear file data entry Drec[jj].file = 0; Drec[jj].err = 0; } } free(fspec); // v.4.1 } // end of include/exclude recs for (ii = 0; ii < Dnf; ii++) // list and remove error files { // (after excluded files removed) if (Drec[ii].err) { wprintf(mLog," *** %s omit: %s \n",strerror(Drec[ii].err),Drec[ii].file); jj = Drec[ii].jindx; BJfiles[jj]--; // un-count file and bytes BJbytes[jj] -= Drec[ii].size; free(Drec[ii].file); Drec[ii].file = 0; } } ii = jj = 0; // repack file arrays after deletions while (ii < Dnf) { if (Drec[ii].file == 0) ii++; else { if (ii > jj) { if (Drec[jj].file) free(Drec[jj].file); Drec[jj] = Drec[ii]; Drec[ii].file = 0; } ii++; jj++; } } Dnf = jj; // final file count in backup set Dbytes = 0.0; for (ii = 0; ii < Dnf; ii++) Dbytes += Drec[ii].size; // compute total bytes from files nfiles = 0; nbytes = 0.0; for (ii = 0; ii < BJnnx; ii++) // compute total files and bytes { // from include/exclude recs nfiles += BJfiles[ii]; nbytes += BJbytes[ii]; } wprintf(mLog," disk files: %d %s \n",nfiles,formatKBMB(nbytes,3)); if ((nfiles != Dnf) || (Dbytes != nbytes)) { // must match wprintf(mLog," *** bug: nfiles: %d Dnf: %d \n",nfiles,Dnf); wprintf(mLog," nbytes: %.0f Dbytes: %.0f \n",nbytes,Dbytes); goto errret; } SortFileList((char *) Drec,sizeof(dfrec),Dnf,'A'); // sort Drec[Dnf] by Drec[].file for (ii = dups = 0; ii < Dnf-1; ii++) // look for duplicate files if (strEqu(Drec[ii].file,Drec[ii+1].file)) { wprintf(mLog," *** duplicate file: %s \n",Drec[ii].file); dups++; } if (dups) goto errret; return 0; errret: BJvalid = 0; dFilesReset(); return 0; } // get existing files at backup location, save in Brec[] array // return -1 if error, else count of backup files // // Linux sort command: // '.' sorts before ' ' (0x2E < 0x20, which is crazy) // Workaround implemented. int bGetFiles() { int gcc, fcc, err, vers, vfound, jj; int bb, bbp, rtype, noret = 0; int lover, hiver, retND, retNV; char command[300], *pp, *psep1; char bfile[maxfcc], *bfile2; double fage; const char *psep2; FILE *fid; struct stat64 filestat; bFilesReset(); // reset backup file list if (! mount(0)) return 0; // validate and mount target v.3.2 wprintx(mLog,0,"\n""find all files at backup location \n",boldfont); sprintf(command,"find %s -type f -or -type l >%s",BJdirk,TFbakfiles); // backup filespecs to temp file v.3.0 err = do_shell("find",command); if (err) return -1; // read filespecs into memory and use memory sort instead of linux sort utility // (apparently cannot do a straight ascii sort, even with LC_ALL=C) gcc = strlen(BD_UKOPPDIRK); // directory for ukopp special files fid = fopen(TFbakfiles,"r"); // read file list if (! fid) zappcrash("cannot open scratch file %s",TFbakfiles); for (bb = 0; bb < maxfs; ) // loop all files at backup location { pp = fgets_trim(bfile,maxfcc-1,fid); // next file if (! pp) break; // eof bfile2 = bfile + BJdcc; // remove backup mount point if (strnEqu(bfile2,BD_UKOPPDIRK,gcc)) continue; fcc = strlen(bfile2); if (fcc > maxfcc-BJdcc-10) { // cannot handle files near limit wprintf(mLog," *** filespec too big, omit: %s...",bfile2); wprintf(mLog,"\n"); continue; } err = lstat64(bfile,&filestat); // check accessibility if (err) { wprintf(mLog," *** %s, omit: %s",strerror(errno),bfile2); wprintf(mLog,"\n"); continue; } else if (! S_ISREG(filestat.st_mode) && // reg. files and symlinks only v.3.0 ! S_ISLNK(filestat.st_mode)) continue; // build memory record for file data Brec[bb].file = strdup(bfile2); // filespec Brec[bb].err = 0; Brec[bb].size = filestat.st_size; // file size Brec[bb].mtime = filestat.st_mtime // last mod time + filestat.st_mtim.tv_nsec * nano; Brec[bb].lover = Brec[bb].hiver = Brec[bb].nexpv = 0; // no versions yet Brec[bb].finc = 0; // no backup yet bb++; } fclose (fid); Bnf = bb; wprintf(mLog," %6d backup files \n",Bnf); if (Bnf == maxfs) { wprintf(mLog," *** max files exceeded \n"); bFilesReset(); return -1; } SortFileList((char *) Brec,sizeof(bfrec),Bnf,'A'); // sort Brec[Bnf] by Brec[].file for (bb = 0, bbp = -1; bb < Bnf; bb++) // loop all files { bfile2 = Brec[bb].file; fcc = strlen(bfile2); vers = 0; psep1 = strstr(bfile2+fcc-10,VSEP1); // look for file version if (psep1) { err = convSI(psep1+2,vers,1,9999,&psep2); // if format not valid, if (err > 1) vers = 0; // assume a current file (vers 0) if (strNeq(psep2,VSEP2)) vers = 0; if (*(psep2+1)) vers = 0; // VSEP2 must be at end v.3.2 if (vers) *psep1 = 0; // remove version from file name } if (! vers) // a current file, not prior version { bbp++; // add new file record Brec[bbp] = Brec[bb]; // copy all data } if (vers) // a prior version, 1-9999 { if (bbp > -1 && strEqu(Brec[bbp].file,bfile2)) { // look back for match with prior file if (Brec[bbp].lover == 0) Brec[bbp].lover = vers; // set first version found if (vers < Brec[bbp].lover) Brec[bbp].lover = vers; // (10) sorts before (9) if (vers > Brec[bbp].hiver) Brec[bbp].hiver = vers; // track lowest and highest vers. found free(bfile2); // free duplicate filespec } else { // version present, but no curr. file bbp++; // add new file record Brec[bbp] = Brec[bb]; // copy all data Brec[bbp].err = -1; // mark file (vers 0) not present Brec[bbp].size = Brec[bbp].mtime = 0; Brec[bbp].lover = Brec[bbp].hiver = vers; // track prior versions present } } } Bnf = bbp + 1; for (bb = 0; bb < Bnf; bb++) // loop all files at backup location { strcpy(bfile,BJdirk); strcat(bfile,Brec[bb].file); bfile2 = bfile + BJdcc; if (BJnnx > 0) { for (jj = 0; jj < BJnnx; jj++) { // find matching backup include rec. rtype = BJrtype[jj]; if (rtype != 2) continue; if (MatchWild(BJfspec[jj],bfile2) == 0) break; } if (jj == BJnnx) { // this file not in backup set Brec[bb].retND = Brec[bb].retNV = 0; // no retention specs noret++; } else { Brec[bb].retND = BJretND[jj]; // get corresp. retention specs v.3.5 Brec[bb].retNV = BJretNV[jj]; } } if (Brec[bb].err == 0) { Cfiles++; // count curr. version files Cbytes += Brec[bb].size; // and total bytes } if (Brec[bb].lover == 0) continue; // no versions present lover = Brec[bb].lover; // version range found hiver = Brec[bb].hiver; retND = Brec[bb].retND; // retention days retNV = Brec[bb].retNV; // retention versions if (! retND) retND = -1; // zero days retention, defeat test vfound = 0; // versions found for (vers = hiver; vers >= lover; vers--) // loop file version, high to low { // v.4.0 setFileVersion(bfile,vers); err = lstat64(bfile,&filestat); // check file exists on backup if (err) { wprintf(mLog," *** version %d missing: %s \n",vers,bfile2); continue; } vfound++; // this file, versions found Vfiles++; // total versioned files and bytes Vbytes += filestat.st_size; fage = (time(0)-filestat.st_mtime)/24.0/3600.0; // file version age in days if (fage <= retND || vfound <= retNV) continue; // this version is to be retained Brec[bb].nexpv++; // count expired versions v.4.0 Pfiles++; // total expired files and bytes Pbytes += filestat.st_size; // (to be purged) } } wprintf(mLog," %6d files not in backup set (unknown retention) \n",noret); wprintf(mLog," %6d (%s) curr. file versions \n",Cfiles,formatKBMB(Cbytes,3)); wprintf(mLog," %6d (%s) prior file versions \n",Vfiles,formatKBMB(Vbytes,3)); wprintf(mLog," %6d (%s) expired prior versions \n",Pfiles,formatKBMB(Pbytes,3)); sprintf(command,"df -h %s",mountdirk); // v.4.2 do_shell("target space",command); return Bnf; } // get all restore files specified by include/exclude records // save in Rrec[] array int rGetFiles() { int ii, jj, cc, rtype, wstat, ninc, nexc; char *fspec; if (! RJvalid) return 0; rFilesReset(); // clear restore files if (bGetFiles() < 1) return 0; // get backup files wprintf(mLog,"\n generating restore file set \n"); for (ii = 0; ii < RJnnx; ii++) // process include/exclude recs { rtype = RJrtype[ii]; fspec = RJfspec[ii]; if (rtype == 2) // include filespec { wprintf(mLog," include %s \n",fspec); for (ninc = jj = 0; jj < Bnf; jj++) // screen all files in backup loc. { wstat = MatchWild(fspec,Brec[jj].file); if (wstat != 0) continue; if (Brec[jj].err) continue; Rrec[Rnf].file = strdup(Brec[jj].file); // add matching files Rrec[Rnf].finc = 0; Rnf++; ninc++; if (Rnf == maxfs) { wprintf(mLog," *** max files exceeded \n"); rFilesReset(); // v.4.4 return 0; } } wprintf(mLog," %d files added \n",ninc); } if (rtype == 3) // exclude filespec { wprintf(mLog," exclude %s \n",fspec); for (nexc = jj = 0; jj < Rnf; jj++) // check all included files (SO FAR) { if (! Rrec[jj].file) continue; wstat = MatchWild(fspec,Rrec[jj].file); if (wstat != 0) continue; free(Rrec[jj].file); // remove matching files Rrec[jj].file = 0; nexc++; } wprintf(mLog," %d files removed \n",nexc); } } ii = jj = 0; // repack after deletions while (ii < Rnf) { if (Rrec[ii].file == 0) ii++; else { if (ii > jj) { if (Rrec[jj].file) free(Rrec[jj].file); Rrec[jj].file = Rrec[ii].file; Rrec[ii].file = 0; } ii++; jj++; } } Rnf = jj; wprintf(mLog," total file count: %d \n",Rnf); cc = strlen(RJfrom); // copy from: /dirk/.../ for (ii = 0; ii < Rnf; ii++) // get selected backup files to restore { if (! strnEqu(Rrec[ii].file,RJfrom,cc)) { wprintf(mLog," *** not within copy-from; %s \n",Rrec[ii].file); RJvalid = 0; // mark restore job invalid continue; } } SortFileList((char *) Rrec,sizeof(rfrec),Rnf,'A'); // sort Rrec[Rnf] by Rrec[].file return 0; } // helper function for backups and reports // // compare disk and backup files, set disp in Drec[] and Brec[] arrays: // n new on disk, not on backup // d deleted on backup, not on disk // m modified on both, but not equal // u unchanged on both, and equal // v versions on backup, only prev. versions are present int setFileDisps() { int dii, bii, comp; char disp; double diff; dii = bii = 0; nnew = nmod = nunc = ndel = comp = 0; Mbytes = 0.0; // total bytes, new and modified files while ((dii < Dnf) || (bii < Bnf)) // scan disk and backup files parallel { if ((dii < Dnf) && (bii == Bnf)) comp = -1; else if ((dii == Dnf) && (bii < Bnf)) comp = +1; else comp = strcmp(Drec[dii].file, Brec[bii].file); if (comp < 0) { // unmatched disk file Drec[dii].disp = 'n'; // new nnew++; // count new files Mbytes += Drec[dii].size; // accumulate Mbytes Drec[dii].bindx = -1; // no matching backup file dii++; } else if (comp > 0) { // unmatched backup file if (Brec[bii].err == 0) { // if current version is present, Brec[bii].disp = 'd'; // file was deleted from disk ndel++; // count deleted files } else Brec[bii].disp = 'v'; // only old versions on backup bii++; } else if (comp == 0) { // file present on disk and backup Drec[dii].bindx = bii; // link disk to backup record v.4.0 if (Brec[bii].err == 0) { diff = Drec[dii].mtime - Brec[bii].mtime; // check if equal mod times if (fabs(diff) > MODTIMETOLR) disp = 'm'; else disp = 'u'; // yes, assume unchanged Drec[dii].disp = Brec[bii].disp = disp; if (disp == 'u') nunc++; // count unchanged files if (disp == 'm') nmod++; // count modified files if (disp == 'm') Mbytes += Drec[dii].size; // and accumulate Mbytes } else { Brec[bii].disp = 'v'; // only old versions on backup Drec[dii].disp = 'n'; // disk file is logically new nnew++; // count new files Mbytes += Drec[dii].size; // accumulate Mbytes } dii++; bii++; } } Mfiles = nnew + nmod + ndel; return 0; } // Sort file list in memory (disk files, backup files, restore files). // Sort ascii sequence, or sort subdirectories in a directory before files. int SortFileList(char *recs, int RL, int NR, char sort) { HeapSortUcomp fcompA, fcompD; // filespec compare funcs if (sort == 'A') HeapSort(recs,RL,NR,fcompA); // ascii compare if (sort == 'D') HeapSort(recs,RL,NR,fcompD); // special compare (directories first) return 0; } int fcompA(cchar *rec1, cchar *rec2) // ascii comparison { // current file (no version) sorts first dfrec *r1 = (dfrec *) rec1; dfrec *r2 = (dfrec *) rec2; return strcmp(r1->file,r2->file); } int fcompD(cchar *rec1, cchar *rec2) // special compare filenames { // subdirectories in a directory compare dfrec *r1 = (dfrec *) rec1; // less than files in the directory dfrec *r2 = (dfrec *) rec2; return filecomp(r1->file,r2->file); } int filecomp(cchar *file1, cchar *file2) // special compare filenames { // subdirectories compare before files cchar *pp1, *pp10, *pp2, *pp20; cchar slash = '/'; int cc1, cc2, comp; pp1 = file1; // first directory level or file pp2 = file2; while (true) { pp10 = strchr(pp1,slash); // find next slash pp20 = strchr(pp2,slash); if (pp10 && pp20) { // both are directories cc1 = pp10 - pp1; cc2 = pp20 - pp2; if (cc1 < cc2) comp = strncmp(pp1,pp2,cc1); // compare the directories else comp = strncmp(pp1,pp2,cc2); if (comp) return comp; else if (cc1 != cc2) return (cc1 - cc2); pp1 = pp10 + 1; // equal, check next level pp2 = pp20 + 1; continue; } if (pp10 && ! pp20) return -1; // only one is a directory, if (pp20 && ! pp10) return 1; // the directory is first comp = strcmp(pp1,pp2); // both are files, compare return comp; } } // reset all backup job data and free allocated memory int BJreset() { for (int ii = 0; ii < BJnnx; ii++) if (BJfspec[ii]) free(BJfspec[ii]); BJnnx = BJvalid = 0; BJvmode = 0; dFilesReset(); // reset dependent disk file data return 0; } // reset all restore job data and free allocated memory int RJreset() { for (int ii = 0; ii < RJnnx; ii++) if (RJfspec[ii]) free(RJfspec[ii]); RJvalid = RJnnx = 0; rFilesReset(); // reset dependent disk file data return 0; } // reset all file data and free allocated memory int dFilesReset() { // disk files data for (int ii = 0; ii < Dnf; ii++) { free(Drec[ii].file); Drec[ii].file = 0; } Dnf = 0; Dbytes = Mbytes = 0.0; return 0; } int bFilesReset() { // backup files data for (int ii = 0; ii < Bnf; ii++) { free(Brec[ii].file); Brec[ii].file = 0; } Bbytes = Bnf = 0; Cbytes = Cfiles = 0; Mbytes = Mfiles = 0; Vbytes = Vfiles = 0; Pbytes = Pfiles = 0; return 0; } int rFilesReset() { // restore files data for (int ii = 0; ii < Rnf; ii++) { free(Rrec[ii].file); Rrec[ii].file = 0; } Rnf = 0; return 0; } // Helper function to copy a file between disk and backup location. // Owner and permissions are transferred for copied files and directories, // but this will do nothing in case target is VFAT (Microsoft) file system. cchar * copyFile(cchar *sfile, cchar *dfile, int mpf) { char file1[maxfcc], file2[maxfcc]; int fid1, fid2, err, rcc, dlevs; char *pp1, *pp2, buff[BIOCC]; const char *errmess = 0; struct stat64 fstat1, fstat2; struct timeval ftimes[2]; *file1 = *file2 = 0; if (mpf == 1) strcpy(file1,BJdirk); // prepend mount point if req. strcat(file1,sfile); if (mpf == 2) strcpy(file2,BJdirk); strcat(file2,dfile); pp2 = file2; dlevs = 0; while (true) { // v.25 pp2 = strchr(pp2+1,'/'); // create missing directory levels if (! pp2) break; // (check and create from top down) *pp2 = 0; err = stat64(file2,&fstat2); if (err) { err = mkdir(file2,0731); if (err) return strerror(errno); dlevs++; } *pp2 = '/'; } while (dlevs) { // v.25 pp1 = (char *) strrchr(file1,'/'); // for created output directories, if (! pp1) break; // copy owner and permissions from pp2 = (char *) strrchr(file2,'/'); // corresponding input directory if (! pp2) break; // (measured from bottom up) *pp1 = *pp2 = 0; // (possibly top levels not set) err = stat64(file1,&fstat1); if (err) return strerror(errno); chmod(file2,fstat1.st_mode); err = chown(file2,fstat1.st_uid,fstat1.st_gid); if (err) wprintf(mLog,"error: %s \n",wstrerror(err)); dlevs--; } *file1 = *file2 = 0; if (mpf == 1) strcpy(file1,BJdirk); // refresh filespecs strcat(file1,sfile); if (mpf == 2) strcpy(file2,BJdirk); strcat(file2,dfile); err = lstat64(file1,&fstat1); // get input file attributes v.3.0 if (err) return strerror(errno); if (S_ISLNK(fstat1.st_mode)) { // input file is symlink rcc = readlink(file1,buff,maxfcc); if (rcc < 0 || rcc > maxfcc-2) return strerror(errno); buff[rcc] = 0; err = symlink(buff,file2); // create output symlink if (err) return strerror(errno); ftimes[0].tv_sec = fstat1.st_atime; // get input file access time v.3.0 ftimes[0].tv_usec = fstat1.st_atim.tv_nsec / 1000; // in microsecs. ftimes[1].tv_sec = fstat1.st_mtime; ftimes[1].tv_usec = fstat1.st_mtim.tv_nsec / 1000; lutimes(file2,ftimes); // set output file access time return 0; } fid1 = open(file1,O_RDONLY+O_NOATIME+O_LARGEFILE); // open input file if (fid1 == -1) return strerror(errno); fid2 = open(file2,O_WRONLY+O_CREAT+O_TRUNC+O_LARGEFILE,0700); // open output file if (fid2 == -1) { errmess = strerror(errno); close(fid1); return errmess; } while (true) { rcc = read(fid1,buff,BIOCC); // read huge blocks if (rcc == 0) break; if (rcc == -1) { errmess = strerror(errno); close(fid1); close(fid2); return errmess; } rcc = write(fid2,buff,rcc); // write blocks if (rcc == -1) { errmess = strerror(errno); close(fid1); close(fid2); return errmess; } if (checkKillPause()) break; } close(fid1); // close input file err = fsync(fid2); // flush output file v.4.4 if (err) return strerror(errno); err = close(fid2); // close output file if (err) return strerror(errno); err = lstat64(file1,&fstat1); // get input file attributes if (err) return strerror(errno); chmod(file2,fstat1.st_mode); // copy owner and permissions err = chown(file2,fstat1.st_uid,fstat1.st_gid); // from input to output file if (err) wprintf(mLog,"error: %s \n",wstrerror(err)); ftimes[0].tv_sec = fstat1.st_atime; // get input file access time ftimes[0].tv_usec = fstat1.st_atim.tv_nsec / 1000; // in microsecs. ftimes[1].tv_sec = fstat1.st_mtime; ftimes[1].tv_usec = fstat1.st_mtim.tv_nsec / 1000; utimes(file2,ftimes); // set output file access time return 0; } // Verify helper function // Verify that file on backup medium is readable, return its length. // Optionally compare backup file to disk file, byte for byte. // returns error message or null if OK. cchar * checkFile(cchar *dfile, int compf, double &tcc) { int vfid = 0, dfid = 0; int err, vcc, dcc, cmperr = 0; char vfile[maxfcc], *vbuff = 0, *dbuff = 0; const char *errmess = 0; double dtime, vtime; int open_flagsV = O_RDONLY+O_NOATIME+O_LARGEFILE+O_DIRECT; // bypass cache v.3.5.2 int open_flagsD = O_RDONLY+O_NOATIME+O_LARGEFILE; // use cache normally v.3.5.3 struct stat64 filestat; tcc = 0.0; strcpy(vfile,BJdirk); // prepend mount point strcat(vfile,dfile); lstat64(vfile,&filestat); // if symlink, check readable v.3.1 if (S_ISLNK(filestat.st_mode)) { vbuff = (char *) malloc(maxfcc); vcc = readlink(vfile,vbuff,maxfcc); if (vcc == -1) errmess = strerror(errno); goto cleanup; } if (compf) goto comparefiles; vfid = open(vfile,open_flagsV); // open for read, large blocks, direct I/O if (vfid == -1) goto checkerr; err = posix_memalign((void**) &vbuff,512,BIOCC); // use aligned buffer if (err) zappcrash("memory allocation failure"); while (1) { vcc = read(vfid,vbuff,BIOCC); if (vcc == 0) break; if (vcc == -1) { errmess = strerror(errno); break; } tcc += vcc; // accumulate length if (checkKillPause()) break; } goto cleanup; comparefiles: vfid = open(vfile,open_flagsV); // open for read, large blocks, direct I/O if (vfid == -1) goto checkerr; dfid = open(dfile,open_flagsD); // disk files, use cached I/O if (dfid == -1) goto checkerr; err = posix_memalign((void**) &vbuff,512,BIOCC); // use aligned buffers if (err) zappcrash("memory allocation failure"); err = posix_memalign((void**) &dbuff,512,BIOCC); if (err) zappcrash("memory allocation failure"); while (1) { vcc = read(vfid,vbuff,BIOCC); // read two files if (vcc == -1) { errmess = strerror(errno); goto cleanup; } dcc = read(dfid,dbuff,BIOCC); if (dcc == -1) { errmess = strerror(errno); goto cleanup; } if (vcc != dcc) cmperr++; // compare buffers if (memcmp(vbuff,dbuff,vcc)) cmperr++; tcc += vcc; // accumulate length if (vcc == 0) break; if (dcc == 0) break; if (checkKillPause()) break; } if (vcc != dcc) cmperr++; if (cmperr) { // compare error lstat64(dfile,&filestat); // v.3.0 dtime = filestat.st_mtime + filestat.st_mtim.tv_nsec * nano; // file modified since snapshot? lstat64(vfile,&filestat); // v.3.0 vtime = filestat.st_mtime + filestat.st_mtim.tv_nsec * nano; if (fabs(dtime-vtime) < MODTIMETOLR) errmess = "compare error"; // no, a real compare error } cleanup: if (vfid) close(vfid); if (dfid) close(dfid); if (vbuff) free(vbuff); if (dbuff) free(dbuff); return errmess; checkerr: // v.3.7 errmess = strerror(errno); if (errno == EINVAL || errno == ENOTSUP) { if (Fgui) zmessageACK(mWin,0,"large block direct I/O not allowed \n %s",errmess); else wprintf(mLog,"large block direct I/O not allowed \n %s",errmess); } if (errno == EPERM) { if (Fgui) zmessageACK(mWin,0,"permission denied \n %s",errmess); else wprintf(mLog,"permission denied \n %s",errmess); } goto cleanup; } // modify filespec to have a specified version // 0 = no version = current version, +N = previous version // returns cc of resulting filespec // warning: filespec must have space for version numbers int setFileVersion(char *filespec, int vers) { int fcc, overs, err; char *psep1; const char *psep2; fcc = strlen(filespec); psep1 = strstr(filespec+fcc-10,VSEP1); // look for file version v.3.2 if (psep1) { overs = 0; err = convSI(psep1+2,overs,&psep2); // if format not valid, take if (err < 2) overs = 1; // as non-versioned file if (strNeq(psep2,VSEP2)) overs = 0; if (*(psep2+1)) overs = 0; // VSEP2 must be at end if (overs) *psep1 = 0; fcc = psep1 - filespec; } if (vers == 0) return fcc; if (! psep1) psep1 = filespec + fcc; strcpy(psep1,VSEP1); sprintf(psep1+2,"%d",vers); strcat(psep1+2,VSEP2); return fcc + strlen(psep1); } // rename a backup file to assign the next version number // update the passed backup file data record, bakrec // returns error message or null if all OK const char * setnextVersion(bfrec &bakrec) { char fspec1[maxfcc], fspec2[maxfcc]; int vers, err; strcpy(fspec1,BJdirk); strcat(fspec1,bakrec.file); strcpy(fspec2,fspec1); vers = bakrec.hiver + 1; setFileVersion(fspec2,vers); err = rename(fspec1,fspec2); if (err) return strerror(errno); bakrec.hiver = vers; if (! bakrec.lover) bakrec.lover = vers; // v.4.0 return null; } // purge expired file versions in backup location // bakrec: backup file data record to use and update // returns error message or null if all OK // fkeep: if true, keep last version unless disk file was deleted // v.4.0 const char * purgeVersions(bfrec &bakrec, int fkeep) { int lover, hiver, loretver, vers; int err, vfound, vpurged; int retND, retNV; double fage; char fspec[maxfcc]; const char *mess = null; struct stat64 filestat; strcpy(fspec,BJdirk); // prepend backup location strcat(fspec,bakrec.file); retND = bakrec.retND; retNV = bakrec.retNV; lover = bakrec.lover; hiver = bakrec.hiver; if (! hiver) return 0; // no versions present if (! retND) retND = -1; // zero days retention, defeat test if (bakrec.disp == 'd') fkeep = 0; // file no longer in backup job if (bakrec.disp == 'v') fkeep = 0; // or disk file deleted loretver = lover; // lowest retained version vfound = 0; // actual versions found vpurged = 0; // versions purged for (vers = hiver; vers >= lover; vers--) // loop file versions, high to low { // v.4.0 setFileVersion(fspec,vers); err = lstat64(fspec,&filestat); // check file exists on backup if (err) continue; vfound++; // count versions found fage = (time(0)-filestat.st_mtime)/24.0/3600.0; // file age in days if (fage <= retND || vfound <= retNV) { // retain this version loretver = vers; // remember lowest retained version continue; } if ((vers == hiver) && fkeep) { // retain last version v.4.0 loretver = vers; continue; } mess = deleteFile(fspec); // purge this version if (mess) break; vpurged++; } bakrec.nexpv = fkeep; // set 0 or 1 expired versions v.4.0 bakrec.lover = loretver; // set new low version v.4.0 if (vpurged == vfound) bakrec.lover = bakrec.hiver = 0; // no versions remaining v.4.1 return mess; } // helper function to delete a file from backup location. // delete parent directories if they are now empty. cchar * deleteFile(cchar *file) { int err; char dfile[maxfcc], *pp; strcpy(dfile,file); err = remove(dfile); if (err) return strerror(errno); while ((pp = (char *) strrchr(dfile,'/'))) // delete empty directory { *pp = 0; err = rmdir(dfile); if (! err) continue; // and parents ... if (errno == ENOTEMPTY) return 0; if (errno == EBUSY) return 0; // see https://lkml.org/lkml/2013/8/28/654 v.4.8 return strerror(errno); } return 0; } // do shell command (subprocess) and echo outputs to log window // returns command status: 0 = OK, +N = error int do_shell(cchar *pname, cchar *command) { char buff[500], *crec; int err, contx = 0; snprintf(buff,499,"\n""shell: %s \n",command); wprintx(mLog,0,buff,boldfont); while ((crec = command_output(contx,command))) // bug fix: remove colon v.3.2 { wprintf(mLog," %s: %s \n",pname,crec); zsleep(0.1); // throttle output a little } err = command_status(contx); if (err == 32) err = 0; // ignore Linux "broken pipe" crap if (err) wprintf(mLog," %s status: %s \n", pname, strerror(err)); else wprintf(mLog," OK \n"); return err; } // supply unused zdialog callback function void KBstate(GdkEventKey *event, int state) { return; }