Login
fsl-ncurses.c at [b7046bf39b]
Login

File f-apps/fsl-ncurses.c artifact 29f55ef0b9 part of check-in b7046bf39b


/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 
/* vim: set ts=2 et sw=2 tw=80: */
/*
  Copyright 2021 The Libfossil Authors, see LICENSES/BSD-2-Clause.txt

  SPDX-License-Identifier: BSD-2-Clause-FreeBSD
  SPDX-FileCopyrightText: 2021 The Libfossil Authors
  SPDX-ArtifactOfProjectName: Libfossil
  SPDX-FileType: Code

  Heavily indebted to the Fossil SCM project (https://fossil-scm.org).
*/
/*
  This file holds a custom fsl_dibu implementation which
  renders its output to an ncurses PAD
*/
#include "libfossil.h"
#include <ncurses.h>
#include <panel.h>
#include <stdarg.h>
#include <locale.h>

#include "fsl-ncurses.h"

void fnc_screen_init(void){
  if(NULL!=stdscr) return;
  setlocale(LC_ALL, "")/*needed for ncurses w/ UTF8 chars*/;
  initscr();
  nonl(); keypad(stdscr,TRUE); cbreak();
  if(has_colors()){
    start_color();
    init_pair(FNColorIndex, COLOR_BLUE, COLOR_BLACK);
    init_pair(FNColorChunkSplitter, COLOR_YELLOW, COLOR_BLACK);
    init_pair(FNColorInsert, COLOR_GREEN, COLOR_BLACK);
    init_pair(FNColorDelete, COLOR_RED, COLOR_BLACK);
    init_pair(FNColorNote, COLOR_WHITE, COLOR_RED);
  }
  curs_set(0);
}

void fnc_screen_shutdown(void){
  if(NULL!=stdscr){
    endwin();
  }
}

/**
   Characters used in rendering scrollbars.
*/
static const struct NCScrollbarConfig {
  char const * vTop;
  char const * vBottom;
  char const * hLeft;
  char const * hRight;
  char const * curPos;
  char const * filler;
} NCScrollbarConfig = {
"△",//vTop
"▽",//vBottom
"◁",//hLeft
"▷",//hRight
"▣",//curPos
"░"//filler
};

/**
   Renders a vertical scrollbar on window tgt at column x, running
   from the given top row to the row (top+barHeight). The scroll
   indicator is rendered depending on the final two arguments:
   lineCount specifies the number of lines in the scrollable
   input and currentLine specifies the top-most line number of the
   currently-displayed data. e.g. a lineCount of 100 and currentLine
   of 1 puts the indicator 1% of the way down the scrollbar. Likewise,
   a lineCount of 100 and currentLine of 90 puts the indicator at the
   90% point.
 */
void fsl_nc_scrollbar_v( WINDOW * const tgt, int top, int x, 
                         int barHeight, int lineCount,
                         int currentLine ){
  int y, bottom = top + barHeight - 1,
    dotPos = top + (int)((double)currentLine / lineCount * barHeight) + 1;
  struct NCScrollbarConfig const * const conf = &NCScrollbarConfig;
  unsigned int const attr = A_REVERSE;
  wattron(tgt, attr);
  mvwprintw(tgt, top, x, "%s", conf->vTop);
  for(y = top+1; y<bottom; ++y){
    mvwprintw(tgt, y, x, "%s", conf->filler);
  }
  wattroff(tgt, attr);
  mvwprintw(tgt, dotPos>=bottom?bottom-1:dotPos, x,
            "%s", conf->curPos);
  wattron(tgt, attr);
  mvwprintw(tgt, bottom, x, "%s", conf->vBottom);
  wattroff(tgt, attr);
}


/**
   Works like fsl_nc_scrollbar_v() but renders a horizontal scrollbar
   at the given left/top coordinates of tgt.
*/
void fsl_nc_scrollbar_h( WINDOW * const tgt, int top, int left, 
                         int barWidth, int colCount,
                         int currentCol ){
  int x, right = left + barWidth - 1,
    dotPos = left + (int)((double)currentCol / colCount * barWidth) + 1;
  struct NCScrollbarConfig const * const conf = &NCScrollbarConfig;
  unsigned int const attr = A_REVERSE;
  wattron(tgt, attr);
  mvwprintw(tgt, top, left, "%s", conf->hLeft);
  for(x = left+1; x<right; ++x){
    mvwprintw(tgt, top, x, "%s", conf->filler);
  }
  wattroff(tgt, attr);
  mvwprintw(tgt, top, dotPos>=x?x-1:dotPos,
            "%s", conf->curPos);
  wattron(tgt, attr);
  mvwprintw(tgt, top, x, "%s", conf->hRight);
  wattroff(tgt, attr);
}

