 ///
/// @file iofile.c
/// @authors k. headley
/// @date 01 mar 2020

/// Cross-platform Carbon Waveglider file utilities

/// @sa doxygen-examples.c for more examples of Doxygen markup


/////////////////////////
// Terms of use
/////////////////////////
/*
 Copyright Information

 Copyright 2002-2020 MBARI
 Monterey Bay Aquarium Research Institute, all rights reserved.

 Terms of Use

 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. You can access the GPLv3 license at
 http://www.gnu.org/licenses/gpl-3.0.html

 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
 (http://www.gnu.org/licenses/gpl-3.0.html)

 MBARI provides the documentation and software code "as is", with no warranty,
 express or implied, as to the software, title, non-infringement of third party
 rights, merchantability, or fitness for any particular purpose, the accuracy of
 the code, or the performance or results which you may obtain from its use. You
 assume the entire risk associated with use of the code, and you agree to be
 responsible for the entire cost of repair or servicing of the program with
 which you are using the code.

 In no event shall MBARI be liable for any damages, whether general, special,
 incidental or consequential damages, arising out of your use of the software,
 including, but not limited to, the loss or corruption of your data or damages
 of any kind resulting from use of the software, any prohibited use, or your
 inability to use the software. You agree to defend, indemnify and hold harmless
 MBARI and its officers, directors, and employees against any claim, loss,
 liability or expense, including attorneys' fees, resulting from loss of or
 damage to property or the injury to or death of any person arising out of the
 use of the software.

 The MBARI software is provided without obligation on the part of the
 Monterey Bay Aquarium Research Institute to assist in its use, correction,
 modification, or enhancement.

 MBARI assumes no responsibility or liability for any third party and/or
 commercial software required for the database or applications. Licensee agrees
 to obtain and maintain valid licenses for any additional third party software
 required.
 */

/////////////////////////
// Headers
/////////////////////////
#include <string.h>
#include <stdio.h>
#include <ctype.h>
#if defined(__PIC24FJ256GA106__) || defined(__PIC24FJ256GA606__) || defined(__PIC24FJ1024GA606__)
#include "pic24.h"
#include "timers.h"
#include "ioserial.h"
#include "iodisk.h"
#include "ff.h"
#include "log.h"
#include "ui.h"
#elif defined(__APPLE__) || defined(__unix__) || defined(__CYGWIN__)
#include <errno.h>
#include <sys/statvfs.h>
#endif
#include "fileutils.h"
#include "iowfile.h"

/////////////////////////
// Macros
/////////////////////////

// These macros should only be defined for
// application main files rather than general C files
/*
/// @def PRODUCT
/// @brief header software product name
#define PRODUCT "TBD_PRODUCT"

/// @def COPYRIGHT
/// @brief header software copyright info
#define COPYRIGHT "Copyright 2002-YYYY MBARI Monterey Bay Aquarium Research Institute, all rights reserved."
/// @def NOWARRANTY
/// @brief header software terms of use
#define NOWARRANTY  \
"This program is distributed in the hope that it will be useful,\n"\
"but WITHOUT ANY WARRANTY; without even the implied warranty of\n"\
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n"\
"GNU General Public License for more details (http://www.gnu.org/licenses/gpl-3.0.html)\n"
*/

/////////////////////////
// Declarations
/////////////////////////

/////////////////////////
// Imports
/////////////////////////

/////////////////////////
// Module Global Variables
/////////////////////////

// FatFS uses the same struct
// (FILINFO) for file and directory nodes
// POSIX uses DIR for directory nodes, and struct stat for file info.
// We use DIRENT_T for reading directory entries
DIRENT_T g_file_info_s, *g_file_info=&g_file_info_s;

/// Working buffer (down from 4096)
static byte g_file_buffer[G_FILEBUF_LEN];

/// local static vars for scan_files function
static uint32_t AccSize;
static uint16_t AccFiles, AccDirs;

#if defined(__PIC24FJ256GA106__) || defined(__PIC24FJ256GA606__) || defined(__PIC24FJ1024GA606__)
// These functions are not yet cross-platform

#if _USE_LFN
TCHAR g_lfname_buf[256];
#endif

/// FAT filesystem type
static const byte g_fatfs_type[] = {0, 12, 16, 32};


/// Print the result of a file op
/// @param rc an FRESULT enum
static void s_fr_perror(int rc) {
    const char *str[] ={
        "OK","DISK_ERR","INT_ERR","NOT_READY","NO_FILE",
        "NO_PATH","INVALID_NAME","DENIED","EXIST",
        "INVALID_OBJECT","WRITE_PROTECTED","INVALID_DRIVE",
        "NOT_ENABLED", "NO_FILE_SYSTEM", "MKFS_ABORTED",
        "TIMEOUT", "LOCKED", "NOT_ENOUGH_CORE", "TOO_MANY_OPEN_FILES"
    };

    OUT_CTTY("err[%d / %s]\r\n", rc, (rc>=0 && rc<=IOW_TOO_MANY_OPEN_FILES ? str[rc] : "UNKNOWN"));
}
#endif

/////////////////////////
// Function Definitions
/////////////////////////

#if defined(__PIC24FJ256GA106__) || defined(__PIC24FJ256GA606__) || defined(__PIC24FJ1024GA606__)

/// Look at all the files and update Acc.* vars
/// @param path Pointer to the path name working buffer
/// @note is recursive

static int s_scan_files(char* path, DIRENT_T *dinfo, uint32_t *r_fsize, uint16_t *r_files, uint16_t *r_dirs) {
    DIR_T dirs,*dp=&dirs;
    int retval=0;
    int i=0;
    char *fn=NULL;

     if(NULL!=path && strlen(path)>0 && NULL!=r_fsize && NULL!=r_files && NULL!=r_dirs){
        if ( iow_opendir(&dp, path) == 0) {
            i = strlen(path);
            while ((iow_readdir(dp, &dinfo) == 0) && dinfo->fname[0]!='\0') {
                if (_FS_RPATH && dinfo->fname[0] == '.') continue;
    #if _USE_LFN
                fn = *dinfo->lfname ? dinfo->lfname : dinfo->fname;
    #else
                fn = dinfo->fname;
    #endif

                if (dinfo->fattrib & AM_DIR) {
                    (*r_dirs)++;
                    path[i] = '/';
                    strcpy(&path[i + 1], fn);
                    retval = s_scan_files(path,dinfo, r_fsize,r_files,r_dirs);
                    path[i] = 0;
                    if (retval != 0) break;
                } else {
                    (*r_files)++;
                    (*r_fsize) += dinfo->fsize;
                }
            }
        }
    }
    return retval;
}

