/*
 * src/client/main.c, part of Complete Goban (game program)
 * Copyright (C) 1995-1996 William Shubert
 * See "configure.h.in" for more copyright information.
 */

#include <wms.h>
#include <clp.h>
#include <str.h>
#include <but.h>
#include <ctext.h>
#include <abut_msg.h>
#include <term.h>
#include <tbin.h>
#include <textin.h>
#include <radio.h>
#include "cgoban.h"
#include "msg.h"
#include "help.h"
#ifdef  _CLIENT_MAIN_H_
        LEVELIZATION ERROR
#endif
#include "client_main.h"


/**********************************************************************
 * Forward Declarations
 **********************************************************************/
static ButOut  unmap(ButWin *win);
static ButOut  map(ButWin *win);
static ButOut  resize(ButWin *win);
static ButOut  destroy(ButWin *win);
static void  dataIn(void *packet, const char *buf, int bufLen);
static ButOut  userInput(But *but, const char *input);
static ButOut  quitPressed(But *but);
static ButOut  gamesPressed(But *but);
static ButOut  playersPressed(But *but);
static ButOut  iResize(ButWin *win);
static ButOut  newState(But *but, int value);
static ButOut  msgBoxKilled(But *okBut);
static void  client2client(CliMain *main, const char *buf);
static ButOut  prevHistory(But *but,
			   KeySym keysym, uint keyModifiers, void *context);
static ButOut  nextHistory(But *but,
			   KeySym keysym, uint keyModifiers, void *context);

/**********************************************************************
 * Functions
 **********************************************************************/
CliMain  *cliMain_create(CliData *data,
			 void (*destroyCallback)(CliMain *main, void *packet),
			 void *packet)  {
  Str  winTitle;
  CliMain  *main;
  int  winW, winH;
  Cgoban *cg;
  int  i;
  bool  err;

  assert(MAGIC(data));
  cg = data->cg;
  main = wms_malloc(sizeof(CliMain));
  MAGIC_SET(main);
  cliData_incRef(data);
  main->data = data;
  winW = (clp_getDouble(cg->clp, "client.main.w") * cg->fontH + 0.5);
  winH = (clp_getDouble(cg->clp, "client.main.h") * cg->fontH + 0.5);
  str_init(&winTitle);
  str_print(&winTitle, "%s Client", data->serverName);
  main->win = butWin_iCreate(main, cg->env, str_chars(&winTitle),
			     winW, winH, &main->iWin, FALSE, 48, 48,
			     unmap, map, resize, iResize, destroy);
  i = clpEntry_iGetInt(clp_lookup(cg->clp, "client.main.x"), &err);
  if (!err)
    butWin_setX(main->win, i);
  i = clpEntry_iGetInt(clp_lookup(cg->clp, "client.main.y"), &err);
  if (!err)
    butWin_setY(main->win, i);
  butWin_setMinW(main->win, cg->fontH * 10);
  butWin_setMinH(main->win, cg->fontH * 10);
  butWin_setMaxW(main->win, 0);
  butWin_setMaxH(main->win, 0);
  butWin_activate(main->win);
  str_deinit(&winTitle);
  main->bg = butBoxFilled_create(main->win, 0, BUT_DRAWABLE);
  butBoxFilled_setPixmaps(main->bg, cg->bgLitPixmap, cg->bgShadPixmap,
			  cg->bgPixmap);
  main->quit = butCt_create(quitPressed, main, main->win, 1,
			    BUT_DRAWABLE|BUT_PRESSABLE, msg_quit);
  main->games = butCt_create(gamesPressed, main, main->win, 1,
			     BUT_DRAWABLE, msg_games);
  main->players = butCt_create(playersPressed, main, main->win, 1,
			       BUT_DRAWABLE, msg_players);
  main->help = butCt_create(cgoban_createHelpWindow,
			    &help_cliMain, main->win, 1,
			    BUT_DRAWABLE|BUT_PRESSABLE, msg_help);
  main->script = abutTerm_create(cg->abut, main->win, 1, FALSE);
  main->message = NULL;
  butTbin_setMaxLines(main->script->tbin, 250);
  if (data->conn.directConn)
    main->tin = butTextin_create(userInput, main, main->win, 1,
				 BUT_DRAWABLE, "", 200);
  else
    main->tin = butTextin_create(userInput, main, main->win, 1,
				 BUT_DRAWABLE|BUT_PRESSABLE|BUT_KEYED,
				 "", 200);

  main->userState = butRadio_create(newState, main, main->win, 1,
				    BUT_DRAWABLE,
				    cliPlayer_unknown - cliPlayer_closed, 3);
  main->looking = butText_create(main->win, 2, BUT_DRAWABLE | BUT_PRESSTHRU,
				 "!", butText_center);
  main->open = butText_create(main->win, 2, BUT_DRAWABLE | BUT_PRESSTHRU,
			      "O", butText_center);
  main->closed = butText_create(main->win, 2, BUT_DRAWABLE | BUT_PRESSTHRU,
				"X", butText_center);
  cliPlayerList_init(&main->playerList, data);
  cliGameList_init(&main->gameList, data, &main->playerList);
  main->state = cliMain_idle;
  main->promptVisible = FALSE;
  str_init(&main->outStr);
  main->iBg = butBoxFilled_create(main->iWin, 0, BUT_DRAWABLE);
  butBoxFilled_setPixmaps(main->iBg, cg->bgLitPixmap, cg->bgShadPixmap,
			  cg->bgPixmap);
  main->iPic = grid_create(&cg->cgbuts, NULL, NULL, main->iWin, 1,
			   BUT_DRAWABLE, 0);
  grid_setStone(main->iPic, goStone_white, FALSE);
  grid_setLineGroup(main->iPic, gridLines_none);
  if (data->server == cliServer_nngs)
    grid_setVersion(main->iPic, CGBUTS_WORLDWEST(3));
  else
    grid_setVersion(main->iPic, CGBUTS_WORLDEAST(4));
  cliLook_init(&main->look, cg, data->server);
  main->historyLen = 0;
  main->historyBeingFilled = 0;
  main->historyBeingViewed = 0;
  for (i = 0;  i < MAIN_HISTORYMAXLEN;  ++i) {
    str_init(&main->history[i]);
  }
  butTextin_setSpecialKey(main->tin, XK_p,
			  ControlMask, ShiftMask|ControlMask,
			  prevHistory, main);
  butTextin_setSpecialKey(main->tin, XK_Up, 0, ShiftMask|ControlMask,
			  prevHistory, main);
  butTextin_setSpecialKey(main->tin, XK_n,
			  ControlMask, ShiftMask|ControlMask,
			  nextHistory, main);
  butTextin_setSpecialKey(main->tin, XK_Down, 0, ShiftMask|ControlMask,
			  nextHistory, main);
  main->destroyCallback = destroyCallback;
  main->packet = packet;
  return(main);
}