static const fsl_dibu_ncu fsl_dibu_ncu_empty = {
{7,0,1,7,0},{0,0},
0/*displayLines*/,
0/*displayWidth*/,
NULL/*pad*/,
{/*cursor*/0,0},
fsl_buffer_empty_m
};
#define DSTATE(VNAME) fsl_dibu_ncu * const VNAME = (fsl_dibu_ncu *)b->pimpl

static int fdb__ncu_out(fsl_dibu_ncu *const sst, unsigned int ncattr,
                       char const *z, fsl_size_t n){
  int rc = 0;
  if(ncattr) wattron(sst->pad, ncattr);
#if 0
  /* Failing at some point i've yet to track down, even though
     rendering is okay. */
  rc = ERR==waddnstr(sst->pad, z, (int)n) ? FSL_RC_IO : 0;
#else
  waddnstr(sst->pad, z, (int)n);
#endif
  if(ncattr) wattroff(sst->pad, ncattr);
  return rc;
}

static int fdb__ncu_outf(fsl_dibu_ncu * const sst, unsigned int ncattr,
                        char const *fmt, ...){
  int rc = 0;
  if(NULL!=stdscr){
    va_list va;
    va_start(va,fmt);
    rc = fsl_buffer_appendfv(fsl_buffer_reuse(&sst->buf), fmt, va);
    if(0==rc) rc = fdb__ncu_out(sst, ncattr, fsl_buffer_cstr(&sst->buf), sst->buf.used);
    va_end(va);
  }
  return rc;
}

#if 0
static int maxColWidth(fsl_dibu const * const b,
                       fsl_dibu_ncu const * const sst,
                       int mwIndex){
  static const short minColWidth =
    10/*b->opt.columnWidth values smaller than this are treated as
        this value*/;
  switch(mwIndex){
    case FDBCols_NUM1:
    case FDBCols_NUM2:
    case FDBCols_MOD: return sst->maxWidths[mwIndex];
    case FDBCols_TEXT1: case FDBCols_TEXT2: break;
    default:
      assert(!"internal API misuse: invalid column index.");
      break;
  }
  int const y =
    (b->opt->columnWidth>0
     && b->opt->columnWidth<=sst->maxWidths[mwIndex])
    ? (int)b->opt->columnWidth
    : (int)sst->maxWidths[mwIndex];
  return minColWidth > y ? minColWidth : y;
}
#endif

static void fdb__dico_update_maxlen(fsl_dibu_ncu * const sst,
                                    int col,
                                    char const * const z,
                                    uint32_t n){
  switch(col){
    case FDBCols_TEXT1: n+=2; break;
    default: break;
  }
  if(sst->maxWidths[col]<n){
    sst->maxWidths[col] = n;
    //n = (uint32_t)fsl_strlen_utf8(z, (fsl_int_t)n);
    //if(sst->maxWidths[col]<n) sst->maxWidths[col] = n;
  }
}

static int fdb__ncu_start(fsl_dibu * const b){
  int rc = 0;
  DSTATE(sst);
  if(1==b->passNumber){
    if(1==++b->fileCount){
      assert(!sst->pad);
      *sst = fsl_dibu_ncu_empty;
    }
    if(0==(FSL_DIFF2_NOINDEX & b->opt->diffFlags)){
      sst->displayLines += 2;
      if(b->fileCount>1){
        ++sst->displayLines/*gap before 2nd+ index line*/;
      }
    }
    sst->displayLines += 2;
    fsl_size_t nName;
    if(b->opt->nameLHS &&
       (nName=fsl_strlen(b->opt->nameLHS)+5) >= sst->displayWidth){
      sst->displayWidth = nName;
    }
    if(b->opt->nameRHS &&
       (nName=fsl_strlen(b->opt->nameRHS)+5) >= sst->displayWidth){
      sst->displayWidth = nName;
    }
    return rc;
  }
  unsigned int attr = COLOR_PAIR(FNColorIndex);
  if(0==(FSL_DIFF2_NOINDEX & b->opt->diffFlags)){
    if(1<++sst->cursor.y){/*gap before 2nd+ index line*/
      fdb__ncu_out(sst, attr,"\n",1);
    }
    fdb__ncu_outf(sst, attr,"Index #%d: %s\n", (int)b->fileCount,b->opt->nameLHS/*RHS?*/);
    fdb__ncu_outf(sst, attr,"%.*c\n", (int)sst->displayWidth-2, '=');
  }
  fdb__ncu_outf(sst, attr,"--- %s\n", b->opt->nameLHS);
  fdb__ncu_outf(sst, attr,"+++ %s\n", b->opt->nameRHS);
  return rc;
}