static return_code_t s_dstat_fn(char* path) {
    return_code_t retval=ERR_INVALID;
    uint32_t avail;
    int test; //return value from file ops
    FSYS_T *fs; // Pointer to file system object

    if(NULL!=path){
    }
    // skip leading whitespace
    while (*path == ' ' || *path=='\t' || *path=='\r' || *path=='\n') path++;

//    if ( (fr=f_getfree(path, (UINT32*) &avail, &fs))==FR_OK){
    if ( (test=iow_fsstat(path, &avail, &fs))==0){
        OUT_CTTY( " FAT type                    FAT%u\r\n", g_fatfs_type[fs->fs_type & 3]);
        OUT_CTTY( " Bytes/Cluster            %8lu\r\n",(fs->csize * 512UL));
        OUT_CTTY( " Number of FATs           %8u\r\n",fs->n_fats);
        OUT_CTTY( " Root DIR entries         %8u\r\n",fs->n_rootdir);
        OUT_CTTY( " Sectors/FAT              %8lu\r\n",fs->fsize);
        OUT_CTTY( " Number of clusters       %8lu\r\n",(fs->n_fatent - 2));
        OUT_CTTY( " Volume start (lba)       %8lu\r\n",fs->volbase);
        OUT_CTTY( " FAT start (lba)          %8lu\r\n",fs->fatbase);
        OUT_CTTY( " DIR start (lba,clustor)  %8lu\r\n",fs->dirbase);
        OUT_CTTY( " Data start (lba)         %8lu\r\n\r\n", fs->database);

        AccSize = AccFiles = AccDirs = 0;

        if ((test=s_scan_files(path,g_file_info,&AccSize,&AccFiles,&AccDirs))!=0) {
           s_fr_perror(test);
            retval=ERR_FAILURE;
        }else{
            OUT_CTTY( " %10"FMT_U32" bytes in %u files %u directories\r\n",
                    AccSize, AccFiles, AccDirs);
            OUT_CTTY(  " %10lu KiB total %10lu KiB available\r\n",
                    ((fs->n_fatent - 2) * (fs->csize / 2)), (avail * (fs->csize / 2)));
            retval=ERR_SUCCESS;
        }
    }else{
        s_fr_perror(test);
        retval=ERR_FAILURE;
    }

    return retval;
}
#elif defined(__APPLE__) || defined(__unix__) || defined(__CYGWIN__)

static int s_scan_files(char* path, DIRENT_T *dinfo, uint32_t *r_fsize, uint16_t *r_files, uint16_t *r_dirs) {
    DIR_T dirs,*dp=&dirs;
    int retval=0;
    int i=0;
    char *fn=NULL;

     if(NULL!=path && strlen(path)>0 && NULL!=r_fsize && NULL!=r_files && NULL!=r_dirs){
        if ( iow_opendir(&dp, path) == 0) {
            i = strlen(path);
            while ((iow_readdir(dp, &dinfo) == 0) && dinfo->d_name[0]!='\0') {
                if (dinfo->d_name[0] == '.') continue;
                fn = dinfo->d_name;
//                OUT_CTTY("%s...\r\n",dinfo->d_name);
                if(dinfo->d_type==DT_DIR){
                    (*r_dirs)++;
                    path[i] = '/';
                    strcpy(&path[i + 1], fn);
                    retval = s_scan_files(path,dinfo, r_fsize,r_files,r_dirs);
                    path[i] = 0;
                    if (retval != 0) break;
                }else if(dinfo->d_type==DT_REG){
                    struct stat finfo;
                    // if file, stat and get attributes...
                    char fpath[512]={0};
                    sprintf(fpath,"%s/%s",path,dinfo->d_name);
                    if(stat(fpath,&finfo)==0){
//                        OUT_CTTY("%s : %"FMT_OFF"\r\n",fpath,finfo.st_size);
                        *r_files += 1;
                        *r_fsize += finfo.st_size;
                    }else{
                        EPRINT("stat %s err[%d/%s]",fpath,errno,strerror(errno));
                    }
                }
            }
        }//else{TRACE();}
    }else{TRACE();}
    return retval;
}

static return_code_t s_dstat_fn(char* path) {
    return_code_t retval=ERR_FAILURE;
    FSYS_T fs_s, *fs=&fs_s; // Pointer to file system object
    int test=0;

    if(NULL != path){

        // skip leading whitespace
        while (*path == ' ' || *path=='\t' || *path=='\r' || *path=='\n') path++;

         if(iow_fsstat(path,NULL,&fs)==0){

            unsigned long total = (fs->f_blocks * fs->f_frsize) / 1024;
            unsigned long available = (fs->f_bavail * fs->f_frsize) / 1024;
            unsigned long free = (fs->f_bfree * fs->f_frsize) / 1024;
            unsigned long used = total - free;

            AccSize = AccFiles = AccDirs = 0;

            if ((test=s_scan_files(path,g_file_info,&AccSize,&AccFiles,&AccDirs))==0) {
                OUT_CTTY( " FSYS Type     :     x%lX\r\n",fs->f_fsid);
                OUT_CTTY( " Block Size    : %12lu\r\n",fs->f_bsize);
                OUT_CTTY( " Fragment Size : %12lu\r\n",fs->f_frsize);
                OUT_CTTY( " inodes        : %12"FMT_FSFILCNT"\r\n\r\n",fs->f_files);

                OUT_CTTY( " %10"FMT_U32" bytes in %u files %u directories\r\n",
                            AccSize, AccFiles, AccDirs);
                OUT_CTTY(" %10lu KiB total %10lu KiB available\r\n",total,available);
                OUT_CTTY(" %10lu KiB used  %10lu KiB free\r\n",used,free);
            }else {
                iow_fstrerror(iow_errno);
                retval=ERR_FAILURE;
            }
        }else{
            EPRINT("statvfs - %s",iow_fstrerror(errno));
        }
    }
    return retval;
}
#else
static return_code_t s_disk_use_fn(char* path)
{
    return_code_t retval=ERR_FAILURE;
    // not implemented
    fprintf(stderr,"not implemented\r\n");
    return retval;
}
#endif

/// COMMAND:<b> stats <sub directory> </b>@n
return_code_t dstat(int argc, char** argv)
{
    return_code_t retval= ERR_SUCCESS;
//    OUT_CTTY("dstat - argc[%d]\r\n",argc);
    if(argc<1){
        char path[256];
        sprintf(path,"/");
        retval=s_dstat_fn(path);
    }else{
        int i=0;
        for(i=0;i<argc;i++){
            // one fail, all fail
            if(NULL!=argv[i] && s_dstat_fn(argv[i])!=ERR_SUCCESS)
                retval=ERR_FAILURE;
        }
    }
    return retval;
}