void  cliMain_activate(CliMain *main)  {
  assert(MAGIC(main));
  main->data->state = cliData_main;
  main->data->conn.newData = dataIn;
  main->data->conn.packet = main;
  main->data->conn.loginMode = FALSE;
  but_setFlags(main->games, BUT_PRESSABLE);
  but_setFlags(main->players, BUT_PRESSABLE);
  but_setFlags(main->tin, BUT_PRESSABLE|BUT_KEYED);
  but_setFlags(main->games, BUT_PRESSABLE);
  but_setFlags(main->userState, BUT_PRESSABLE);
  cliConn_prompt(&main->data->conn);
  cliConn_send(&main->data->conn,
	       "toggle quiet 0\ntoggle verbose 0\ngames\nwho\n");
  if (main->data->server == cliServer_nngs)
    cliConn_send(&main->data->conn, "client 5\n");
}


void  cliMain_destroy(CliMain *main)  {
  Cgoban  *cg;

  assert(MAGIC(main));
  if (main->destroyCallback)
    main->destroyCallback(main, main->packet);
  cliMain_clearMessage(main);
  if (main->win)  {
    cg = main->data->cg;
    clp_setDouble(cg->clp, "client.main.w",
		  (double)butWin_w(main->win) / (double)cg->fontH);
    clp_setDouble(cg->clp, "client.main.h",
		  (double)butWin_h(main->win) / (double)cg->fontH);
    clp_setInt(cg->clp, "client.main.x", butWin_x(main->win));
    clp_setInt(cg->clp, "client.main.y", butWin_y(main->win));
    butWin_setDestroy(main->win, NULL);
    butWin_destroy(main->win);
  }
  cliGameList_deinit(&main->gameList);
  cliPlayerList_deinit(&main->playerList);
  cliLook_deinit(&main->look);
  str_deinit(&main->outStr);
  cliData_closeConns(main->data);
  cliData_decRef(main->data);
  MAGIC_UNSET(main);
  wms_free(main);
}