static int fdb__ncu_chunkHeader(fsl_dibu* const b,
                                  uint32_t lnnoLHS, uint32_t linesLHS,
                                  uint32_t lnnoRHS, uint32_t linesRHS ){
  DSTATE(sst);
  if(1==b->passNumber){
    if(FSL_DIFF2_LINE_NUMBERS & b->opt->diffFlags){
      ++sst->displayLines;
    }
    ++sst->displayLines;
    return 0;
  }
  unsigned int const attr = COLOR_PAIR(FNColorChunkSplitter);
  if(FSL_DIFF2_LINE_NUMBERS & b->opt->diffFlags){
    fdb__ncu_outf(sst,attr, "%.40c\n", '~');
  }
  return fdb__ncu_outf(sst, attr, "@@ -%" PRIu32 ",%" PRIu32
                       " +%" PRIu32 ",%" PRIu32 " @@\n",
                       lnnoLHS, linesLHS, lnnoRHS, linesRHS);
}

static int fdb__ncu_skip(fsl_dibu * const b, uint32_t n){
  b->lnLHS += n;
  b->lnRHS += n;
  return 0;
}

/**
   Render the line numbers, if enabled. For an insert, pass lnL=0,
   for a delete pass lnR=0. For common lines, pass non-0 for both.
*/
static int fdb__ncu_lineno(fsl_dibu * const b, uint32_t lnL, uint32_t lnR){
  int rc = 0;
  if(FSL_DIFF2_LINE_NUMBERS & b->opt->diffFlags){
    DSTATE(sst);
    unsigned int attr = 0;
    if(lnL){
      attr = COLOR_PAIR(FNColorDelete) | A_BOLD;
      rc  = fdb__ncu_outf(sst, lnR ? 0 : attr, "%*" PRIu32 " ",
                          (int)sst->maxWidths[FDBCols_NUM1], lnL);
    }else{
      rc = fdb__ncu_outf(sst, 0, "%.*c ",
                         (int)sst->maxWidths[FDBCols_NUM1], ' ');
    }
    if(0==rc){
      if(lnR){
        attr = COLOR_PAIR(FNColorInsert) | A_BOLD;
        rc = fdb__ncu_outf(sst, lnL ? 0 : attr, "%*" PRIu32 " ",
                           (int)sst->maxWidths[FDBCols_NUM2], lnR);
      }else{
        rc = fdb__ncu_outf(sst, 0, "%.*c ",
                           (int)sst->maxWidths[FDBCols_NUM2], ' ');
      }
    }
  }
  return rc;
}

static int fdb__ncu_common(fsl_dibu * const b, fsl_dline const * pLine){
  DSTATE(sst);
  ++b->lnLHS;
  ++b->lnRHS;
  if(1==b->passNumber){
    ++sst->displayLines;
    fdb__dico_update_maxlen(sst, FDBCols_TEXT1, pLine->z, pLine->n);
    //fdb__dico_update_maxlen(sst, FDBCols_TEXT2, pLine->z, pLine->n);
    return 0;
  }
  const int rc = fdb__ncu_lineno(b, b->lnLHS, b->lnRHS);
  return rc ? rc : fdb__ncu_outf(sst, 0, " %.*s\n", (int)pLine->n, pLine->z);
}
static int fdb__ncu_insertion(fsl_dibu * const b, fsl_dline const * pLine){
  DSTATE(sst);
  ++b->lnRHS;
  if(1==b->passNumber){
    ++sst->displayLines;
    fdb__dico_update_maxlen(sst, FDBCols_TEXT1, pLine->z, pLine->n);
    return 0;
  }
  const int rc = fdb__ncu_lineno(b, 0, b->lnRHS);
  return rc ? rc
    : fdb__ncu_outf(sst, COLOR_PAIR(FNColorInsert)|A_BOLD,
                    "+%.*s\n", (int)pLine->n, pLine->z);
  return rc;
}
static int fdb__ncu_deletion(fsl_dibu * const b, fsl_dline const * pLine){
  DSTATE(sst);
  ++b->lnLHS;
  if(1==b->passNumber){
    ++sst->displayLines;
    fdb__dico_update_maxlen(sst, FDBCols_TEXT1, pLine->z, pLine->n);
    return 0;
  }
  const int rc = fdb__ncu_lineno(b, b->lnLHS, 0);
  return rc ? rc
    : fdb__ncu_outf(sst, COLOR_PAIR(FNColorDelete)|A_BOLD,
                     "-%.*s\n", (int)pLine->n, pLine->z);
}
static int fdb__ncu_replacement(fsl_dibu * const b,
                                fsl_dline const * lineLhs,
                                fsl_dline const * lineRhs) {
  int rc = b->deletion(b, lineLhs);
  if(0==rc) rc = b->insertion(b, lineRhs);
  return rc;
}
                 