// Functions below have cross-platform implementations
#if defined(__PIC24FJ256GA106__) || defined(__PIC24FJ256GA606__) || defined(__PIC24FJ1024GA606__)
static return_code_t s_dlist_fn(char* arg, bool full, int nopts, char **opts)
{
    return_code_t retval=ERR_INVALID;

    if(NULL!=arg){

        INT32 totBytes;
        UINT totFiles, totDirs;
        FATFS *fs; // Pointer to file system object
        DIR_T dir_s, *dp=&dir_s;

        // skip leading whitespace
        while (*arg == ' ' || *arg=='\t' || *arg=='\r' || *arg=='\n') arg++;

        if(iow_opendir(&dp,arg)==0){
            totBytes = totFiles = totDirs = 0;
            for (;;) {

                if(iow_readdir(dp,&g_file_info)==0){
                    if(g_file_info_s.fname[0]=='\0')break;
                    if (g_file_info_s.fattrib & AM_DIR) {
                        totDirs++;
                    } else {
                        totFiles++;
                        totBytes += g_file_info_s.fsize;
                    }
#if _USE_LFN
                    char *lfname="";
                    lfname=g_lfname_buf;
#endif

                    bool match=true;
                    if(nopts>0){
                        int j=0;
                        match=false;
                        while(j<nopts){
                            if(strstr(g_file_info_s.fname,opts[j])!=NULL){
                                match=true;
                                break;
                            }
                            j++;
                        }
                    }
                    if(match){
                    if(full){
                    OUT_CTTY( " %c%c%c%c%c %u/%02u/%02u %02u:%02u %9lu  %-12s  %s\r\n",
                            (g_file_info_s.fattrib & AM_DIR) ? 'D' : '-',
                            (g_file_info_s.fattrib & AM_RDO) ? 'R' : '-',
                            (g_file_info_s.fattrib & AM_HID) ? 'H' : '-',
                            (g_file_info_s.fattrib & AM_SYS) ? 'S' : '-',
                            (g_file_info_s.fattrib & AM_ARC) ? 'A' : '-',
                            (g_file_info_s.fdate >> 9) + 1980,
                            (g_file_info_s.fdate >> 5) & 15,
                            g_file_info_s.fdate & 31,
                            (g_file_info_s.ftime >> 11),
                            (g_file_info_s.ftime >> 5) & 63,
                            g_file_info_s.fsize,
#if _USE_LFN
                            lfname
#else
                            g_file_info_s.fname
#endif
                            );
                    }else{
                        OUT_CTTY( "%s\r\n",g_file_info_s.fname);
                    }
                    }
                }else{
                    // readdir failed
                    retval=ERR_FAILURE;
                    break;
                }
            }// end for
            if(full){
            OUT_CTTY( "\r\n");
            OUT_CTTY( " %10.2f KiB in %u files %u directories\r\n", totBytes/1024., totFiles, totDirs);
            if (f_getfree(arg, (DWORD*) & totBytes, &fs) == FR_OK) {
                OUT_CTTY( " %10lu KiB free\r\n", (totBytes * fs->csize * 512)/1024);
            }
            }
        }else{
            EPRINT("%s",iow_fstrerror(iow_errno));
            retval=ERR_FAILURE;
        }

    }// else invalid arg
    return 0;
}

#elif defined(__APPLE__) || defined(__unix__) || defined(__CYGWIN__)
static return_code_t s_dlist_fn(char* path, bool full, int nopts, char **opts)
{
    return_code_t retval=ERR_INVALID;

    if(NULL!=path){

        uint32_t totBytes;
        uint32_t totFiles, totDirs;
        DIR_T *dp=NULL;
        // skip leading whitespace
        while (*path == ' ' || *path=='\t' || *path=='\r' || *path=='\n') path++;

        if(iow_opendir(&dp,path)==0){
            totBytes = totFiles = totDirs = 0;
            for (;;) {

                if(iow_readdir(dp,&g_file_info)==0){
                    if(g_file_info->d_name[0]=='\0')break;
                    // update file/dir counts...
                    if(g_file_info->d_type==DT_DIR){
                        totDirs++;
                    }else{
                        totFiles++;
                    }
                    struct stat finfo;
                    // if file, stat and get attributes...
                    char fpath[512]={0};
                    sprintf(fpath,"%s/%s",path,g_file_info->d_name);
                    if(stat(fpath,&finfo)==0){
                        totBytes += finfo.st_size;

                        // output entry
                        char *time_str=striso8601(NULL,finfo.st_mtime*1000LLU,ISO8601_PREC_MSEC);
                    bool match=true;
                    if(nopts>0){
                        int j=0;
                        match=false;
                        while(j<nopts){
                            if(strstr(g_file_info->d_name,opts[j])!=NULL){
                                match=true;
                                break;
                            }
                            j++;
                        }
                    }
                    if(match){

                        if(full){
                        OUT_CTTY( " %c%c%c%c%c%c %c%c%c%c%c%c%c%c%c%c%c%c%c %s %9"FMT_OFF"  %-12s\r\n",
                           (S_ISREG(finfo.st_mode)  ? 'R' : '-'),
                           (S_ISCHR(finfo.st_mode)  ? 'C' : '-'),
                           (S_ISBLK(finfo.st_mode)  ? 'B' : '-'),
                           (S_ISFIFO(finfo.st_mode) ? 'F' : '-'),
                           (S_ISLNK(finfo.st_mode)  ? 'L' : '-'),
                           (S_ISSOCK(finfo.st_mode) ? 'S' : '-'),
                           (S_ISDIR(finfo.st_mode)  ? 'd' : '-'),
                           ((finfo.st_mode&S_IRUSR) ? 'r' : '-'),
                           ((finfo.st_mode&S_IWUSR) ? 'w' : '-'),
                           ((finfo.st_mode&S_IXUSR) ? 'x' : '-'),
                           ((finfo.st_mode&S_IRGRP) ? 'r' : '-'),
                           ((finfo.st_mode&S_IWGRP) ? 'w' : '-'),
                           ((finfo.st_mode&S_IXGRP) ? 'x' : '-'),
                           ((finfo.st_mode&S_IROTH) ? 'r' : '-'),
                           ((finfo.st_mode&S_IWOTH) ? 'w' : '-'),
                           ((finfo.st_mode&S_IXOTH) ? 'x' : '-'),
                           ((finfo.st_mode&S_ISUID) ? 'S' : '.'),
                           ((finfo.st_mode&S_ISGID) ? 'G' : '.'),
                           ((finfo.st_mode&S_ISVTX) ? 'T' : '.'),
                           time_str,
                           (long long int)finfo.st_size,
                           g_file_info->d_name);
                    }else{
                        OUT_CTTY( "%s\r\n",g_file_info->d_name);
                    }
                    }
                    }else{
                        // stat failed
                        EPRINT("stat %s [%d/%s]",fpath,errno,strerror(errno));
                        break;
                    }
                }else{
                    // readdir failed
                    retval=ERR_FAILURE;
                    break;
                }
            }// end for

            if(full){
            // get free space, output summary...
            struct statvfs vfs;
            if(statvfs(path,&vfs)==0){
                unsigned long total = vfs.f_blocks * vfs.f_frsize / 1024;
                unsigned long available = vfs.f_bavail * vfs.f_frsize / 1024;
                unsigned long free = vfs.f_bfree * vfs.f_frsize / 1024;
                unsigned long used = total - free;

                OUT_CTTY("\r\n %10.2f KiB in %"FMT_U32" files %"FMT_U32" directories:\r\n",totBytes/1024.0,totFiles,totDirs);
                OUT_CTTY(" %10lu KiB total %10lu KiB available\r\n",total,available);
                OUT_CTTY(" %10lu KiB used  %10lu KiB free\r\n",used,free);
            }else{
                EPRINT("statfvs %s",iow_fstrerror(iow_errno));
                retval=ERR_FAILURE;
            }
            }
            iow_closedir(dp);

        }else{
            EPRINT("opendir %s - %s",path,iow_fstrerror(iow_errno));
            retval=ERR_FAILURE;
        }

    }// else invalid arg
    return retval;
}
#endif // s_ls_fn implementations