static ButOut  unmap(ButWin *win)  {
  return(0);
}


static ButOut  map(ButWin *win)  {
  return(0);
}


static ButOut  resize(ButWin *win)  {
  CliMain  *main = butWin_packet(win);
  int  w = butWin_w(win), h = butWin_h(win);
  int  bw = butEnv_stdBw(butWin_env(win));
  int  fontH = main->data->cg->fontH;
  int  midY;

  assert(MAGIC(main));
  but_resize(main->bg, 0, 0, w, h);
  midY = (w - (bw * 2 + fontH * 6)) / 2;
  but_resize(main->games, bw * 2, bw * 2, midY - bw * 2, fontH * 2);
  but_resize(main->userState, midY + bw, bw * 2, fontH * 6, fontH * 2);
  but_resize(main->closed, midY + bw, bw * 2, fontH * 2, fontH * 2);
  but_resize(main->open, midY + bw + fontH * 2, bw * 2, fontH * 2, fontH * 2);
  but_resize(main->looking, midY + bw + fontH * 4, bw * 2,
	     fontH * 2, fontH * 2);
  but_resize(main->players, midY + bw * 2 + fontH * 6, bw * 2,
	     w - (midY + bw * 4 + fontH * 6), fontH * 2);
  abutTerm_resize(main->script, bw * 2, bw * 3 + fontH * 2,
		  w - bw * 4, h - fontH * 6 - bw * 6);
  but_resize(main->tin, bw * 2, h - fontH * 4 - bw * 3, w - bw * 4, fontH * 2);
  midY = (w - bw) / 2;
  but_resize(main->help, bw * 2, h - fontH * 2 - bw * 2,
	     midY - bw * 2, fontH * 2);
  but_resize(main->quit, midY + bw, h - fontH * 2 - bw * 2,
	     w - (midY + bw * 3), fontH * 2);
  return(0);
}


static ButOut  destroy(ButWin *win)  {
  CliMain  *main = win->packet;
  Cgoban  *cg;

  assert(MAGIC(main));
  cg = main->data->cg;
  clp_setDouble(cg->clp, "client.main.w",
		(double)butWin_w(win) / (double)cg->fontH);
  clp_setDouble(cg->clp, "client.main.h",
		(double)butWin_h(win) / (double)cg->fontH);
  clp_setInt(cg->clp, "client.main.x", butWin_x(win));
  clp_setInt(cg->clp, "client.main.y", butWin_y(win));
  main->win = NULL;
  cliMain_destroy(main);
  return(0);
}


