/*************************************************************************
* TinyFugue - programmable mud client
* Copyright (C) 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2002, 2003, 2004, 2005, 2006-2007 Ken Keys
*
* TinyFugue (aka "tf") is protected under the terms of the GNU
* General Public License. See the file "COPYING" for details.
************************************************************************/
static const char RCSid[] = "$Id: history.c,v 35004.114 2007/01/13 23:12:39 kkeys Exp $";
/****************************************************************
* Fugue history and logging *
* *
* Maintains the circular lists for input and output histories. *
* Handles text queuing and file I/O for logs. *
****************************************************************/
#include <limits.h>
#include "tfconfig.h"
#include "port.h"
#include "tf.h"
#include "util.h"
#include "pattern.h"
#include "search.h" /* CQueue; List in recall_history() */
#include "tfio.h"
#include "history.h"
#include "socket.h" /* xworld() */
#include "world.h"
#include "output.h" /* update_status_field(), etc */
#include "attr.h"
#include "macro.h" /* add_new_macro() */
#include "cmdlist.h"
#include "keyboard.h" /* keybuf */
#include "variable.h" /* set_var_by_*() */
#include "signals.h" /* interrupted() */
const int feature_history = !(NO_HISTORY - 0);
#if !NO_HISTORY
#define GLOBALSIZE 1000 /* global history size */
#define LOCALSIZE 100 /* local history size */
#define INPUTSIZE 100 /* command history buffer size */
typedef struct History { /* circular list of lines, and logfile */
CQueue cq;
TFILE *logfile;
const char *logname;
} History;
static int next_hist_opt(const char **ptr, int *offsetp, History **histp,
void *u);
static void save_to_hist(History *hist, conString *line);
static void save_to_log(History *hist, const conString *str);
static void hold_input(const conString *str);
static void listlog(World *world);
static void stoplog(World *world);
static int do_watch(const char *args, int id, int *wlines, int *wmatch);
static struct History input[1];
static int wnmatch = 4, wnlines = 5, wdmatch = 2, wdlines = 5;
struct History globalhist_buf, localhist_buf;
struct History * const globalhist = &globalhist_buf;
struct History * const localhist = &localhist_buf;
int log_count = 0;
int nohistory = 0; /* supress history (but not log) recording */
int nolog = 0; /* supress log (but not history) recording */
#define histline(hist, i) \
((String*)(hist)->cq.data[nmod(i, (hist)->cq.maxsize)])
static void free_hist_datum(void *datum, const char *file, int line)
{
Stringfree_fl(datum, file, line);
}
struct History *init_history(History *hist, int maxsize)
{
if (!hist) hist = (History*)XMALLOC(sizeof(History));
hist->logfile = NULL;
init_cqueue(&hist->cq, maxsize, free_hist_datum);
return hist;
}
inline void sync_input_hist(void)
{
input->cq.index = input->cq.last;
}
void init_histories(void)
{
init_history(input, INPUTSIZE);
init_history(globalhist, GLOBALSIZE);
init_history(localhist, LOCALSIZE);
save_to_hist(input, blankline);
sync_input_hist();
}
#if USE_DMALLOC
void free_histories(void)
{
free_history(input);
free_history(globalhist);
free_history(localhist);
}
#endif
void free_history(History *hist)
{
free_cqueue(&hist->cq);
if (hist->logfile) {
tfclose(hist->logfile);
--log_count;
update_status_field(NULL, STAT_LOGGING);
}
}
static void save_to_hist(History *hist, conString *line)
{
if (line->time.tv_sec < 0) gettime(&line->time);
if (!hist->cq.data)
hist->cq.maxsize = histsize;
encqueue(&hist->cq, line);
line->links++;
}
static void save_to_log(History *hist, const conString *str)
{
if (wraplog) {
/* ugly, but some people want it */
const char *p = str->data;
int i = 0, first = TRUE, len, remaining = str->len;
do { /* must loop at least once, to handle empty string case */
if (!first && wrapflag)
for (i = wrapspace; i; i--) tfputc(' ', hist->logfile);
len = wraplen(p, remaining, !first);
tfnputs(p, len, hist->logfile);
first = FALSE;
p += len;
remaining -= len;
} while (remaining);
} else {
tfputs(str->data, hist->logfile);
}
tfflush(hist->logfile);
}
void recordline(History *hist, conString *line)
{
if (!(line->attrs & F_NOHISTORY) && !nohistory)
save_to_hist(hist, line);
if (hist->logfile && !nolog && !(line->attrs & F_NOLOG))
save_to_log(hist, line);
}
static void hold_input(const conString *instr)
{
String *str = Stringnew(instr->data, -1, sockecho() ? 0 : F_GAG);
str->links++;
gettime(&str->time);
cqueue_replace(&input->cq, str, input->cq.last);
}
void record_input(const conString *str)
{
int is_duplicate = 0;
sync_input_hist();
if (!str->data) return;
if (input->cq.size > 1) {
const String *prev_line = histline(input, input->cq.last-1);
is_duplicate = (strcmp(str->data, prev_line->data) == 0);
}
if (!is_duplicate) {
hold_input(str);
save_to_hist(input, blankline);
sync_input_hist();
}
if (input->logfile && !nolog) save_to_log(input, str);
}
/* recall_input() parameter combinations:
*
* mode=0: step n times
* mode=1: search n times
* mode=2: go to end with sign(n)
*/
String *recall_input(int n, int mode)
{
int i, stop, dir;
String *str, *pat = NULL;
CQueue *cq = &input->cq;
if (cq->index == cq->last) hold_input(CS(keybuf));
stop = (n < 0) ? cq->first : cq->last;
if (cq->index == stop) return NULL;
dir = (n < 0) ? -1 : 1;
if (mode == 2) {
i = stop;
} else {
i = nmod(cq->index + dir, cq->maxsize);
pat = (mode==1) ? cq->data[cq->last] : NULL;
}
if (n < 0) n = -n;
/* Search until we find a non-gagged match. */
#define match(s, p) (s->len > p->len && strncmp(s->data, p->data, p->len) == 0)
while (1) {
str = (String*)cq->data[i];
if ((!(str->attrs & F_GAG) && (!pat || match(str, pat))))
if (!--n) break;
if (i == stop) return NULL;
i = nmod(i + dir, cq->maxsize);
}
#undef match
cq->index = i;
return str;
}
struct Value *handle_recall_command(String *args, int offset)
{
return newint(do_recall(args, offset));
}
int do_recall(String *args, int offset)
{
int hist_start, n0, n1, i, j, want, numbers;
int count = 0, mflag = matching, quiet = 0, truth = !0, jump = 256;
long ival;
int before = 0, after = 0, out_of_range = 0;
int lastprinted, incontext;
Value val[1];
struct timeval tv0, tv1, *tvp0, *tvp1;
const char *ptr;
AUTO_BUFFER(recall_time_format);
attr_t attrs = 0, tmpattrs;
char opt;
Pattern pat;
World *world = xworld();
History *hist = NULL;
String *line;
static List stack[1] = {{ NULL, NULL }};
String *buffer = NULL;
STATIC_STRING(startmsg, "================ Recall start ================",0);
STATIC_STRING(endmsg, "================= Recall end =================",0);
STATIC_STRING(divider, "--", 0);
#if DEVELOPMENT
int locality;
String *nextline = NULL;
#endif
init_pattern_str(&pat, NULL);
startopt(CS(args), "ligw:a:f:t:m:vqA#B#C#");
while ((opt = next_hist_opt(&ptr, &offset, &hist, &ival))) {
switch (opt) {
case 'a': case 'f':
if (!parse_attrs(ptr, &tmpattrs, 0))
goto do_recall_exit;
attrs |= tmpattrs;
break;
case 't':
Stringcpy(recall_time_format, ptr);
break;
case 'm':
if ((mflag = enum2int(ptr, 0, enum_match, "-m")) < 0)
goto do_recall_exit;
break;
case 'v':
truth = 0;
break;
case 'q':
quiet = 1;
break;
case 'A':
after = ival;
break;
case 'B':
before = ival;
break;
case 'C':
before = after = ival;
break;
default: goto do_recall_exit;
}
}
if (!hist) hist = world ? world->history : globalhist;
ptr = args->data + offset;
#if DEVELOPMENT
if ((locality = (ptr && *ptr == '?'))) ptr++;
#endif
if ((numbers = (ptr && *ptr == '#'))) ptr++;
while (is_space(*ptr)) ptr++;
tvp0 = tvp1 = NULL;
n0 = 0;
n1 = hist->cq.total - 1;
want = hist->cq.size;
if (!ptr || !*ptr) {
eprintf("missing arguments");
goto do_recall_exit;
} else if (*ptr == '-') { /* -y */
++ptr;
if (!parsenumber(ptr, &ptr, TYPE_DTIME | TYPE_INT, val)) {
eprintf("syntax error in recall range");
goto do_recall_exit;
}
if (val->type & TYPE_DTIME) {
tvp1 = &tv1;
tv1 = val->u.tval;
if (val->type & TYPE_HMS)
abstime(&tv1);
} else /* if (val->type & TYPE_INT) */ {
n0 = n1 = hist->cq.total - val->u.ival;
}
} else if (*ptr == '/') { /* /x */
++ptr;
want = strtoint(ptr, &ptr);
} else if (is_digit(*ptr)) { /* x... */
if (!parsenumber(ptr, &ptr, TYPE_DTIME | TYPE_INT, val)) {
eprintf("syntax error in recall range");
goto do_recall_exit;
}
if (val->type & TYPE_DTIME) {
tvp0 = &tv0;
tv0 = val->u.tval;
} else /* if (val->type & TYPE_INT) */ {
n0 = val->u.ival;
}
if (*ptr != '-') { /* x */
if (val->type & TYPE_DTIME) {
struct timeval now;
gettime(&now);
tvsub(&tv0, &now, &tv0);
} else {
n0 = hist->cq.total - n0;
}
} else if (is_digit(*++ptr)) { /* x-y */
if (val->type & TYPE_INT) n0 = n0 - 1;
else if (val->type & TYPE_HMS) abstime(&tv0);
if (!parsenumber(ptr, &ptr, TYPE_DTIME | TYPE_INT, val)) {
eprintf("syntax error in recall range");
goto do_recall_exit;
}
if (val->type & TYPE_DTIME) {
tvp1 = &tv1;
tv1 = val->u.tval;
if (val->type & TYPE_HMS)
abstime(&tv1);
} else /* if (type & TYPE_INT) */ {
n1 = val->u.ival - 1;
}
} else { /* x- */
if (val->type & TYPE_INT) n0 = n0 - 1;
else if (val->type & TYPE_HMS) abstime(&tv0);
}
}
if (*ptr && !is_space(*ptr)) {
eprintf("extra characters after recall range: %s", ptr);
goto do_recall_exit;
}
while (is_space(*ptr)) ++ptr;
if (*ptr && !init_pattern(&pat, ptr, mflag))
goto do_recall_exit;
if (hist->cq.size == 0)
goto do_recall_exit; /* (after parsing, before searching) */
if (!quiet && tfout == tfscreen) {
nohistory++; /* don't save this output in history */
oputline(startmsg);
oflush(); /* in case this takes a while */
}
hist_start = hist->cq.total - hist->cq.size;
if (n0 < hist_start) n0 = hist_start;
if (n1 >= hist->cq.total) n1 = hist->cq.total - 1;
if (n0 <= n1 && (!tvp0 || !tvp1 || tvcmp(tvp0, tvp1) <= 0)) {
attrs = ~attrs;
lastprinted = n1 + 1;
incontext = 0;
if (hist == input) hold_input(CS(keybuf));
for (i = n1; i >= hist_start; i--) {
if (i < n0 || want <= 0) {
if (incontext) out_of_range = 1;
else break;
}
line = histline(hist, i);
if (interrupted()) {
(buffer = Stringnew(NULL, 32, 0))->links++;
tftime(buffer, blankline, &line->time);
eprintf("history scan interrupted at #%d, %S", i, buffer);
Stringfree(buffer);
buffer = NULL;
break;
}
if (tvp1 && tvcmp(&line->time, tvp1) > 0) {
/* globalhist isn't chronological, but we can optimize others */
if (hist == globalhist || !jump)
continue;
/* take large steps backward searching for something < tv1 */
for (i -= jump; i >= n0; i -= jump) {
line = histline(hist, i);
if (tvcmp(&line->time, tvp1) <= 0)
break;
}
i += jump;
jump = 0; /* don't do this again */
continue;
}
/* globalhist isn't chronological, but we can optimize others */
if (tvp0 && tvcmp(&line->time, tvp0) < 0) {
if (incontext) {
out_of_range = 1;
} else {
if (hist == globalhist) continue;
break;
}
}
if (gag && (line->attrs & F_GAG & attrs)) continue;
if (!out_of_range && !!patmatch(&pat, CS(line), NULL) == truth) {
want--;
j = i + after;
if (j >= lastprinted - 1) {
j = lastprinted - 1;
} else if ((before || after) && stack->head) {
inlist((void*)divider, stack, NULL);
}
incontext = before;
} else if (incontext) {
incontext--;
j = i;
} else {
continue;
}
for ( ; j >= i; j--) {
line = histline(hist, j);
if (numbers) {
if (!buffer)
buffer= Stringnew(NULL, line->len + 8, 0);
Sappendf(buffer, "%d: ", j+1);
}
if (recall_time_format->data) {
if (!buffer)
buffer= Stringnew(NULL, line->len + 20, 0);
if (!*recall_time_format->data) {
Stringadd(buffer, '[');
tftime(buffer, time_format, &line->time);
Stringadd(buffer, ']');
} else {
tftime(buffer, CS(recall_time_format), &line->time);
}
Stringadd(buffer, ' ');
}
#if DEVELOPMENT
if (locality) {
char sign = '+';
long diff = (char*)nextline - (char*)line;
if (nextline > line) diff -= sizeof(String) + line->size;
else diff += nextline ? (sizeof(String)+nextline->size) : 0;
if (diff < 0) { sign = '-'; diff = -diff; }
if (!buffer)
buffer = Stringnew(NULL, 40, 0);
Sprintf(buffer, "%d (%010p): %c%lx", j, line, sign, diff);
nextline = line;
line = buffer;
buffer = NULL;
} else
#endif
/* share line if possible: copy only if different */
if (buffer) {
line = SStringcat(buffer, CS(line));
line->attrs &= attrs & F_ATTR;
buffer = NULL;
} else if (line->attrs & ~attrs & F_ATTR) {
line = Stringnew(line->data, line->len, line->attrs & attrs);
}
inlist((void*)line, stack, NULL);
lastprinted = j;
count++;
}
}
}
while (stack->head)
oputline(CS((String *)unlist(stack->head, stack)));
if (!quiet && tfout == tfscreen) {
oputline(endmsg);
nohistory--;
}
do_recall_exit:
free_pattern(&pat);
Stringfree(recall_time_format);
return count;
}
static int do_watch(const char *args, int id, int *wlines, int *wmatch)
{
int out_of, match;
if (!*args) {
oprintf("%% %s %sabled.", special_var[id].val.name,
getintvar(id) ? "en" : "dis");
return 1;
} else if (cstrcmp(args, "off") == 0) {
set_var_by_id(id, 0);
oprintf("%% %s disabled.", special_var[id].val.name);
return 1;
} else if (cstrcmp(args, "on") == 0) {
/* do nothing */
} else {
if ((match = numarg(&args)) < 0) return 0;
if ((out_of = numarg(&args)) < 0) return 0;
*wmatch = match;
*wlines = out_of;
}
set_var_by_id(id, 1);
oprintf("%% %s enabled, searching for %d out of %d lines",
special_var[id].val.name, *wmatch, *wlines);
return 1;
}
struct Value *handle_watchdog_command(String *args, int offset)
{
return newint(do_watch(args->data + offset, VAR_watchdog,
&wdlines, &wdmatch));
}
struct Value *handle_watchname_command(String *args, int offset)
{
return newint(do_watch(args->data + offset, VAR_watchname,
&wnlines, &wnmatch));
}
int is_watchname(History *hist, String *line)
{
int nmatches = 1, i;
const char *old, *end;
STATIC_BUFFER(buf);
if (!watchname || !gag || line->attrs & F_GAG) return 0;
if (is_space(*line->data)) return 0;
for (end = line->data; *end && !is_space(*end); ++end);
for (i = ((wnlines >= hist->cq.size) ? hist->cq.size - 1 : wnlines);
i > 0; i--)
{
old = histline(hist, hist->cq.last - i)->data;
if (strncmp(old, line->data, end - line->data) != 0) continue;
if (++nmatches == wnmatch) break;
}
if (nmatches < wnmatch) return 0;
Sprintf(buf, "{%.*s}*", end - line->data, line->data);
oprintf("%% Watchname: gagging \"%S\"", buf);
return add_new_macro(buf->data, "", NULL, NULL, "", gpri, 100, F_GAG,
0, MATCH_GLOB);
}
int is_watchdog(History *hist, String *line)
{
int nmatches = 0, i;
const char *old;
if (!watchdog || !gag || line->attrs & F_GAG) return 0;
for (i = ((wdlines >= hist->cq.size) ? hist->cq.size - 1 : wdlines);
i > 0; i--)
{
old = histline(hist, hist->cq.last - i)->data;
if (cstrcmp(old, line->data) == 0 && (++nmatches == wdmatch)) return 1;
}
return 0;
}
String *history_sub(String *line)
{
STATIC_BUFFER(pattern);
STATIC_BUFFER(buffer);
char *replacement, *loc = NULL;
String *src = NULL;
int i;
pattern->data = line->data + 1;
if (!(replacement = strchr(pattern->data, '^'))) return NULL;
*replacement = '\0';
pattern->len = replacement - pattern->data;
for (i = 1; i < input->cq.size; i++) {
src = histline(input, input->cq.last - i);
loc = strstr(src->data, pattern->data);
if (loc) break;
}
*(replacement++) = '^';
if (!loc) return NULL;
Stringtrunc(buffer, 0);
SStringncat(buffer, CS(src), loc - src->data);
SStringocat(buffer, CS(line), replacement - line->data);
SStringocat(buffer, CS(src), loc - src->data + pattern->len);
return buffer;
}
static void stoplog(World *world)
{
if (world->history->logfile) tfclose(world->history->logfile);
world->history->logfile = NULL;
}
static void listlog(World *world)
{
if (world->history->logfile)
oprintf("%% Logging world %s output to %s",
world->name, world->history->logfile->name);
}
/* Parse "ligw:" history options. If another option is found, it is returned,
* so the caller can parse it. If end of options is reached, 0 is returned.
* '?' is returned for error. *histp will contain a pointer to the history
* selected by the "ligw:" options. *histp will be unchanged if no relavant
* options are given; the caller should assign a default before calling.
*/
static int next_hist_opt(const char **ptr, int *offsetp, History **histp,
void *u)
{
World *world;
char c;
const char *p;
int selected = 0;
if (!ptr) ptr = &p;
while ((c = nextopt(ptr, u, NULL, offsetp))) {
switch (c) {
case 'l':
if (selected++) goto multiple_error;
*histp = localhist;
break;
case 'i':
if (selected++) goto multiple_error;
*histp = input;
break;
case 'g':
if (selected++) goto multiple_error;
*histp = globalhist;
break;
case 'w':
if (selected++) goto multiple_error;
if (!(world = named_or_current_world(*ptr)))
return '?';
*histp = world->history;
break;
default:
return c; /* let caller handle it */
}
}
return c;
multiple_error:
eprintf("only one of the -ligw options may be used.");
return '?';
}
struct Value *handle_recordline_command(String *args, int offset)
{
History *history = globalhist;
char opt;
struct timeval tv, *tvp = NULL;
conString *line = NULL;
int attrflag = 0;
attr_t attrs = 0, tmpattrs;
const char *ptr;
startopt(CS(args), "lgiw:t@a:p");
while ((opt = next_hist_opt(&ptr, &offset, &history, &tv))) {
switch (opt) {
case 't': tvp = &tv; break;
case 'p': attrflag = 1; break;
case 'a':
if (!parse_attrs(ptr, &tmpattrs, 0))
return shareval(val_zero);
attrs |= tmpattrs;
break;
default:
return shareval(val_zero);
}
}
if (attrflag) {
line = CS(decode_attr(CS(args), attrs, offset));
/* if encoding was invalid, just copy without decoding */
}
if (!line) {
line = CS(Stringodup(CS(args), offset));
line->attrs = adj_attr(line->attrs, attrs);
}
line->links++;
if (tvp)
line->time = tv;
nolog++;
if (history == input)
record_input(line);
else
recordline(history, line);
nolog--;
conStringfree(line);
return shareval(val_one);
}
struct Value *handle_log_command(String *args, int offset)
{
History *history;
History dummy;
TFILE *logfile = NULL;
const char *name;
if (restriction >= RESTRICT_FILE) {
eprintf("restricted");
return shareval(val_zero);
}
history = &dummy;
startopt(CS(args), "lgiw:");
if (next_hist_opt(NULL, &offset, &history, NULL))
return shareval(val_zero);
if (history == &dummy && !(args->len - offset)) {
/* "/log" */
if (log_count) {
if (input->logfile)
oprintf("%% Logging input to %s", input->logfile->name);
if (localhist->logfile)
oprintf("%% Logging local output to %s",
localhist->logfile->name);
if (globalhist->logfile)
oprintf("%% Logging global output to %s",
globalhist->logfile->name);
mapworld(listlog);
} else {
oputs("% Logging disabled.");
}
return shareval(val_one);
} else if (cstrcmp(args->data + offset, "OFF") == 0) {
/* "/log [options] OFF" */
if (history == &dummy) {
if (log_count) {
if (input->logfile) tfclose(input->logfile);
input->logfile = NULL;
if (localhist->logfile) tfclose(localhist->logfile);
localhist->logfile = NULL;
if (globalhist->logfile) tfclose(globalhist->logfile);
globalhist->logfile = NULL;
mapworld(stoplog);
log_count = 0;
update_status_field(NULL, STAT_LOGGING);
}
} else if (history->logfile) {
tfclose(history->logfile);
history->logfile = NULL;
--log_count;
update_status_field(NULL, STAT_LOGGING);
}
return shareval(val_one);
} else if (cstrcmp(args->data+offset, "ON") == 0 || !(args->len - offset)) {
/* "/log [options] [ON]" */
if (!(name = tfname(NULL, "LOGFILE")))
return shareval(val_zero);
logfile = tfopen(name, "a");
} else {
/* "/log [options] <filename>" */
name = expand_filename(args->data + offset);
logfile = tfopen(name, "a");
}
if (!logfile) {
operror(name);
return shareval(val_zero);
}
if (history == &dummy) history = globalhist;
if (history->logfile) {
tfclose(history->logfile);
history->logfile = NULL;
log_count--;
}
do_hook(H_LOG, "%% Logging to file %s", "%s", logfile->name);
history->logfile = logfile;
log_count++;
update_status_field(NULL, STAT_LOGGING);
return shareval(val_one);
}
#define histname(hist) \
(hist == globalhist ? "global" : (hist == localhist ? "local" : \
(hist == input ? "input" : "world")))
struct Value *handle_histsize_command(String *args, int offset)
{
History *hist;
int maxsize = 0, size;
const char *ptr;
hist = globalhist;
startopt(CS(args), "lgiw:");
if (next_hist_opt(NULL, &offset, &hist, NULL))
return shareval(val_zero);
if (args->len - offset) {
ptr = args->data + offset;
if ((maxsize = numarg(&ptr)) <= 0) return shareval(val_zero);
if (!resize_cqueue(&hist->cq, maxsize)) {
eprintf("not enough memory for %d lines.", maxsize);
maxsize = 0;
}
/* XXX resize corresponding screen */
}
size = hist->cq.maxsize ? hist->cq.maxsize : histsize;
oprintf("%% %s history capacity %s %ld lines.",
histname(hist), maxsize ? "changed to" : "is",
size);
hist->cq.index = hist->cq.last;
return newint(size);
}
long hist_getsize(const struct History *hist)
{
return (hist && hist->cq.maxsize) ? hist->cq.maxsize : histsize;
}
#endif /* NO_HISTORY */
syntax highlighted by Code2HTML, v. 0.9.1