static int fdb__ncu_edit(fsl_dibu * const b,
                           fsl_dline const * lineLHS,
                           fsl_dline const * lineRHS){
  int rc = b->deletion(b, lineLHS);
  if(0==rc) rc = b->insertion(b, lineRHS);
  return rc;
}

static int fdb__ncu_finish(fsl_dibu * const b){
  int rc = 0;
  DSTATE(sst);
  if(1==b->passNumber){
    if(sst->lineCount[0] < b->lnLHS) sst->lineCount[0] = b->lnLHS;
    if(sst->lineCount[1] < b->lnRHS) sst->lineCount[1] = b->lnRHS;
    // Calculate line number column widths...
    if(FSL_DIFF2_LINE_NUMBERS & b->opt->diffFlags){
      uint32_t x = 1, li = sst->lineCount[0];
      while( li >= 10 ){ li /= 10; ++x; }
      sst->maxWidths[FDBCols_NUM1] = x;
      x = 1;
      li = sst->lineCount[1];
      while( li >= 10 ){ li /= 10; ++x; }
      sst->maxWidths[FDBCols_NUM2] = x;
    }else{
      sst->maxWidths[FDBCols_NUM1]
        = sst->maxWidths[FDBCols_NUM2] = 4/*fudge value :/ */;
    }
    sst->displayWidth = 5/*semi-magic: avoids RHS truncation*/;
    for( int i = 0; i<FDBCols_count; ++i ){
      sst->displayWidth += sst->maxWidths[i];
    }
    if(sst->pad){
      int w, h, maxW;
      getmaxyx(sst->pad, h, w);
      if(h){/*unused but we need it for the getmaxyx() call*/}
      maxW = (int)sst->displayWidth>w
        ? (int)sst->displayWidth
        : w;
      if(wresize(sst->pad, sst->displayLines, maxW)){
        rc = FSL_RC_OOM;
      }
      sst->displayWidth = (unsigned)maxW;
    }else{
      ++sst->displayLines/* "END" marker */;
      sst->pad = newpad((int)sst->displayLines, (int)sst->displayWidth);
      if(!sst->pad) rc = FSL_RC_OOM;
    }
    if(FSL_RC_OOM==rc){
      rc = fcli_err_set(rc, "Could not (re)allocate PAD for "
                        "diff display.");
    }else{
      fsl_buffer_reserve(&sst->buf, sst->displayWidth*2);
    }
    return rc;
  }
  return rc;
}

static int fdb__ncu_finally(fsl_dibu * const b){
  DSTATE(sst);
  b->fileCount = 0;
  return fdb__ncu_outf(sst, A_REVERSE, "(END)%.*c\n",
                       (int)sst->displayWidth-7, '=');
}

static void fsl__diff_builder_ncu_finalizer(fsl_dibu * const b){
  DSTATE(sst);
  fsl_buffer_clear(&sst->buf);
  if(sst->pad){
    delwin(sst->pad);
  }
  *sst = fsl_dibu_ncu_empty;
  *b = fsl_dibu_empty;
  fsl_free(b);
}

bool fsl_dibu_is_ncu(fsl_dibu const * const b){
  return b && b->typeID==&fsl_dibu_ncu_empty;
}

fsl_dibu_ncu * fsl_dibu_ncu_pimpl(fsl_dibu * const b){
  return b->typeID==&fsl_dibu_ncu_empty
    ? (fsl_dibu_ncu*)b->pimpl : NULL;
}