static void  dataIn(void *packet, const char *buf, int bufLen)  {
  CliMain  *main = packet;
  int  dataType, args, userNum;
  CliGameState  oldState;
  const char  *temp;

  assert(MAGIC(main));
  if (buf == NULL)  {
    str_print(&main->outStr, msg_cliHangup,
	      main->data->serverName, strerror(bufLen));
    abutMsg_winCreate(main->data->cg->abut, "Cgoban Error",
		      str_chars(&main->outStr));
    cliMain_destroy(main);
  } else  {
    oldState = main->state;
    str_copyCharsLen(&main->outStr, buf, bufLen);
    str_catChar(&main->outStr, '\n');
    if (main->state == cliMain_inFile)  {
      if (!strcmp(buf, main->fileEnd))
	main->state = cliMain_idle;
      else
	cliMain_log(main, str_chars(&main->outStr));
    } else if (bufLen)  {
      args = sscanf(buf, "%d", &dataType);
      if (args != 1)  {
	dataType = 0;
	args = 1;
#if  DEBUG
	printf("Did not pass: \"%s\"\n", buf);
#endif
      } else  {
	while (isdigit(*buf))  {
	  ++buf;
	  --bufLen;
	}
	++buf;
	--bufLen;
	str_copyCharsLen(&main->outStr, buf, bufLen);
	str_catChar(&main->outStr, '\n');
      }
      assert(args == 1);
      /*
       * Set promptReady to FALSE.  If we got a 1, we'll immediately set it to
       *   TRUE so no harm done.
       */
      main->data->conn.promptReady = FALSE;
      switch(dataType)  {
      case 1:  /* Prompt. */
	main->state = cliMain_idle;
	if (!main->promptVisible)  {
	  butTbin_insert(main->script->tbin, "#> ");
	  main->promptVisible = TRUE;
	}
	cliConn_prompt(&main->data->conn);
	break;
      case 2:  /* Beep. */
	XBell(butEnv_dpy(main->data->cg->env), 0);
	break;
      case 5:  /* Error. */
	XBell(butEnv_dpy(main->data->cg->env), 0);
	cliMain_log(main, str_chars(&main->outStr));
	break;
      case 7:  /* Game info. */
	cliGameList_gotGameInfo(&main->gameList, buf);
	break;
      case 8:  /* Help. */
	assert(!strcmp(buf, "File"));
	main->state = cliMain_inFile;
	main->fileEnd = "8 File";
	break;
      case 9:  /* Misc. */
	/*
	 * 9 Use <match wms W 19 21 11> or <decline wms> to respond.
	 * 9 Requesting match in 21 min with wwms as White.
	 * 9 wwms declines your request for a match.
	 * 9 You decline the match offer from wms.
	 */
	if (!strcmp(buf, "File"))  {
	  main->state = cliMain_inFile;
	  main->fileEnd = "9 File";
	} else if (!strncmp(buf, "Removing game ", 14))
	  cliGameList_notObserving(&main->gameList, buf);
	else if (!strncmp(buf, "Removing @ ", 11))
	  cliGameList_deadStone(&main->gameList, buf);
	else if (!strncmp(buf, "You can check ", 14))
	  cliGameList_selectDead(&main->gameList, buf);
	else if (!strncmp(buf, "Use <match", 10))  {
	  char  oppName[21];
	  const char  *oppRank;
	  int  rankDiff;

	  args = sscanf(buf, "Use <match %20s", oppName);
	  oppRank = cliPlayerList_getRank(&main->playerList, oppName);
	  if (oppRank == NULL)
	    rankDiff = 1;
	  else
	    rankDiff = strcmp(oppRank,
			      cliPlayerList_getRank(&main->playerList,
						    str_chars(&main->data->
							      userName)));
	  cliMatch_matchCommand(main->data, buf, &main->playerList.match,
				rankDiff);
	} else if (((bufLen > 34) &&
		  !strcmp(buf + bufLen - 34,
			  "declines your request for a match.")) ||
		 ((bufLen > 26) &&
		  !strcmp(buf + bufLen - 26,
			  "withdraws the match offer.")))
	  cliMatch_declineCommand(main->playerList.match, buf);
	else if (strncmp(buf, "Adding game to", 14) &&
		 strncmp(buf, "{Game ", 6) &&
		 strncmp(buf, "Requesting ", 11) &&
		 strncmp(buf, "You decline", 11) &&
		 strncmp(buf, "Match [", 7) &&
		 strncmp(buf, "Updating ", 9) &&
		 strncmp(buf, "Declining offer", 15) &&
		 ((bufLen < 26) || strcmp(buf + bufLen - 26,
					  "updates the match request.")))  {
	  cliMain_log(main, str_chars(&main->outStr));
	}
	break;
      case 11:  /* Kibitzes. */
	cliGameList_kibitz(&main->gameList, buf);
	break;
      case 15:  /* Game moves. */
	cliGameList_gotMove(&main->gameList, buf);
	/*
	 * On NNGS, if you start a game when your state is closed, you
	 *   become open.  Update the main window's radio button here.
	 */
	if ((main->gameList.playGame >= 0) &&
	    (cliPlayer_closed + butRadio_get(main->userState) ==
	     cliPlayer_closed))  {
	  butRadio_set(main->userState, cliPlayer_open - cliPlayer_closed,
		       FALSE);
	}
	break;
      case 19:  /* Say. */
	cliGameList_say(&main->gameList, buf);
	break;
      case 21:  /* Shouts & game start/stop info & user log in/out. */
	if (!strncmp(buf, "{Game ", 6))  {
	  if (strchr(buf, '@') != NULL)  {
	    /* A continued game. */
	    cliGameList_gameStarts(&main->gameList, buf);
	  } else  {
	    cliGameList_gameGone(&main->gameList, buf, bufLen);
	  }
	} else if (!strncmp(buf, "{Match ", 7))  {
	  /* A new game. */
	  cliGameList_gameStarts(&main->gameList, buf);
	} else if (!strcmp(buf + bufLen - 17, "has disconnected}"))  {
	  cliPlayerList_disconnected(&main->playerList, buf);
	} else if (!strcmp(buf + bufLen - 15, "has connected.}"))  {
	  cliPlayerList_connected(&main->playerList, buf);
	} else
	  cliMain_log(main, str_chars(&main->outStr));
	break;
      case 22:  /* A board. */
	cliLook_gotData(&main->look, str_chars(&main->outStr));
	break;
      case 24:  /* Tells. */
	for (temp = buf;  *temp != ':';  ++temp);
	temp += 2;
	if (!strncmp(temp, "CLIENT: ", 8))  {
	  client2client(main, temp);
	} else if (!cliGame_tellIsSay(&main->gameList, &main->outStr))
	  cliMain_log(main, str_chars(&main->outStr));
	break;
      case 25:  /* Results list. */
	assert(!strcmp(buf, "File"));
	main->state = cliMain_inFile;
	main->fileEnd = "25 File";
	break;
      case 27:  /* Who output. */
	main->state = cliMain_gotWho;
	cliPlayerList_whoOutput(&main->playerList, buf);
	break;
      case 28:  /* Undo. */
	cliGameList_gotUndo(&main->gameList, buf);
	break;

      case 20:  /* Game results. */
      case 23:  /* A list of stored games. */
      case 32:  /* Channel tell. */
      case 39:  /* Version number. */
      case 40:  /* Confirming who you spoke to, or something like that. */
      case 42:  /* IGS "user" output. */
      case 500:  /* NNGS emotes. */
      case 501:  /* NNGS tell you who the emote went to. */
      default:
	cliMain_log(main, str_chars(&main->outStr));
	break;
      }
      if ((oldState == cliMain_gotWho) && (main->state != cliMain_gotWho))  {
	cliPlayerList_sort(&main->playerList);
	userNum = cliPlayerList_lookupPlayer(&main->playerList,
					     str_chars(&main->data->userName));
	if ((userNum >= 0) &&
	    (butRadio_get(main->userState) ==
	     cliPlayer_unknown - cliPlayer_closed))  {
	  butRadio_set(main->userState,
		       main->playerList.players[userNum].state -
		       cliPlayer_closed,
		       FALSE);
	}
      }
    }
  }
}