/// COMMAND: <b>LS|DIR [<dir>...]</b>@n
return_code_t dlist(int argc, char** argv)
{

    return_code_t retval= ERR_SUCCESS;
    bool full=true;
//    if(argc==0){
//        retval=s_dlist_fn("/",full);
//    }else{
    int i=0;
    int count=0;
    char *opt[argc];
    char **opts=opt;
    int nopts=0;
    for(i=0;i<argc;i++){
        // one fail, all fail
        if(NULL!=argv[i]){
            if( strcmp(argv[i],"Q")==0){
                full=false;
            }else if( strcmp(argv[i],"F")==0){
                full=true;
            }else if( argv[i][0]=='*'){
                opt[nopts++]=&argv[i][1];
            }else {
                if(s_dlist_fn(argv[i],full,nopts,opts)!=ERR_SUCCESS){
                retval=ERR_FAILURE;
                }
                count++;
            }
        }
//    }
    }
    if(count==0)
        retval=s_dlist_fn("/",full,nopts,opts);
    return retval;
}

/// COMMAND: <b>mv <old-name> <new-name></b>@n
return_code_t frename(int argc, char** argv)
{
    return_code_t retval=ERR_FAILURE;

    if(argc>=2){
        if(iow_frename(argv[0],argv[1])==0){
            retval= ERR_SUCCESS;
        }
        OUT_CTTY("ren %s\r\n",iow_fstrerror(iow_errno));
    }

    return retval;
}

/// COMMAND: <b>rm <file> [<file>...]</b>@n
return_code_t fdelete(int argc, char** argv)
{
    return_code_t retval=ERR_SUCCESS;

    int i=0;
    for(i=0;i<argc;i++){
        if(iow_unlink(argv[i])==0){
            retval= ERR_SUCCESS;
        }
        OUT_CTTY("rm %s\r\n",iow_fstrerror(iow_errno));
   }
    return retval;
}

/// COMMAND: <b>cp <old-name> <new-name></b>@n
return_code_t fcopy(int argc, char** argv)
{
    return_code_t retval=ERR_FAILURE;
    size_t numRead, numWrote;
    uint32_t totbytes;
#if defined(__PIC24FJ256GA106__) || defined(__PIC24FJ256GA606__) || defined(__PIC24FJ1024GA606__)
    FILE_T file_s[2], *fp[2]={&file_s[0],&file_s[1]};
    DSHOW_TOKS(argc,argv);
#else
    FILE_T *fp[2]={NULL,NULL};
#endif

     if(argc>=2){
        OUT_CTTY( "Opening \"%s\"\r\n", argv[0]);

        if(iow_fopen(&fp[0],argv[0],(IOW_OPEN_EXISTING|IOW_READ))==0){
            OUT_CTTY( "Creating \"%s\"\r\n", argv[1]);
            if(iow_fopen(&fp[1],argv[1],(IOW_CREATE_ALWAYS|IOW_WRITE))==0){
                OUT_CTTY( "Copying...\r\n");
                totbytes = 0;
                for (;;) {
                    numRead=numWrote=0;
                    if((numRead=iow_fread(fp[0], g_file_buffer, G_FILEBUF_LEN))>0){
                        if((numWrote=iow_fwrite(fp[1], g_file_buffer, numRead))==numRead){
                            totbytes += numWrote;
                        }else{
                            // error or disk full
                            EPRINT("write [%s]",iow_fstrerror(iow_errno));
                            break;
                        }
                    }else{
                        // error or EOF
                        EPRINT("read [%s]",iow_fstrerror(iow_errno));
                        break;
                    }

//                    OUT_CTTY("rd[%"FMT_SZ"] wr[%"FMT_SZ"] tot[%"FMT_U32"] status[%d/%d]\r\n",
//                            numRead,numWrote,totbytes,iow_feof(fp[0]), iow_feof(fp[0]));

                    if(iow_feof(fp[0]) || iow_feof(fp[0])){
                        iow_clearerr(fp[0]);
                        // could there be pending bytes?
                        break;
                    }
                }
                OUT_CTTY( "%"FMT_U32" bytes copied\r\n", totbytes);
                iow_fclose(fp[0]);
                iow_fclose(fp[1]);
                retval= ERR_SUCCESS;
            }else{
                EPRINT("open dest [%s]",iow_fstrerror(iow_errno));
                iow_fclose(fp[0]);
            }
        }else{
                EPRINT("open src [%s]",iow_fstrerror(iow_errno));
                iow_fclose(fp[0]);
        }
    }// else invalid arg
    return retval;
}