fsl_dibu * fsl_dibu_ncu_alloc(void){
  fsl_dibu * rc =
    fsl_dibu_alloc((fsl_size_t)sizeof(fsl_dibu_ncu));
  if(rc){
    rc->typeID = &fsl_dibu_ncu_empty;
    rc->chunkHeader = fdb__ncu_chunkHeader;
    rc->start = fdb__ncu_start;
    rc->skip = fdb__ncu_skip;
    rc->common = fdb__ncu_common;
    rc->insertion = fdb__ncu_insertion;
    rc->deletion = fdb__ncu_deletion;
    rc->replacement = fdb__ncu_replacement;
    rc->edit = fdb__ncu_edit;
    rc->finish = fdb__ncu_finish;
    rc->finally = fdb__ncu_finally;
    rc->finalize = fsl__diff_builder_ncu_finalizer;
    rc->twoPass = true;
    assert(0!=rc->pimpl);
    fsl_dibu_ncu * const sst = (fsl_dibu_ncu*)rc->pimpl;
    *sst = fsl_dibu_ncu_empty;
    assert(0==rc->implFlags);
    assert(0==rc->lnLHS);
    assert(0==rc->lnRHS);
    assert(NULL==rc->opt);
  }
  return rc;
}

void fsl_dibu_ncu_basic_loop(fsl_dibu_ncu * const nc,
                             WINDOW * const w){
  int pTop = 0, pLeft = 0;
  int wW/*window width*/, wH/*window height*/,
    wTop/*window Y*/, wLeft/*window X*/,
    pW/*pad width*/, pH/*pad height*/,
    vH/*viewport height*/, vW/*viewport width*/,
    ch/*wgetch() result*/;
  int const hScroll = 2/*cols to shift for horizontal scroll*/;
  int showHeader = 1;
  if(!nc->pad){
    mvwaddstr(stdscr,1,0,"No diff output generated. Tap a key.");
    wrefresh(stdscr);
    wgetch(stdscr);
    return;
  }
  keypad(w,TRUE);
  cbreak();
  getyx(w,wTop,wLeft);
  getmaxyx(nc->pad,pH,pW);
  noecho();
  while(1){
    getmaxyx(w,wH,wW)/*can change dynamically*/;
    //touchwin(w);
    bool const scrollV = wH<pH;
    bool const scrollH = wW<pW;
    vH = wH - scrollV;
    vW  = wW - wLeft - scrollH;
    if(vH>=LINES){
      vH = LINES - scrollH;
    }
    if(vW>=COLS-1){
      vW = COLS-wLeft-scrollH;
    }
    if(pTop + wH - scrollH >= pH) pTop = pH - wH + scrollH;
    if(pTop<0) pTop=0;
    if(pLeft<0) pLeft=0;
    else if(pW - pLeft < vW) pLeft = pW - vW;
    switch(showHeader){
      case 1:{
        unsigned int attr = COLOR_PAIR(FNColorNote) | A_BOLD | A_BLINK;
        char const * zLabel = "Arrows scroll, 'q'uits, ctrl-l refreshes.";
        wattron(w, attr);
        mvwprintw(w,1,(vW/2 - fsl_strlen(zLabel)/2), zLabel);
        wattroff(w, attr);
        ++showHeader;
        break;
      }
      case 2:
        wmove(w,1,0);
        wclrtoeol(w);
        ++showHeader;
        break;
    }
    prefresh(nc->pad, pTop, pLeft, wTop, wLeft, vH,vW);
    if(scrollV) fsl_nc_scrollbar_v(w, 0, wW-1, wH-1+!scrollH,
                                   pH - vH, pTop);
    if(scrollH) fsl_nc_scrollbar_h(w, wH-1, 0, wW-1+!scrollV,
                                   pW - vW, pLeft);
    doupdate();
    ch = wgetch(w);
    if('q'==ch) break;
    switch(ch){
      case KEY_HOME: pTop = 0; break;
      case KEY_END: pTop = pH - vH;
      case KEY_DOWN: ++pTop; break;
      case KEY_UP: --pTop; break;
      case KEY_NPAGE: pTop += vH; break;
      case KEY_PPAGE: pTop -= vH; break;
      case KEY_LEFT: pLeft -= hScroll; break;
      case KEY_RIGHT: pLeft += hScroll; break;
      case KEY_ctrl('l'):
        wclear(w);
        wrefresh(w);
        break;        
    }
  }
}