/* -*- 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 #include #include #include #include "fsl-ncurses.h" static unsigned ncAttr[fsl_dibu_nc_attr_End + 1 - fsl_dibu_nc_attr__start] = {0,0,0,0,0,0}; unsigned int fsl_dibu_nc_attr_get(fsl_dibu_nc_attr_e n){ return ncAttr[n - fsl_dibu_nc_attr__start]; } void fsl_dibu_nc_attr_set(fsl_dibu_nc_attr_e n, unsigned int v){ ncAttr[n - fsl_dibu_nc_attr__start] = v; } 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(); #define CP(N,F,B,A) init_pair(N, F, B); \ fsl_dibu_nc_attr_set(N, COLOR_PAIR(N) | (A)) CP(fsl_dibu_nc_attr_Window, COLOR_WHITE, COLOR_BLACK, 0); CP(fsl_dibu_nc_attr_Index, COLOR_CYAN, COLOR_BLACK, A_BOLD); CP(fsl_dibu_nc_attr_ChunkSplitter, COLOR_YELLOW, COLOR_BLACK, 0); CP(fsl_dibu_nc_attr_Common, COLOR_WHITE, COLOR_BLACK, 0); //A_DIM CP(fsl_dibu_nc_attr_Insert, COLOR_GREEN, COLOR_BLACK, A_BOLD); CP(fsl_dibu_nc_attr_Delete, COLOR_RED, COLOR_BLACK, A_BOLD); CP(fsl_dibu_nc_attr_Help, COLOR_WHITE, COLOR_RED, (A_BOLD | A_BLINK)); #undef CP fsl_dibu_nc_attr_set(fsl_dibu_nc_attr_End, fsl_dibu_nc_attr_get(fsl_dibu_nc_attr_Window) | A_REVERSE); wbkgd(stdscr, fsl_dibu_nc_attr_get(fsl_dibu_nc_attr_Window)); } 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; yfiller); } 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; xfiller); } 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]maxWidths[col] = n; //n = (uint32_t)fsl_strlen_utf8(z, (fsl_int_t)n); //if(sst->maxWidths[col]maxWidths[col] = n; } } static int fdb__ncu_start(fsl_dibu * const b){ int rc = 0; DSTATE(sst); b->implFlags = 0; if(sst->bufIns.mem) fsl_buffer_reuse(&sst->bufIns); else fsl_buffer_reserve(&sst->bufIns, 1024 * 2); 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 = fsl_dibu_nc_attr_get(fsl_dibu_nc_attr_Index); if(0==(FSL_DIFF2_NOINDEX & b->opt->diffFlags)){ if(b->fileCount>1){/*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; } /** If fsl_dibu_ncu diff builder b has any INSERT lines to flush, this flushes them. Sets b->impl->implFlags to 0. */ static int fdb__ncu_flush_ins(fsl_dibu * const b){ int rc = 0; DSTATE(sst); b->implFlags = 0; if(sst->bufIns.used>0){ rc = fdb__ncu_out(sst, fsl_dibu_nc_attr_get(fsl_dibu_nc_attr_Insert), fsl_buffer_cstr(&sst->bufIns), sst->bufIns.used); fsl_buffer_reuse(&sst->bufIns); } 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 = fsl_dibu_nc_attr_get(fsl_dibu_nc_attr_ChunkSplitter); 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; } /** Outputs line numbers, if configured to, to b->pimpl->pad. - lnL!=0, lnR!=0: common lines - lnL!=0, lnR==0: deletion - lnL==0, lnR!=0: insertion */ 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){ // common or delete attr = fsl_dibu_nc_attr_get(lnR ? fsl_dibu_nc_attr_Common : fsl_dibu_nc_attr_Delete); rc = fdb__ncu_outf(sst, attr, "%*" PRIu32 " ", (int)sst->maxWidths[FDBCols_NUM1], lnL); }else if(1==b->implFlags){ // insert during deletion grouping rc = fsl_buffer_appendf(&sst->bufIns, "%.*c ", (int)sst->maxWidths[FDBCols_NUM1], ' '); }else{ // insert w/o deleting grouping rc = fdb__ncu_outf(sst, 0, "%.*c ", (int)sst->maxWidths[FDBCols_NUM1], ' '); } if(0==rc){ if(!lnL && lnR && 1==b->implFlags){ // insert during deletion grouping rc = fsl_buffer_appendf(&sst->bufIns, "%*"PRIu32" ", (int)sst->maxWidths[FDBCols_NUM2], lnR); }else if(lnR){ // common or insert w/o deletion grouping attr = fsl_dibu_nc_attr_get(lnL ? fsl_dibu_nc_attr_Common : fsl_dibu_nc_attr_Insert); rc = fdb__ncu_outf(sst, attr, "%*" PRIu32 " ", (int)sst->maxWidths[FDBCols_NUM2], lnR); }else{ // deletion 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); return 0; } int rc = fdb__ncu_flush_ins(b); if(0==rc) rc = fdb__ncu_lineno(b, b->lnLHS, b->lnRHS); return rc ? rc : fdb__ncu_outf(sst, fsl_dibu_nc_attr_get(fsl_dibu_nc_attr_Common), " %.*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; } int rc = fdb__ncu_lineno(b, 0, b->lnRHS); if(0==rc){ if(1==b->implFlags){ rc = fsl_buffer_appendf(&sst->bufIns,"+%.*s\n", (int)pLine->n, pLine->z); }else{ rc = fdb__ncu_outf(sst, fsl_dibu_nc_attr_get(fsl_dibu_nc_attr_Insert), "+%.*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; } int rc = (1==b->implFlags) ? 0 : fdb__ncu_flush_ins(b); if(0==rc){ b->implFlags = 1; rc = fdb__ncu_lineno(b, b->lnLHS, 0); } return rc ? rc : fdb__ncu_outf(sst, fsl_dibu_nc_attr_get(fsl_dibu_nc_attr_Delete), "-%.*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; idisplayWidth += 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; else{ wbkgd(sst->pad, fsl_dibu_nc_attr_get(fsl_dibu_nc_attr_Window)); } } 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; } rc = fdb__ncu_flush_ins(b); fsl_buffer_reuse(&sst->bufIns); fsl_buffer_reuse(&sst->buf); return rc; } static int fdb__ncu_finally(fsl_dibu * const b){ DSTATE(sst); b->fileCount = 0; return fdb__ncu_outf(sst, fsl_dibu_nc_attr_get(fsl_dibu_nc_attr_End), "(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); fsl_buffer_clear(&sst->bufIns); 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(); noecho(); getyx(w,wTop,wLeft); getmaxyx(nc->pad,pH,pW); while(1){ getmaxyx(w,wH,wW)/*can change dynamically*/; //touchwin(w); bool const scrollV = wH=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 = fsl_dibu_nc_attr_get(fsl_dibu_nc_attr_Help); 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; } } }