#ifdef WITH_XCAT
void xcat_help(){

    OUT_CTTY("\r\n");
    OUT_CTTY(" use: cat [h v<n>] [l<n>] [r:<rspec>] <file>\r\n");
    OUT_CTTY(" brief: Show text file regions, specified by time, lines, offset\r\n");
    OUT_CTTY(" input: h        -  help message\r\n");
    OUT_CTTY(" input: v[<n>]   -  verbose output level\r\n");
    OUT_CTTY(" input: l<n>     -  last <n> lines of file\r\n");
    OUT_CTTY(" input: s<n>c<m> -  m lines starting at lines n\r\n");
    OUT_CTTY(" input: <file>   -  file name\r\n");
    OUT_CTTY(" input: <rspec>  -  range specifier\r\n");
    OUT_CTTY("                   comma separated list of range modes and values:\r\n");
    OUT_CTTY("                   <start_mode>,<val>,<end_mode>,<val>  [1,2]\r\n");
    OUT_CTTY("                   <mode> : range type [blims+-]\r\n");
    OUT_CTTY("                            b - byte mode\r\n");
    OUT_CTTY("                            l - line mode\r\n");
    OUT_CTTY("                            i - time mode (ISO8601)     [3]\r\n");
    OUT_CTTY("                            m - time mode (epoch msec)  [3]\r\n");
    OUT_CTTY("                            s - time mode (epoch sec    [3]\r\n");
    OUT_CTTY("                            - - from end of file        [4]\r\n");
    OUT_CTTY("                    <val> : range value\r\n");
    OUT_CTTY("                            line/byte count (index >=0)\r\n");
    OUT_CTTY("                            timestamp (epoch sec/msec or ISO8601)\r\n");
    OUT_CTTY("  Notes\r\n");
    OUT_CTTY("   [1] range specifiers optional; use empty field (no space) if unspecified (,,)\r\n");
    OUT_CTTY("   [2] valid end modes include [blims]; others ignored. Start/end time formats must match.\r\n");
    OUT_CTTY("   [3] file lines must start with specified timestamp (leading space OK)\r\n");
    OUT_CTTY("   [4] line/byte start modes only\r\n");
    OUT_CTTY("    -  Defaults: line mode, start at beginning of file, show all\r\n");
    OUT_CTTY("    -  Use CR or CTRL-D to interrupt\r\n");
    OUT_CTTY("   [5] range values are zero-indexed\r\n");
    OUT_CTTY("\r\n");
    OUT_CTTY("examples:\r\n");
    OUT_CTTY("# show last 10 lines of sys2000.000\r\n");
    OUT_CTTY("   cat r:-,10 sys2000.000\r\n");
    OUT_CTTY("   or\r\n   cat l10 sys2000.000\r\n");
    OUT_CTTY("# show lines 4 to 9 sys2000.000\r\n");
    OUT_CTTY("   cat s4c6 sys2000.000\r\n");
    OUT_CTTY("# show first 10 lines of sys2000.000\r\n");
    OUT_CTTY("   cat r:,,,10 sys2000.000\r\n");
    OUT_CTTY("# show first 10 lines of sys2000.000 >= 2020-03-28T00:00:00\r\n");
    OUT_CTTY("   cat r:i,2020-03-28T00:00:00,l,10 sys2000.000\r\n");
    OUT_CTTY("# show first 10 lines of foo.csv >= 1592974762 (Tue Jun 23 21:59:22 PDT 2020)\r\n");
    OUT_CTTY("   cat r:s,1592974762,l,10 foo.csv\r\n");
    OUT_CTTY("# show 100 bytes starting at line 10 sys2000.000\r\n");
    OUT_CTTY("   cat r:l,10,b,100 sys2000.000\r\n");
    OUT_CTTY("\r\n");
    return;
}

// read and optionally output n lines,
// return offset of last line
uint32_t s_fspan_lines(FILE_T *fp, uint32_t count, bool output)
{

    uint32_t retval=0;
    if(NULL!=fp){
        //        TRACE();
        //        OUT_CTTY("fp[%"FMT_U32"] count[%"FMT_U32"] out[%c]\r\n",iow_ftell(fp),count,(output?'Y':'N'));

        uint32_t start_ofs=iow_ftell(fp);
        uint32_t r_ofs=start_ofs;
        uint32_t r_bytes=0;
        char buf[XC_BUF_LEN]={0};
        uint32_t rem_count=count;
        retval=start_ofs;

        while(( (rem_count>0) || (count==0) ) &&
              iow_feof(fp)==0 &&
              iow_ferror(fp)==0){
            ZERO_MEM(buf,XC_BUF_LEN);
            // mark the file read position
            // and fill the buffer
            r_ofs=iow_ftell(fp);
            if( (r_bytes=iow_fread(fp,buf, XC_BUF_LEN))>0 ){
                // iterate over the buffer, counting lines
                // output as indicated
                char *cur=buf;
                while(cur<(buf+r_bytes) && (rem_count>0 || count==0)){
                    if(output){
                        OUT_CTTY("%c",*cur);
                    }
                    if(*cur=='\n'){
                        rem_count--;
                        start_ofs=r_ofs+cur-buf+1;
                        retval=start_ofs;
                    }
                    cur++;
                }
            }else{
                // error
                TRACE();
                OUT_CTTY("read failed\r\n");
            }
            // break on user input
            int brk=iow_get_char(false);
            if(brk == '\r' || brk == '\n' || brk==CTRL_D) break;

        }
    }
    //    TRACE();
    //    OUT_CTTY("fp[%"FMT_U32"] retval[%"FMT_U32"]\r\n",iow_ftell(fp),retval);
    return retval;
}

// read and optionally output n lines,
// return offset of last line
uint32_t s_fspan_bytes(FILE_T *fp, uint32_t count, bool output)
{

    uint32_t retval=0;
    if(NULL!=fp){
        //        TRACE();
        //        OUT_CTTY("fp[%"FMT_U32"] count[%"FMT_U32"] out[%c]\r\n",iow_ftell(fp),count,(output?'Y':'N'));

        uint32_t start_ofs=iow_ftell(fp);
        uint32_t r_ofs=start_ofs;
        uint32_t r_bytes=0;
        char buf[XC_BUF_LEN]={0};
        uint32_t rem_count=count;
        retval=start_ofs;

        while(( (rem_count>0) || (count==0) ) &&
              iow_feof(fp)==0 &&
              iow_ferror(fp)==0){
            ZERO_MEM(buf,XC_BUF_LEN);
            // mark the file read position
            // and fill the buffer
            r_ofs=iow_ftell(fp);
            if( (r_bytes=iow_fread(fp,buf, XC_BUF_LEN))>0 ){
                // iterate over the buffer, counting lines
                // output as indicated
                char *cur=buf;
                while(cur<(buf+r_bytes) && (rem_count>0 || count==0)){
                    if(output){
                        OUT_CTTY("%c",*cur);
                    }rem_count--;
                    start_ofs=r_ofs+cur-buf+1;
                    retval=start_ofs;
                    cur++;
                }
            }else{
                // error
                TRACE();
                OUT_CTTY("read failed\r\n");
            }
            // break on user input
            int brk=iow_get_char(false);
            if(brk == '\r' || brk == '\n' || brk==CTRL_D) break;

        }
    }
    //    TRACE();
    //    OUT_CTTY("fp[%"FMT_U32"] retval[%"FMT_U32"]\r\n",iow_ftell(fp),retval);
    return retval;
}

