/* $Id$ */
#include "bbs.h"
/*
* piaip's new implementation of pager(more) with mmap,
* designed for unlimilited length(lines).
*
* Author: Hung-Te Lin (piaip), 2005
* <piaip@csie.ntu.edu.tw>
*
* MAJOR IMPROVEMENTS:
* - Clean source code, and more readble to mortal
* - Correct navigation
* - Excellent search ability (for correctness and user behavior)
* - Less memory consumption (mmap is not considered)
* - Better support for large terminals
* - Unlimited file length and line numbers
*
* TODO:
* - Speed up with supporting Scroll [done]
* - Support PTT_PRINTS [done]
* - Wrap long lines or left-right wide navigation
* - Big5 truncation
* - Non-local article header format incompatible with old more
*
* WONTDO:
* - The message seperator line is different from old more.
* I decided to abandon the old style (which is buggy).
* > old style: increase one line to show seperator
* > pmore style: use blank line for seperator.
*
* HINTS:
* - Remember mmap pointers are NOT null terminated strings.
* You have to use strn* APIs and make sure not exceeding mmap buffer.
* DO NOT USE strcmp, strstr, strchr, ...
* - Scroll handling is painful. If you displaed anything on screen,
* remember to MFDISP_DIRTY();
*/
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <ctype.h>
#include <string.h>
// Platform Related. NoSync is faster but if we don't have it...
#ifndef MAP_NOSYNC
#define MAP_NOSYNC MAP_SHARED
#endif
//#define DEBUG
// -------------------------- <FEATURES>
#define PMORE_USE_PTT_PRINTS
#define PMORE_USE_SCROLL
// -------------------------- </FEATURES>
typedef struct
{
unsigned char
*start, *end, // file buffer
*disps, *dispe, // disply start/end
*maxdisps; // a very special pointer,
// consider as "disps of last page"
off_t len, // file total length
lineno, // lineno of disps
oldlineno; // last drawn lineno, < 0 means full update
} MmappedFile;
MmappedFile mf = { 0, 0, 0, 0, 0, 0, 0 }; // current file
/* mf_* navigation commands and return value meanings */
enum {
MFNAV_OK, // navigation ok
MFNAV_EXCEED, // request exceeds buffer
} MF_NAV_COMMANDS;
#define MFNAV_PAGE (t_lines-2) // when navigation, how many lines in a page to move
#define MFDISP_PAGE (t_lines-1) // for display, the real number of lines to be shown.
#define MFDISP_DIRTY() { mf.oldlineno = -1; }
#define RESETMF() { memset(&mf, 0, sizeof(mf)); mf.oldlineno = -1; }
#define RESETAH() { memset(&ah, 0, sizeof(ah)); }
#define ANSI_ESC (0x1b)
/* search records */
typedef struct
{
int len;
int (*cmpfunc) (const char *, const char *, size_t);
char search_str[81]; // maybe we can change to dynamic allocation
} SearchRecord;
SearchRecord sr = { 0, strncmp, "" };
enum {
MFSEARCH_FORWARD,
MFSEARCH_BACKWARD,
} MFSEARCH_DIRECTION;
int mf_backward(int); // used by mf_attach
/*
* mmap basic operations
*/
int mf_attach(unsigned char *fn)
{
struct stat st;
int fd = open(fn, O_RDONLY, 0600);
if(fd < 0)
return 0;
if (fstat(fd, &st) || ((mf.len = st.st_size) <= 0) || S_ISDIR(st.st_mode))
{
mf.len = 0;
close(fd);
return 0;
}
/*
mf.len = lseek(fd, 0L, SEEK_END);
lseek(fd, 0, SEEK_SET);
*/
mf.start = mmap(NULL, mf.len, PROT_READ,
MAP_NOSYNC, fd, 0);
close(fd);
if(mf.start == MAP_FAILED)
{
RESETMF();
return 0;
}
mf.end = mf.start + mf.len;
mf.disps = mf.dispe = mf.start;
mf.lineno = 0;
// build maxdisps
mf.disps = mf.end - 1;
mf_backward(MFNAV_PAGE);
mf.maxdisps = mf.disps;
mf.disps = mf.dispe = mf.start;
mf.lineno = 0;
return 1;
}
void mf_detach()
{
if(mf.start) {
munmap(mf.start, mf.len);
RESETMF();
}
}
/*
* lineno calculation, and moving
*/
void mf_sync_lineno()
{
unsigned char *p;
mf.lineno = 0;
for (p = mf.start; p < mf.disps; p++)
if(*p == '\n')
mf.lineno ++;
}
int mf_backward(int lines)
{
int flFirstLine = 1;
// first, because we have to trace back to line beginning,
// add one line.
lines ++;
// now try to rollback for lines
if(lines == 1)
{
/* special case! just rollback to start */
while ( mf.disps > mf.start &&
*(mf.disps-1) != '\n')
mf.disps --;
mf.disps --;
lines --;
}
else while(mf.disps > mf.start && lines > 0)
{
while (mf.disps > mf.start && *--mf.disps != '\n');
if(flFirstLine)
{
flFirstLine = 0; lines--;
continue;
}
if(mf.disps >= mf.start)
mf.lineno--, lines--;
}
if(mf.disps == mf.start)
mf.lineno = 0;
else
mf.disps ++;
if(lines > 0)
return MFNAV_OK;
else
return MFNAV_EXCEED;
}
int mf_forward(int lines)
{
while(mf.disps <= mf.maxdisps && lines > 0)
{
while (mf.disps <= mf.maxdisps && *mf.disps++ != '\n');
if(mf.disps <= mf.maxdisps)
mf.lineno++, lines--;
}
if(mf.disps > mf.maxdisps)
mf.disps = mf.maxdisps;
if(lines > 0)
return MFNAV_OK;
else
return MFNAV_EXCEED;
}
int mf_goTop()
{
mf.disps = mf.start;
mf.lineno = 0;
return MFNAV_OK;
}
int mf_goBottom()
{
mf.disps = mf.maxdisps;
/*
mf.disps = mf.end-1;
mf_backward(MFNAV_PAGE);
*/
// lineno?
mf_sync_lineno();
return MFNAV_OK;
}
int mf_goto(int lineno)
{
mf.disps = mf.start;
mf.lineno = 0;
return mf_forward(lineno);
}
int mf_viewedNone()
{
return (mf.disps <= mf.start);
}
int mf_viewedAll()
{
return (mf.dispe >= mf.end);
}
/*
* search!
*/
int mf_search(int direction)
{
unsigned char *s = sr.search_str;
int l = sr.len;
int flFound = 0;
if(!*s)
return 0;
if(direction == MFSEARCH_FORWARD)
{
mf_forward(1);
while(mf.disps < mf.end - l)
{
if(sr.cmpfunc(mf.disps, s, l) == 0)
{
flFound = 1;
break;
} else
mf.disps ++;
}
mf_backward(0);
if(mf.disps > mf.maxdisps)
mf.disps = mf.maxdisps;
mf_sync_lineno();
}
else if(direction == MFSEARCH_BACKWARD)
{
mf_backward(1);
while (!flFound && mf.disps > mf.start)
{
while(!flFound && mf.disps < mf.end-l && *mf.disps != '\n')
{
if(sr.cmpfunc(mf.disps, s, l) == 0)
{
flFound = 1;
} else
mf.disps ++;
}
if(!flFound)
mf_backward(1);
}
mf_backward(0);
if(mf.disps < mf.start)
mf.disps = mf.start;
mf_sync_lineno();
}
if(flFound)
MFDISP_DIRTY();
return flFound;
}
/*
* Format Related
*/
typedef struct
{
int lines; // header lines
int authorlen;
int boardlen;
} ArticleHeader;
ArticleHeader ah;
void mf_parseHeader()
{
/* format:
* AUTHOR: author BOARD: blah
* XXX: xxx
* XXX: xxx
* [blank, fill with seperator]
*
* #define STR_AUTHOR1 "作者:"
* #define STR_AUTHOR2 "發信人:"
* #define STR_POST1 "看板:"
* #define STR_POST2 "站內:"
*/
RESETAH();
ah.lines = -1;
ah.authorlen= -1;
ah.boardlen = -1;
if(mf.len > LEN_AUTHOR2)
{
if (strncmp(mf.start, STR_AUTHOR1, LEN_AUTHOR1) == 0)
{
ah.lines = 3; // local
ah.authorlen = LEN_AUTHOR1;
}
else if (strncmp(mf.start, STR_AUTHOR2, LEN_AUTHOR2) == 0)
{
ah.lines = 4;
ah.authorlen = LEN_AUTHOR2;
}
/* traverse for author length */
{
unsigned char *p = mf.start;
unsigned char *pb = p;
/* first, go to line-end */
while(p < mf.end && *p != '\n')
p++;
pb = p;
/* next, rollback for ':' */
while(p > mf.start && *p != ':')
p--;
if(p > mf.start && *p == ':')
{
ah.boardlen = pb - p;
while (p > mf.start && *p != ' ')
p--;
if( *p == ' ')
{
ah.authorlen = p - mf.start - ah.authorlen;
} else
ah.boardlen = -1, ah.authorlen = -1;
} else
ah.authorlen = -1;
}
}
}
/*
* display mf content from disps for MFDISP_PAGE
*/
#define STR_ANSICODE "[0123456789;,"
#define DISP_HEADS_LEN (4) // strlen of each heads
static const char *disp_heads[] = {"作者", "標題", "時間", "轉信"};
void mf_disp()
{
int lines = 0, col = 0, currline = 0;
int startline = 0, endline = MFDISP_PAGE-1;
#ifdef PMORE_USE_SCROLL
/* process scrolling */
if (mf.oldlineno >= 0 && mf.oldlineno != mf.lineno)
{
int scrll = mf.lineno - mf.oldlineno, i;
int reverse = (scrll > 0 ? 0 : 1);
if(reverse)
scrll = -scrll;
else
{
/* because bottom status line is also scrolled,
* we have to erase it here.
*/
move(b_lines, 0);
clrtoeol();
}
if(scrll > MFDISP_PAGE)
scrll = MFDISP_PAGE;
if(reverse)
{
// clear the line which will be scrolled
// to bottom (status line position).
move(b_lines - scrll, 0);
clrtoeol();
// move(b_lines, 0);
// clrtoeol();
}
i = scrll;
while(i-- > 0)
if (reverse)
rscroll(); // v
else
scroll(); // ^
if(reverse)
{
startline = 0; // v
endline = scrll-1;
}
else
{
startline = MFDISP_PAGE - scrll; // ^
endline = MFDISP_PAGE - 1;
}
move(startline, 0);
// return; // uncomment if you want to observe scrolling
}
else
#endif
clear(), move(0, 0);
mf.dispe = mf.disps;
while (lines < MFDISP_PAGE)
{
int inAnsi = 0;
currline = mf.lineno + lines;
col = 0;
/* Is currentline visible? */
if (lines < startline || lines > endline)
{
while(mf.dispe < mf.end && *mf.dispe != '\n')
mf.dispe++;
col = t_columns; /* prevent printing trailing '\n' */
}
/* Now, consider what kind of line
* (header, seperator, or normal text)
* is current line.
*/
else if (currline == ah.lines)
{
/* case 1, header seperator line */
outs("\033[36m");
for(col = 0; col < t_columns -2; col+=2)
{
outs("─");
}
outs("\033[m");
while(mf.dispe < mf.end && *mf.dispe != '\n')
mf.dispe++;
}
else if (currline < ah.lines)
{
/* case 2, we're printing headers */
int w = t_columns - 2, i_author = 0;
int flDrawBoard = 0, flDrawAuthor = 0;
const char *ph = disp_heads[currline];
if (currline == 0 && ah.boardlen > 0)
flDrawAuthor = 1;
draw_header:
if(flDrawAuthor)
w = t_columns - 2 - ah.boardlen - 6;
else
w = t_columns - 2;
outs("\033[47;34m "); col++;
/* special case for STR_AUTHOR2 */
if(!flDrawBoard)
{
outs(ph);
col += DISP_HEADS_LEN; // strlen(disp_heads[currline])
} else {
/* display as-is */
while (*mf.dispe != ':' && *mf.dispe != '\n')
if(col++ < t_columns)
outc(*mf.dispe++);
}
while (*mf.dispe != ':' && *mf.dispe != '\n')
mf.dispe ++;
if(*mf.dispe == ':') {
outs(" \033[44;37m"); col++;
mf.dispe ++;
}
while (col < w) { // -2 to match seperator
int flCanDraw = (*mf.dispe != '\n');
if(flDrawAuthor)
flCanDraw = i_author < ah.authorlen;
if(flCanDraw)
{
/* strip ansi in headers */
unsigned char c = *mf.dispe++;
if(inAnsi)
{
if (!strchr(STR_ANSICODE, c))
inAnsi = 0;
} else {
if(c == ANSI_ESC)
inAnsi = 1;
else
outc(c), col++, i_author++;
}
} else {
outc(' '), col++;
}
}
if (flDrawAuthor)
{
flDrawBoard = 1;
flDrawAuthor = 0;
while(*mf.dispe == ' ')
mf.dispe ++;
goto draw_header;
}
outs("\033[m");
// skip to end of line
while(mf.dispe < mf.end && *mf.dispe != '\n')
mf.dispe++;
}
else if(mf.dispe < mf.end)
{
/* case 3, normal text */
long dist = mf.end - mf.dispe;
long flResetColor = 0;
int srlen = -1;
// first check quote
if(dist > 1 &&
(*mf.dispe == ':' || *mf.dispe == '>') &&
*(mf.dispe+1) == ' ')
{
outs("\033[36m");
flResetColor = 1;
} else if (dist > 2 &&
(!strncmp(mf.dispe, "※", 2) ||
!strncmp(mf.dispe, "==>", 3)))
{
outs("\033[32m");
flResetColor = 1;
}
while(mf.dispe < mf.end && *mf.dispe != '\n')
{
if(inAnsi)
{
if (!strchr(STR_ANSICODE, *mf.dispe))
inAnsi = 0;
if(col < t_columns)
outc(*mf.dispe);
} else {
if(*mf.dispe == ANSI_ESC)
inAnsi = 1;
else if(srlen < 0 && sr.search_str[0] && // support search
//tolower(sr.search_str[0]) == tolower(*mf.dispe) &&
mf.end - mf.dispe > sr.len &&
sr.cmpfunc(mf.dispe, sr.search_str, sr.len) == 0)
{
outs("\033[7m");
srlen = sr.len-1;
flResetColor = 1;
}
#ifdef PMORE_USE_PTT_PRINTS
/* special case to resolve dirty Ptt_Prints */
if(inAnsi &&
mf.end - mf.dispe > 2 &&
*(mf.dispe+1) == '*')
{
int i;
char buf[64]; // make sure ptt_prints will not exceed
memset(buf, 0, sizeof(buf));
strncpy(buf, mf.dispe, 3); // ^[[*s
mf.dispe += 2;
Ptt_prints(buf, NO_RELOAD); // result in buf
i = strlen(buf);
if (col + i >= t_columns)
i = t_columns - col;
if(i > 0)
{
buf[i] = 0;
col += i;
outs(buf);
}
inAnsi = 0;
} else
#endif
{
if(col < t_columns)
outc(*mf.dispe);
if(!inAnsi)
{
col++;
if (srlen == 0)
outs("\033[m");
if(srlen >= 0)
srlen --;
}
}
}
mf.dispe ++;
}
if(flResetColor)
outs("\033[m");
}
if(mf.dispe < mf.end)
mf.dispe ++;
if(col < t_columns-1) /* can we do so? */
outc('\n');
else
move(lines+1, 0);
lines ++;
}
mf.oldlineno = mf.lineno;
}
/* --------------------- MAIN PROCEDURE ------------------------- */
static const char * const pmore_help[] = {
"\0閱\讀文章功\能鍵使用說明",
"\01游標移動功\能鍵",
"(↑) 上捲一行",
"(↓)(Enter) 下捲一行",
"(^B)(PgUp)(BackSpace) 上捲一頁",
"(→)(PgDn)(Space) 下捲一頁",
"(0)(g)(Home) 檔案開頭",
"($)(G) (End) 檔案結尾",
"\01其他功\能鍵",
"(/) 搜尋字串",
"(n/N) 重複正/反向搜尋",
// "(TAB) URL連結",
"(Ctrl-T) 存到暫存檔",
"(;/:/f/b) 跳至某行/某頁/下/上篇",
"(a/A) 跳至同一作者下/上篇",
"([-/]+) 主題式閱\讀 上/下",
"(t) 主題式循序閱\讀",
"(q)(←) 結束",
"(h)(H)(?) 輔助說明畫面",
"\01本系統使用 piaip 的新式瀏覽程式",
NULL
};
/*
* piaip's more, a replacement for old more
*/
int pmore(char *fpath, int promptend)
{
int flExit = 0, retval = 0;
int ch = 0;
STATINC(STAT_MORE);
if(!mf_attach(fpath))
return -1;
/* reset and parse article header */
mf_parseHeader();
clear();
while(!flExit)
{
mf_disp();
if(promptend == NA && mf_viewedAll())
break;
move(b_lines, 0);
// clrtoeol(); // this shall be done in mf_disp to speed up.
#ifdef DEBUG
prints("L#%d prmpt=%d Disp:%08X/%08X/%08X, File:%08X/%08X(%d)",
(int)mf.lineno,
promptend,
(unsigned int)mf.disps,
(unsigned int)mf.maxdisps,
(unsigned int)mf.dispe,
(unsigned int)mf.start, (unsigned int)mf.end,
(int)mf.len);
#else
if(mf.len)
{
char *printcolor;
char buf[256]; // orz
int i;
if(mf_viewedAll())
printcolor = "37;44";
else if (mf_viewedNone())
printcolor = "34;46";
else
printcolor = "33;45";
prints("\033[0;%sm", printcolor);
sprintf(buf,
" 瀏覽 第 %1d 頁 \033[30;47m (%02d - %02d行,%3d%%) ",
(int)(mf.lineno / MFNAV_PAGE)+1,
(int)(mf.lineno + 1),
(int)(mf.lineno + MFDISP_PAGE),
(int)((unsigned long)(mf.dispe-mf.start) * 100 / mf.len)
);
outs(buf);
i = strlen(buf);
i -= 8; // ANSI codes in buf
i += 22; // trailing msg columns
i = t_columns - i - 2;
while(i-- > 0)
outc(' ');
if(i == -1) /* enough buffer */
outs( "\033[31;47m(h)\033[30m按鍵說明 "
"\033[31m←[q]\033[30m離開 ");
outs("\033[m");
}
#endif
ch = igetch();
switch (ch) {
/* ------------------ EXITING KEYS ------------------ */
case 'r': // Ptt: put all reply/recommend function here
case 'R':
case 'Y':
case 'y':
flExit = 1, retval = 999;
break;
case 'X':
flExit = 1, retval = 998;
break;
case 'A':
flExit = 1, retval = AUTHOR_PREV;
break;
case 'a':
flExit = 1, retval = AUTHOR_NEXT;
break;
case 'F':
case 'f':
flExit = 1, retval = READ_NEXT;
break;
case 'B':
case 'b':
flExit = 1, retval = READ_PREV;
break;
case KEY_LEFT:
case 'q':
flExit = 1, retval = FULLUPDATE;
break;
/* from Kaede, thread reading */
case ']':
case '+':
flExit = 1, retval = RELATE_NEXT;
break;
case '[':
case '-':
flExit = 1, retval = RELATE_PREV;
break;
case '=':
flExit = 1, retval = RELATE_FIRST;
break;
case 't':
if (mf_viewedAll())
flExit = 1, retval = RELATE_NEXT;
else
mf_forward(MFNAV_PAGE);
break;
/* ------------------ NAVIGATION KEYS ------------------ */
/* Simple Navigation */
case Ctrl('F'):
case KEY_PGDN:
mf_forward(MFNAV_PAGE);
break;
case Ctrl('B'):
case KEY_PGUP:
mf_backward(MFNAV_PAGE);
break;
case '0':
case 'g':
case KEY_HOME:
mf_goTop();
break;
case '$':
case 'G':
case KEY_END:
mf_goBottom();
break;
/* Compound Navigation */
case '\r':
case '\n':
case KEY_DOWN:
if (mf_viewedAll() ||
(promptend == 2 && (ch == '\r' || ch == '\n')))
flExit = 1, retval = READ_NEXT;
else
mf_forward(1);
break;
case ' ':
if (mf_viewedAll())
flExit = 1, retval = READ_NEXT;
else
mf_forward(MFNAV_PAGE);
break;
case KEY_RIGHT:
if(mf_viewedAll())
promptend = 0, flExit = 1, retval = 0;
else
mf_forward(MFNAV_PAGE);
break;
case KEY_UP:
if(mf_viewedNone())
flExit = 1, retval = READ_PREV;
else
mf_backward(1);
break;
case Ctrl('H'):
if(mf_viewedNone())
flExit = 1, retval = READ_PREV;
else
mf_backward(MFNAV_PAGE);
break;
/* ------------------ SEARCH KEYS ------------------ */
case '/':
{
char ans[4] = "n";
sr.search_str[0] = 0;
getdata_buf(b_lines - 1, 0, "[搜尋]關鍵字:", sr.search_str,
40, DOECHO);
if (sr.search_str[0]) {
if (getdata(b_lines - 1, 0, "區分大小寫(Y/N/Q)? [N] ",
ans, sizeof(ans), LCECHO) && *ans == 'y')
sr.cmpfunc = strncmp;
else
sr.cmpfunc = strncasecmp;
if (*ans == 'q')
sr.search_str[0] = 0;
}
sr.len = strlen(sr.search_str);
mf_search(MFSEARCH_FORWARD);
MFDISP_DIRTY();
}
break;
case 'n':
mf_search(MFSEARCH_FORWARD);
break;
case 'N':
mf_search(MFSEARCH_BACKWARD);
break;
/* ------------------ SPECIAL KEYS ------------------ */
case ';':
case ':':
{
char buf[10];
int i = 0;
int pageMode = (ch == ':');
getdata(b_lines-1, 0,
(pageMode ? "Goto Page: " : "Goto Line: "),
buf, 5, DOECHO);
if(buf[0]) {
i = atoi(buf);
if(i-- > 0)
mf_goto(i * (pageMode ? MFNAV_PAGE : 1));
}
MFDISP_DIRTY();
}
break;
case Ctrl('T'):
{
char buf[10];
getdata(b_lines - 2, 0, "把這篇文章收入到暫存檔?[y/N] ",
buf, 4, LCECHO);
if (buf[0] == 'y') {
setuserfile(buf, ask_tmpbuf(b_lines - 1));
Copy(fpath, buf);
}
MFDISP_DIRTY();
}
break;
case 'h':
case 'H':
case '?':
// help
show_help(pmore_help);
MFDISP_DIRTY();
break;
case 'E':
// admin edit any files other than ve help file
if (HAS_PERM(PERM_SYSOP) && strcmp(fpath, "etc/ve.hlp")) {
mf_detach();
vedit(fpath, NA, NULL);
return 0;
}
break;
}
}
mf_detach();
if (retval == 0 && promptend) {
pressanykey();
clear();
} else
outs(reset_color);
return retval;
}
/* vim:sw=4
*/