static ButOut  userInput(But *but, const char *input)  {
  CliMain  *main = but_packet(but);

  assert(MAGIC(main));
  if (input[0] != '\0') {
    str_copyChars(&main->history[main->historyBeingFilled], input);
    if (++main->historyBeingFilled >= MAIN_HISTORYMAXLEN)
      main->historyBeingFilled = 0;
    main->historyBeingViewed = 0;
    if (++main->historyLen >= MAIN_HISTORYMAXLEN)
      main->historyLen = MAIN_HISTORYMAXLEN - 1;
  }
  str_print(&main->data->cmdBuild, "%s\n", input);
  cliConn_send(&main->data->conn, str_chars(&main->data->cmdBuild));
  if (!main->promptVisible)
    str_print(&main->data->cmdBuild, "#> %s\n", input);
  butTbin_insert(main->script->tbin, str_chars(&main->data->cmdBuild));
  butTextin_set(but, "", FALSE);
  main->promptVisible = FALSE;
  return(0);
}


static ButOut  quitPressed(But *but)  {
  cliMain_destroy(but_packet(but));
  return(0);
}


static ButOut  iResize(ButWin *win)  {
  CliMain  *main = butWin_packet(win);
  int  w, h, size;
  int  border;

  assert(MAGIC(main));
  w = butWin_w(win);
  h = butWin_h(win);
  but_resize(main->iBg, 0,0, w,h);
  border = 2*butEnv_stdBw(butWin_env(win));
  size = w - border * 2;
  but_resize(main->iPic, border, border, size, size);
  return(0);
}


static ButOut  gamesPressed(But *but)  {
  CliMain  *main = but_packet(but);

  assert(MAGIC(main));
  cliGameList_openWin(&main->gameList);
  return(0);
}


static ButOut  playersPressed(But *but)  {
  CliMain  *main = but_packet(but);

  assert(MAGIC(main));
  cliPlayerList_openWin(&main->playerList);
  return(0);
}