// read and optionally output n lines until specified time is reached
// assumes line begins with time (POSIX sec/msec or IOS8601)
// return offset of target line
int s_fspan_time(FILE_T *fp, uint64_t time, xcat_flags_t flags, bool output, uint32_t *o_match, uint32_t *o_stop)
{
    int retval=-1;
    if(NULL!=fp){
        //        OUT_CTTY("fp[%"FMT_U32"] count[%"FMT_U64"] fmt[%08X] out[%c]\r\n",iow_ftell(fp),time,flags,(output?'Y':'N'));

        uint32_t start_ofs=iow_ftell(fp);
        uint32_t line_ofs=start_ofs;
        uint32_t end_ofs=start_ofs;
        uint32_t save_ofs=start_ofs;
        char tbuf[XC_TBUF_LEN]={0};
        uint32_t line_time=0;
        bool match_found=false;

        while(( (line_time<time) || (time==0) ) &&
              !match_found &&
              iow_feof(fp)==0 &&
              iow_ferror(fp)==0){

            // read until non-time char
            // or end of buffer or line
            char *cp=tbuf;
            ZERO_MEM(tbuf,XC_TBUF_LEN);
            bool skip_space=true;
            for(cp=tbuf;cp<(tbuf+XC_TBUF_LEN);){
                if(iow_fread(fp,cp,1)==1){
                    if(*cp=='\n'){
                        line_ofs=save_ofs;
                        save_ofs=iow_ftell(fp);
                        break;
                    }else if(skip_space && isspace((int)*cp)){
                        // skip leading whitespace
                        TRACE();
                    }else if((isdigit((int)*cp) ||
                              *cp=='-' ||
                              *cp==':' ||
                              *cp=='T' ||
                              *cp=='Z')
                             ){
                        if(skip_space)line_ofs=iow_ftell(fp)-1;
                        skip_space=false;
                        // is time
                        cp++;
                    }else{
                        *cp='\0';
                        break;
                    }
                    *cp='\0';
                }else{
                    TRACE();
                    OUT_CTTY("read failed\r\n");
                    break;
                }// iow_fread
                if(iow_feof(fp)!=0){
                    break;
                }
            }// for

            // parse time
            uint64_t line_time=0;
            cp=tbuf;
            while(isspace((int)*cp) && *cp!='\0')cp++;
            struct tm tstm={0};
            time_t tstt={0};
            if(XC_TFMT(flags)!=XCAT_TFMT_ISO && sscanf(tbuf,"%"FMT_U64"",&line_time)==1){
                // time is POSIX
                //                OUT_CTTY("tbuf[%s] ltime[%"FMT_U64"] stime[%"FMT_U64"]\r\n",tbuf,line_time,time);
            }else if( XC_TFMT(flags)==XCAT_TFMT_ISO &&  (tstt=parse_iso8601(cp,&tstm))!=(time_t)-1){
                // time is ISO8601 (epoch sec)
                line_time=tstt;
                if(XC_TFMT_MSEC(flags)){
                    line_time *= 1000;
                }
            }else{
                //                TRACE();
            }// else skip

            // read until end of line
            while(iow_fread(fp,tbuf,1)==1 && tbuf[0]!='\n');
            end_ofs=iow_ftell(fp)-1;

            // compare to range
            if(line_time>=time){
                //                TRACE();
                // found match condition
                match_found=true;
                retval=0;
                break;
            }

            // break on user input
            int brk=iow_get_char(false);
            if(brk == '\r' || brk == '\n' || brk==CTRL_D) break;

        }// while

        if(output && end_ofs>start_ofs){
            iow_fseek(fp,start_ofs);
            uint32_t ofs=iow_ftell(fp);
            while(iow_feof(fp)==0 && (time==0 || ofs<end_ofs) ){
                ZERO_MEM(tbuf,XC_TBUF_LEN);
                if(iow_fread(fp,tbuf,1)==1){
                    OUT_CTTY("%c",tbuf[0]);
                }else{
                    TRACE();
                    break;
                }
                ofs=iow_ftell(fp);
                // break on user input
                int brk=iow_get_char(false);
                if(brk == '\r' || brk == '\n' || brk==CTRL_D) break;
            }
        }

        if(match_found){
            if(NULL!=o_match)*o_match=line_ofs;
            if(NULL!=o_stop)*o_stop=end_ofs;
        }

        //        OUT_CTTY("\r\nfp[%"FMT_U32"] start_ofs[%"FMT_U32"] line_ofs[%"FMT_U32"] end_ofs[%"FMT_U32"] retval[%d]\r\n",iow_ftell(fp),start_ofs,line_ofs,end_ofs,retval);

    }// invalid arg

    return retval;
}

// xcat h v<n> [r:<bmode>,<bval>,<emode>,<eval>] <file>
// modes: blims-file
// b bytes
// l lines
// i ISO
// m POSIX msec
// s POSIX sec
// - from tail