static ButOut  newState(But *but, int value)  {
  CliPlayerState  newState = value + cliPlayer_closed;
  CliMain  *main = but_packet(but);
  int  userNum;

  assert(MAGIC(but));
  switch(newState)  {
  case cliPlayer_looking:
    cliConn_send(&main->data->conn, "toggle open 1\ntoggle looking 1\n");
    break;
  case cliPlayer_open:
    cliConn_send(&main->data->conn, "toggle open 1\ntoggle looking 0\n");
    break;
  case cliPlayer_closed:
    cliConn_send(&main->data->conn, "toggle open 0\ntoggle looking 0\n");
    break;
  default:
    assert(0);
    break;
  }
  userNum = cliPlayerList_lookupPlayer(&main->playerList,
				       str_chars(&main->data->userName));
  if (userNum >= 0)  {
    cliPlayerList_setState(&main->playerList, userNum, newState);
  }
  return(0);
}


void  cliMain_message(CliMain *main, const char *message)  {
  AbutMsgOpt  ok;

  assert(MAGIC(main));
  cliMain_clearMessage(main);
  ok.name = msg_ok;
  ok.callback = msgBoxKilled;
  ok.packet = main;
  ok.keyEq = cg_return;
  main->message = abutMsg_optCreate(main->data->cg->abut, main->win, 4,
				    message, NULL, NULL, 1, &ok);
}


void  cliMain_clearMessage(CliMain *main)  {
  assert(MAGIC(main));
  if (main->message)
    abutMsg_destroy(main->message, FALSE);
  main->message = NULL;
}


static ButOut  msgBoxKilled(But *okBut)  {
  CliMain  *main = but_packet(okBut);

  assert(MAGIC(main));
  assert(main->message != NULL);
  abutMsg_destroy(main->message, FALSE);
  main->message = NULL;
  return(0);
}


void  cliMain_log(CliMain *main, const char *str)  {
  if (main->promptVisible)  {
    butTbin_delete(main->script->tbin,
		   butTbin_len(main->script->tbin) - 3, 3);
    main->promptVisible = FALSE;
  }
  butTbin_insert((main)->script->tbin, (str));
}


static void  client2client(CliMain *main, const char *buf)  {
  char  oppName[31], freeChar = 'x';
  int  matches, hcap;
  float  komi;

  if (strncmp(buf, "CLIENT: <cgoban ", 16))
    return;
  while ((*buf != '>') && (*buf != '\0'))
    ++buf;
  if ((buf[0] == '\0') || (buf[1] == '\0') || (buf[2] == '\0'))
    return;  /* Not actually from cgoban.  Oops. */
  buf += 2;
  matches = sscanf(buf, "match %30s wants handicap %d, komi %f, %c",
		   oppName, &hcap, &komi, &freeChar);
  if (matches >= 3)  {
    /* Extra match info. */
    cliMatch_extraInfo(main->playerList.match, oppName, hcap, komi,
		       freeChar == 'f');
  }
}


static ButOut  prevHistory(But *but,
			   KeySym keysym, uint keyModifiers, void *context) {
  CliMain  *main = context;
  int  i;

  assert(MAGIC(main));
  if (main->historyBeingViewed == -main->historyLen) {
    return(BUTOUT_ERR);
  }
  if (main->historyBeingViewed == 0) {
    str_copyChars(&main->history[main->historyBeingFilled],
		  butTextin_get(main->tin));
  }
  --main->historyBeingViewed;
  i = main->historyBeingFilled + main->historyBeingViewed;
  if (i < 0)
    i += MAIN_HISTORYMAXLEN;
  butTextin_set(main->tin, str_chars(&main->history[i]), FALSE);
  return(0);
}


static ButOut  nextHistory(But *but,
			   KeySym keysym, uint keyModifiers, void *context) {
  CliMain  *main = context;
  int  i;

  assert(MAGIC(main));
  if (main->historyBeingViewed == 0) {
    return(BUTOUT_ERR);
  }
  ++main->historyBeingViewed;
  i = main->historyBeingFilled + main->historyBeingViewed;
  if (i < 0)
    i += MAIN_HISTORYMAXLEN;
  butTextin_set(main->tin, str_chars(&main->history[i]), FALSE);
  return(0);
}