return_code_t xcat(char *arg, xcat_config_t *cfg)
{
    return_code_t retval=ERR_FAILURE;

#if defined(__PIC24FJ256GA106__) || defined(__PIC24FJ256GA606__) || defined(__PIC24FJ1024GA606__)
    FILE_T file_s, *fp=&file_s;
#else
    FILE_T *fp;
#endif

    uint32_t file_len=0;

    if(iow_fopen(&fp,arg,IOW_READ)==0){
        file_len=iow_fsize(fp);
        bool args_valid=true;
        bool start_found=false;


        // validate ranges
        if(XC_MODE_BYTES(cfg->bflags) || XC_MODE_LINES(cfg->bflags) ){
            if(cfg->range_n[0]>file_len ||
               cfg->range_n[1]>file_len ||
               (cfg->range_n[0]+cfg->range_n[1])>file_len){
                args_valid=false;
            }
            if(cfg->range_n[0]<0){
                cfg->range_n[0]=0;
            }
        }

        if(XC_MODE_TIME(cfg->bflags)){
            // format unset
            if(XC_TFMT(cfg->bflags)==0){
                args_valid=false;
            }
            // multiple formats set
            if( !((XC_TFMT(cfg->bflags)==XCAT_TFMT_ISO) ||
                  (XC_TFMT(cfg->bflags)==XCAT_TFMT_POSIX_SEC) ||
                  (XC_TFMT(cfg->bflags)==XCAT_TFMT_POSIX_MSEC))) {
                args_valid=false;
            }
        }

        if(args_valid){
            // find start location
            uint32_t start_ofs=0;

            if(XC_MODE_BYTES(cfg->bflags)){
                if( XC_REF_TAIL(cfg->bflags) ){
                    iow_fseek(fp, file_len-cfg->range_n[0]);
                }else{
                    iow_fseek(fp,cfg->range_n[0]);
                }
                start_ofs=iow_ftell(fp);
                start_found=(iow_errno==IOW_OK?true:false);
            }else if(XC_MODE_LINES(cfg->bflags)){
                int32_t lines=0;
                if( XC_REF_TAIL(cfg->bflags) ){
                    uint32_t ofs=file_len;
                    lines=cfg->range_n[0]+1;
                    char cbuf[1]={0};
                    while(iow_ferror(fp)==0 &&  lines>0){

                        iow_fseek(fp,ofs);
                        cbuf[0]='\0';

                        if(iow_fread(fp,cbuf,1)==1 && cbuf[0]=='\n'){
                            lines--;
                            start_ofs=iow_ftell(fp);
                        }

                        if(ofs==0 || lines==0){
                            start_found=true;
                            break;
                        }
                        ofs--;
                    }
                }else{
                    if(cfg->range_n[0]==0){
                        iow_fseek(fp,0);
                        start_found=(iow_errno==IOW_OK?true:false);
                    }else{
                        start_ofs=s_fspan_lines(fp,cfg->range_n[0],false);
                        start_found=true;
                    }
                }

            }else if(XC_MODE_TIME(cfg->bflags)){
                uint32_t match=0xFFFFFFFF;
                s_fspan_time(fp,cfg->range_t[0],cfg->bflags,false,&match,NULL);
                start_ofs=match;
                start_found=(match!=0xFFFFFFFF?true:false);
            }

            //        OUT_CTTY("fp[%"FMT_U32"] start_ofs[%"FMT_U32"]\r\n",iow_ftell(fp),start_ofs);
            if(start_found){

                //                 OUT_CTTY("\r\n");
                // go to start offset
                iow_fseek(fp,start_ofs);

                // iterate until end condition
                if(XC_MODE_BYTES(cfg->eflags)){
                    //            TRACE();
                    //            OUT_CTTY("fp[%"FMT_U32"]\r\n",iow_ftell(fp));
                    s_fspan_bytes(fp,cfg->range_n[1],true);
                }else if(XC_MODE_LINES(cfg->eflags)){
                    //            TRACE();
                    //            OUT_CTTY("fp[%"FMT_U32"]\r\n",iow_ftell(fp));
                    s_fspan_lines(fp,cfg->range_n[1],true);
                }else if(XC_MODE_TIME(cfg->eflags)){
                    //            TRACE();
                    //            OUT_CTTY("fp[%"FMT_U32"]\r\n",iow_ftell(fp));
                    s_fspan_time(fp,cfg->range_t[1],cfg->eflags,true,NULL,NULL);
                }

                OUT_CTTY("\r\n");

                retval=ERR_SUCCESS;
            }// start_found
        }// args_valid
    }else{
        // open failed
        iow_fstrerror(iow_errno);
        iow_fclose(fp);
    }
    return retval;
}

#else
/// COMMAND: <b>cat <file-name></b>@n
/// mode H (head) prints first len bytes of file
/// mode T (tail) prints last len bytes of file
/// uses HEAD mode if no/invalid mode specified
/// default len is all bytes
/// interrupt using new-line from console
return_code_t fcat(char *arg, byte mode, size_t len)
{
    return_code_t retval=ERR_FAILURE;
    size_t numRead=0;
#if defined(__PIC24FJ256GA106__) || defined(__PIC24FJ256GA606__) || defined(__PIC24FJ1024GA606__)
    FILE_T file_s, *fp=&file_s;
#else
    FILE_T *fp;
#endif

    OUT_CTTY("\r\n");
    if(iow_fopen(&fp,arg,IOW_READ)==0){
    // initialize default read length [bytes remaining]
    uint32_t read_len=iow_fsize(fp);

    // adjust read_len and start offset
    // per specified mode and offset
    if(len>0 && len<=iow_fsize(fp)){
        read_len=len;
        if(mode==FP_MODE_TAIL){
             iow_fseek(fp, iow_fsize(fp)-len);
        }
    }
    // initialize read request size
    size_t buf_sz = (sizeof(g_file_buffer)-1);
    size_t read_req = ( buf_sz > read_len ? read_len : buf_sz );

    // read until read_len is zero
//   DPRINT("%s | FSIZE[%lu] len[%u] read_req[%u] buf_sz [%u] read_len[%lu] numRead[%u] res[%d] brk[%02X]\r\n",__FUNCTION__,FSIZE(&fl),len,read_req,buf_sz,read_len,numRead,res,brk);
     while ((numRead = iow_fread(fp, g_file_buffer, read_req)) == read_req &&
            (numRead > 0) && read_len>0) {
        read_len-=numRead;
        read_req = ( buf_sz > read_len ? read_len : buf_sz );
        g_file_buffer[numRead] = '\0';
#if defined(__PIC24FJ256GA106__) || defined(__PIC24FJ256GA606__) || defined(__PIC24FJ1024GA606__)
        byte flag=0x0;
        serPutPrettyString(g_ctty, (char*) g_file_buffer,&flag);
#else
        OUT_CTTY("%s",(char *)g_file_buffer);
#endif
        // check for user interrupt
//        byte brk=0;
//        serGetByte(g_ctty, &brk);
        int brk=iow_get_char(false);
        if(brk == '\r' || brk == '\n' || brk==CTRL_D) break;
    }

    iow_fclose(fp);
    OUT_CTTY("\r\n");
    retval=ERR_SUCCESS;

    }else{
        iow_fstrerror(iow_errno);
        iow_fclose(fp);
    }

    return retval;
}
#endif //#ifndef WITH_EXTENDED_FCAT


#ifdef WITH_IOF_LESS
return_code_t fless(char *fpath, uint32_t page_rows)
{
    return_code_t retval=ERR_FAILURE;
#if defined(__PIC24FJ256GA106__) || defined(__PIC24FJ256GA606__) || defined(__PIC24FJ1024GA606__)
    FILE_T file_s, *fp=&file_s;
#else
    FILE_T *fp;
#endif
//    FRESULT res=0;
    byte ctrl=0;
    int32_t ofs=0;
    int32_t lines=0;
    uint32_t eof=0;
    uint32_t oofs=0;
    size_t rb=0;
    char lbuf[IOF_LESS_LINEBUF_LEN];
    char *cp=lbuf;
    enum {HELP=0,WAIT,QUIT, PGUP, PGDN, HEAD, TAIL,SEARCH}state;
    char srch[64]={0};

    if(NULL!=fpath && page_rows>0){

        // open or die
//        res = f_open(fp, fpath, FA_READ);

        if((iow_fopen(&fp,fpath,IOW_READ))==0){

            // initialize state machine parameters
            bool init=false;
            state=HELP;
            eof=iow_fsize(fp)-1;

            // enter state machine:
            // - compute offset (modified for some states/operations)
            // - output text
            // - get input
            // - update state
            while(state!=QUIT){

                // adjust file offset per state
                if(state==HEAD){
                     ofs=0;
                }else if(state==TAIL){
                     ofs=eof;
                }else if(state==PGUP){
                    ofs=oofs;
                }else if(state==PGDN){
                    // N/C to ofs
                }else if(state==HELP){
                    OUT_CTTY("\r\n         Command Summary\r\n");
                    OUT_CTTY("    Move :    Up [u U]   Down [d D CR]\r\n");
                    OUT_CTTY("    Jump : First [g <]   Last [G >]\r\n");
                    OUT_CTTY("  Search :  Find [f /]   Next [n N]\r\n");
                    OUT_CTTY(" Session :  Help [h H ?] Quit [q Q x X]\r\n");

                    if(!init){
                        init=true;
                        state=HEAD;
                        ofs=0;
                    }
                }
                // save offset
                oofs=ofs;

                // modify offset if
                // paging up or jump to end
                if(state==PGUP || state==TAIL){
                     lines=0;

                     // must move up one extra '\n'
                     // then advance offset by one
                     // to start at beginning of page
                     while(ofs>0 && lines<(page_rows+1)){

                        ofs-=(ofs>0 ? 1 : ofs);

                        iow_fseek(fp,ofs);

                        if( (rb=iow_fread(fp,cp,1))==1){
                           lines += ( *cp=='\n' ? 1 : 0);
                        }else{
                            TRACE();
                        }

                     }
                    ofs++;
                }

                // modify offset for search
                if(state==SEARCH){
                     // find offset of next instance of search string
                    // set state to do nothing if string not found
                    state=WAIT;
                    DWORD gofs=ofs;
                    int sflags[2]={false,false};

                     while(!sflags[1]){
                        ZERO_MEM(lbuf,IOF_LESS_LINEBUF_LEN);
                        if( iow_fgets(fp,lbuf,IOF_LESS_LINEBUF_LEN)!=NULL ){
                            if(strstr(lbuf,srch)!=NULL){
                                ofs=gofs;
                                // restore state to enable paging
                                state=SEARCH;
                                break;
                            }
                            rb=strlen(lbuf);
                            if( (rb>0 && rb<IOF_LESS_LINEBUF_LEN) || (lbuf[IOF_LESS_LINEBUF_LEN-1]=='\r') || (lbuf[255]=='\n') ){
//                        DPRINT("rb[%3u] gofs[%6ld] eof[%6ld]\r\n",rb,gofs,eof);
                                lines++;
                                gofs+=rb;
                            }
                        }else{
//                            break;
                            gofs=eof;
                        }
                        if(gofs>=eof){
                             iow_fseek(fp,0);
                             gofs=0;
                             sflags[0]=true;
                        }
                        if(sflags[0] && gofs>ofs)
                            sflags[1]=true;
                    }
                    if(state==WAIT){
                        OUT_CTTY("[%s] - not found\r\n",srch);
                    }
                }
                 // check, limit ofs boundaries
                 ofs=(ofs>eof ? 0 : ofs);
                 oofs=ofs;


                // Output page at ofs
                if(state!=WAIT && state!=HELP){
                    lines=0;
                    iow_fseek(fp,ofs);
                    OUT_CTTY("\r\n\r\n");
                    while(lines<page_rows && ofs<=eof){
                        ZERO_MEM(lbuf,256);

                        if( iow_fgets(fp,lbuf,IOF_LESS_LINEBUF_LEN)!=NULL ){
                            rb=strlen(lbuf);
                            if( (rb>0 && rb<IOF_LESS_LINEBUF_LEN) || (lbuf[IOF_LESS_LINEBUF_LEN-1]=='\r') || (lbuf[255]=='\n') ){
                                lines++;
                                ofs+=rb;
                            }
                            cp=lbuf;
                            while(*cp=='\r' || *cp=='\n')cp++;
                            OUT_CTTY("%s",lbuf);
                        }else{
                            break;
                        }
                    }

                     OUT_CTTY(":");
                }

                // Get input
                ctrl=iow_get_char(true);

                // Update state
                state=WAIT;

                switch(ctrl){
                    case 'U':
                    case 'u':
                        state=PGUP;
                        break;
                    case 'D':
                    case 'd':
                    case '\r':
                        state=PGDN;
                        break;
                    case '<':
                    case 'g':
                        state=HEAD;
                        break;
                    case '>':
                    case 'G':
                         state=TAIL;
                      break;
                    case '/':
                    case 'F':
                    case 'f':
                        OUT_CTTY("\b/");
                        ZERO_MEM(srch,IOF_SRCH_MAX_BYTES);
                        iow_get_line(srch,IOF_SRCH_MAX_BYTES);
                        state=SEARCH;
                        break;

                    case 'N':
                    case 'n':
                         OUT_CTTY("next[%s]\r\n",srch);
                       state=SEARCH;
                        break;

                    case 'Q':
                    case 'q':
                    case 'X':
                    case 'x':
                        state=QUIT;
                        break;
                    case 'H':
                    case 'h':
                    case '?':
                        state=HELP;
                        break;
                }
            }
            iow_fclose(fp);
            retval=ERR_SUCCESS;
        }else{
            EPRINT("%s",iow_fstrerror(iow_errno));
            iow_fclose(fp);
       }
    }
    OUT_CTTY("\r\n");
    return retval;
}
#else
return_code_t fless(char *fpath, uint32_t page_rows)
{
    WMSG("Not implemented");
    return ERR_FAILURE;
}
#endif // WITH_IOF_LESS
