#!/usr/local/bin/pike

constant cvs_version = "$Id: slashdot.pike,v 1.15 1999/04/02 07:34:19 neotron Exp $";
//#define DEBUG

//#define NO_THREADS

#if constant(thread_create) && !defined(NO_THREADS)
#define USE_THREADS
#endif

#define HOME combine_path(getenv("HOME"), ".pslashdot/")
#define IMGDIR combine_path(HOME, "topic_images/")
object clist, status, detail_frame, main_window, mbuttonbox, 
  hdb, ctable, htable, crtable, urls_window, settings;
int selected, compact_mode, working;
array headlines = ({}), message_list;
int sort_order;
multiset new_headlines = (<>);
multiset visible = (<>);
mapping image_cache=([]); // Cached topic images.
constant check_image ="R0lGODlhDwAPAOMAAP///+rq2n5+dCMjIVhYTgoKCqSkmL6+rNDQwOUWSuUWSuUWSuUWSuUWSuUW\r\nSuUWSiH5BAEKAA8ALAAAAAAPAA8AAARB8MlJJajYhpBpEENwdQBRCCNZFOIEpA9gnCkgGLXZxseK\r\nXC8axVYYAGc7l6BAeBFQGSIuBBsuBdBObJkk/bTDTAQAOw==";

#define BOLD "bold", "r"
#define NORMAL "medium", "r"
#define ITALIC "medium", "o"
#define BOLDITALIC "bold", "o"
#define FONT(x, y) sprintf("-*-helvetica-%s-%s-*-*-%d-*-*-*-*-*-iso8859-1", x, y)
constant font_sizes = ([
  "def": 12,
  "h1" : 34,
  "h2" : 25,
  "h3" : 20,
  "h4" : 17
]);

// Cache with fonts in GDK format
mapping fonts = ([]);

// Load a specific font using the stored settings with the default as
// a fallback.
void load_font(string type)
{
  object font;
  if(fonts[type])
    destruct(fonts[type]);
  if(catch(font = GDK.Font(settings["font_"+type])))
  {
    werror("Couldn't load font "+settings["font_"+type]+"\n");
    catch { 
      switch(type)
      {
       case "normal":
	font = GDK.Font(FONT(NORMAL, font_sizes->def));
	break;
       case "i":
	font = GDK.Font(FONT(ITALIC, font_sizes->def));
	break;
       case "bi":
	font = GDK.Font(FONT(BOLDITALIC, font_sizes->def));
	break;
       case "b":
	font = GDK.Font(FONT(BOLD, font_sizes->def));
	break;
       default:
	font = GDK.Font(FONT(BOLD, font_sizes[type]));
	break;
      }
    };
  }
  if(!font)
  {
    werror("Fatal: Couldn't load any font for "+type+"\n");
    exit(1);
  }
  fonts[type] = font;
}

// Load all fonts.
void load_fonts()
{
  foreach( ({ "normal", "i", "h4", "h3", "h2", "h1", "b", "bi"}),
	   string header)
    load_font(header);
}

// Cached version of the "checked" image.
mapping check;

// Buttons to be "locked" while "working".
object refreshb, commentb, prevb, nextb, refetchb, compressb;

// Need this for parse_html.
import spider;

// While working, certain functions are blocked (ie those that fetch
// URLs. This function makes buttons with these functions insensitive.
int work(int now) {
  working = now;
  now = !now;
  refreshb -> set_sensitive(now);
  compressb -> set_sensitive(now);
  if(detail_child) {
    commentb -> set_sensitive(now);
    refetchb -> set_sensitive(now);
    
    if(!now || selected < 1)                      prevb->set_sensitive(0);
    else                                          prevb->set_sensitive(1);
    if(!now || (selected+1) >= sizeof(headlines)) nextb->set_sensitive(0);
    else                                          nextb->set_sensitive(1);
  }
  if(!working)
    alarm(0);
  return working;
}

// Returns the version, based on the cvs version. If it has an
// argument, the returned string is good as a user agent.
string version(int|void user_agent)
{
  string v;
  sscanf(cvs_version,"%*sslashdot.pike,v %s ", v);
  if(user_agent)   return "PiGTK_Reader/"+v;
  else             return "Slashdot Reader v"+v;
}

// Function used to "semi-parse" html. Most things are simply
// removed. Some features are used parsed and used however. This
// includes headers and ul/ol lists.
mixed parser(string tag, mapping m, string s, mapping data)
{
  if(!data)
    data = ([]);
  s = replace(s, ({ "\n","\t"}), ({" ", " "}));
  switch(tag)
  {
   case "br": return "{br}"+s;
   case "p": case "tr": return "{p}"+s;
   case "cite": return " \""+s+"\"";
   case "li":
    if(data->count) {
      s = "{br}"+"{ }" * (data->depth) +data->count+". "+s;
      data->count++;
    } else
      s = "{br}" +"{ }" * (data->depth) +"* "+s;
    return s;

   case "dd":
    s = "{br}" + "{ }" * (data->depth +1 ) +s;
    break;
   case "dt":
    s = "{br}" + "{ }" * (data->depth) +s;
    break;
    
   case "dl":
    int oldcount = data->count;
    data->count = 0;
    data->depth++;
    s = parse_html(s, ([ ]), (["ol":parser,"li": parser, "ul":parser,
			       "dl":parser, "dt": parser, "dd": parser ]),
		   data);
    data->depth--;
    data->count = oldcount;
    break;

   case "ol":
    int oldcount = data->count;
    data->count = 1;
    data->depth++;
    s = parse_html(s, ([ ]), (["ol":parser,"li": parser, "ul":parser,
			       "dl": parser, "dt": parser, "dd": parser ]),
		   data);
    data->depth--;
    data->count = oldcount;
    break;
   case "ul":
    int oldcount = data->count;
    data->count = 0;
    data->depth++;
    s = parse_html(s, ([ ]), (["ol":parser, "li": parser, "ul":parser,
			       "dl": parser,"dt": parser, "dd": parser]),
		   data);
    data->depth--;
    data->count = oldcount;
    break;
   case "i":
    if(data->header) return s;
    if(!data->italic && !data->bold) 
      s = "{{font{i}"+s+"}}";
    else if(data->bold)
      s = "}}{{font{bi}"+s+"}}{{font{b}";
    else return s;
    data->italic = 1;
    s = parse_html(s, ([ ]), (["i":parser, "b":parser,
			       "h1": parser, "h2": parser, "h3": parser,
			       "h4": parser, "h5": parser, "h6": parser,
			       "h7": parser ]), data);
    data->italic = 0;
    return s;
   case "b":
    if(data->header) return s;
    if(!data->italic && !data->bold) 
      s = "{{font{b}"+s+"}}";
    else if(data->italic)
      s = "}}{{font{bi}"+s+"}}{{font{i}";
    else return s;
    data->bold = 1;
    s = parse_html(s, ([ ]), (["i":parser, "b":parser,
			       "h1": parser, "h2": parser, "h3": parser,
			       "h4": parser, "h5": parser, "h6": parser,
			       "h7": parser ]), data);
    data->bold = 0;
    return s;
   case "h5": case "h6": case "h7":
    tag = "h4";
   case "h1":  case "h2": case "h3": case "h4":
    if(data->header)
      return s;
    else if(data->italic)
      if(data->bold)
	s = "}}[p}{{font{"+tag+"}"+s+"}}{p}{{font{bi}";
      else
	s = "}}{p}{{font{"+tag+"}"+s+"}}{p}{{font{i}";
    else if(data->bold)
      s = "}}{p}{{font{"+tag+"}"+s+"}}{p}{{font{b}";
    else
      s = "{p}{{font{"+tag+"}"+s+"}}{p}";

    data->header = 1;
    s = parse_html(s, ([ ]), (["i":parser, "b":parser,
			       "h1": parser, "h2": parser, "h3": parser,
			       "h4": parser, "h5": parser, "h6": parser,
			       "h7": parser ]), data);
    data ->header = 0;
    return s;
   case "a":
    if(!m->href)
      break;
    if(!data->urls)
      data->urls = ([]);
    sscanf(s, "%*[ \t]%s", s);
    if(sscanf(reverse(s), "%*[ \t]%s", s) == 2)
      s = reverse(s);
    if(m->href[0] == '/')
      m->href = "http://slashdot.org"+m->href;
    else if(!search(m->href, "articles"))
      m->href = "http://slashdot.org/"+m->href;
    data->urls[m->href] = decode_html(s);
    break;
  }
  return s;
}

// Remove left-over <...> and decodes html entities.
string decode_html(string s)
{
  string p1, p2;
  s = " "+s;
  while(sscanf(s, "%s<%*s>%s", p1, p2) == 3) 
    s = p1 + p2;
  s = s[1..];
  return replace(s, indices(h2c), values(h2c));
}

// Removes html tags and does some limited parsing.
string remove_html(string in, mapping|void data)
{
  //  rm("article");
  //  Stdio.write_file("article", in);
  in = parse_html(in || "", 
		  ([]), 
		  ([  "br":parser, "p": parser,
		      "a": parser, "i":parser, "u":parser, "b":parser,
		      "strong":parser, "em":parser,
		      "table":parser, "td":parser, "tr":parser,
		      "dl": parser,"dt": parser, "dd": parser,
		      "th":parser, "blockquote":parser,
		      "center":parser,"font": parser,
		      "h1":parser, "h2":parser, "h3":parser, "h4":parser,
		      "ul":parser, "ol":parser,
		      "cite": parser ]), data);
  //  werror("%s\n", replace(in, "{p}", "\n\n"));
  return decode_html(in); //replace(in, "{p}", "\n\n"));
}

// Sort the healines in the list.
void f_sort_headlines(int|void q, object|void widget, mixed...args)
{
  int new;
  if(sizeof(args)) {
    new = args[0];
    switch(new) {
     case 2: new = 0;  break;  // Title
     case 4: new = 3;  break;  // Author Field
     case 3: new = 2;  break;  // Date
     case 0: new = -1; break;  // New
     case 7: new = 6;  break;  // Num Comments
    }
  } else
    new = -1;
  if(sort_order != new) {
    sort_order = new;
    switch(new) {
     case 1: // Sort on unread status
      array tmp = ({});
    
      for(int i = 0; i < sizeof(headlines); i++)
	tmp += ({ (!htable[ headlines[i][1] ]->read) +
		  headlines[i][2] });
      sort(tmp, headlines);
      headlines = reverse(headlines);
      break;
     case -1: // Sort on new / date
      array tmp = ({});
      for(int i = 0; i < sizeof(headlines); i++)
	tmp += ({ (new_headlines[ headlines[i][1] ]) +
		  headlines[i][2] });
      sort(tmp, headlines);
      headlines = reverse(headlines);
      break;

     case 6: // Comments, sort by integer
      sort(Array.map(column(headlines, new),
		     lambda(string s) { return s  == "N/A"? 1000000 :
		       (int)s; }),
	   headlines);
      break;
    
     default:
      sort(Array.map(column(headlines, new), lower_case), headlines);
      if(new == 2)
	headlines = reverse(headlines); // Want newest first, by default.
      break;
    }
  }  else
    headlines = reverse(headlines);
  clist->clear();
  clist->freeze();
  int num;
  object col1 = GDK.Color(240,240,255);
  object col2 = GDK.Color(255,255,255);
  object fg = GDK.Color(0,0,0);
  foreach(headlines, array w)  {
    clist->append(({ "", "", w[0], w[2][..15], w[3], w[5], w[6]  }));
    if(!htable[ w[ 1 ] ]->read)
      clist->set_pixmap(num, 1, check->img, check->alpha);
    if(new_headlines[ w[ 1 ] ])
      clist->set_pixmap(num, 0, check->img, check->alpha);
    if(num % 2)
      clist->set_background(num, col1);
    else
      clist->set_background(num, col2);
    clist->set_foreground(num, fg);
    num++;
  }
  clist->thaw();
}

// Alarm function used if a connection locks up or doesn't finish.
int fetch_timeout()
{
  set_status("Timeout downloading data.");
  call_out(set_status, 15);
  work(0);
  set_status();
  return 1;
}


// Open an URL and send a request and return the resulting file
// object.
object open_url(string host, string file, int|void is_image)
{
  alarm(100);
  work(1);
  int res;
  object fd = Stdio.FILE();
  if(catch(res = fd->connect(host, 80)) || !res)
  {
    destruct(fd);
    set_status("HTTP Connection Failed.");
    call_out(set_status, 15);
    if(!is_image) {
      remove_call_out(load_headlines);
      call_out(load_headlines, 30);
    }
    alarm(0);
    work(0);
    return 0;
  }
  fd->write("GET "+file+" HTTP/1.0\r\n"
	    "Host: "+host+":80\r\n"
	    "User-Agent: "+version(1)+"\r\n\r\n");
  return fd;
}


// Open an URL using the function above, read all data from the
// object and return it.
string get_url(mixed ... args)
{
  object fd;
  if(!(fd = open_url(@args))) 
    return 0;
  string line, s = "";
  while(line = fd->gets())
    s += line +"\n";
  destruct(fd);
  alarm(0);
  work(0);
  return s;
}

// Change message on the status display.
void set_status(string|void text)
{
  remove_call_out(set_status);
  remove_call_out(set_status);
  remove_call_out(set_status);
  if(text)
    status->set(text);
  else
    status->set(version()+": (idle)");
}

// Download and parse comments, with threads, from Slashdot.
array get_slashdot_comments_threaded(array entry, int cache_only)
{
  array messages = ({});
  if(ctable[ entry[1] ])
    return ctable[ entry[1] ];
  if(!cache_only)
    return 0;
  string path;
  if(!sscanf(entry[1], "http://slashdot.org/"+entry[7]+"/%s.shtml", path))
    return 0;
  object fd = open_url("slashdot.org",  "/comments.pl?mode=nested&sid="+path, 1);
  if(!fd)
    return 0;
#if defined(USE_THREADS)
  object progress = GTK.ProgressBar();
  object label = GTK.Label("Downloaded 0 KB\n");
  object win = GTK.Window(GTK.WINDOW_TOPLEVEL)
    -> set_title("Download Progress")
    -> set_policy(1,1,0)
    -> add(GTK.Frame("Fetching Comments...")
	   -> border_width(5)
	   -> add(GTK.Vbox(0,0)
		  -> set_spacing(10)
		  -> add(progress)
		  -> add(label)))
    -> show_all();
  
  float max = (float)((int)entry[6] || 300);
  float downloaded = 1.0;
#endif
  string line, msg;
  int started, level;
  while(line = fd->gets())
  {
    alarm(20);
    
#if defined(USE_THREADS)
    downloaded += strlen(line) / 1024.0;
    set_status(sprintf("Decoded %d comments (%d KB).",
		       sizeof(messages), (int)downloaded));
    label->set(sprintf("Downloaded %d KB\nDecoded %d comments.",
		       (int)downloaded, sizeof(messages)));
    progress->update(min(sizeof(messages)/ max, 1.0));
#endif
    string subject, author, email, message, comment;
    string lline = lower_case(line);
    if(!started)
      if(search(lline, "<tr><td bgcolor=cccccc>") != -1)
	started++;
      else
	continue;
    else if(lline == "<ul>")
      level++;
    else if(lline[strlen(lline)-5..strlen(lline)-1] == "</ul>")
      level--;
    if(msg) {
      if(lline != " ] </font></td></tr>")
	msg += line +"\n";
      else {
	//	write("GOT [[[ %s ]]] \n\n", msg);
	if(sscanf(msg, "%*s<TR><TD bgcolor=cccccc>\n<A name=\"%*d\"><B>%s</B>"
		  "</A>%*sby %s on%*s<TD bgcolor=ffffff>%s</TD>%*s[%*s<A href=%s>",
		  subject, author, message, comment) == 10)
	{
	  if(sscanf(author, "<A href=\"mailto:%s\">%s</A>", email, author))
	    email = "("+email+")";
	  else
	    email = 0;
	  message = replace(message - "\r", "\n", " ");

	  messages += ({ ({ author, email,
			    ("  "*level) +" "+ subject, comment, message }) });
	  msg = 0;
	}
      }
    }
    if(search(lline, "<tr><td bgcolor=cccccc>") != -1)
      msg = line+"\n";
  }
#if defined(USE_THREADS)
  destruct(progress);
  destruct(win);
#endif
  set_status("Downloaded "+sizeof(messages)+" comments.");
  call_out(set_status, 20);
  destruct(fd);
  work(0);
  if(sizeof(messages))
  {
    ctable[ entry[1] ] = messages;
    save_pslashdata();
    mapping hl = htable[ entry[1] ];
    hl->lines[6] = (string)sizeof(messages);
    htable[ entry[1] ] = hl;
    save_pslashdata();
  }
  return messages;
}

// Download and parse comments from Slashdot.
array get_slashdot_comments(array entry, int cache_only)
{
  array messages = ({});
  if(ctable[ entry[1] ])
    return ctable[ entry[1] ];
  if(!cache_only)
    return 0;
  string path;
  if(!sscanf(entry[1], "http://slashdot.org/"+entry[7]+"/%s.shtml", path))
    return 0;
  object fd = open_url("slashdot.org",  "/comments.pl?mode=flat&sid="+path, 1);
  if(!fd)
    return 0;
#if defined(USE_THREADS)
  object progress = GTK.ProgressBar();
  object label = GTK.Label("Downloaded 0 KB\n");
  object win = GTK.Window(GTK.WINDOW_TOPLEVEL)
    -> set_title("Download Progress")
    -> set_policy(1,1,0)
    -> add(GTK.Frame("Fetching Comments...")
	   -> border_width(5)
	   -> add(GTK.Vbox(0,0)
		  -> set_spacing(10)
		  -> add(progress)
		  -> add(label)))
    -> show_all();
  
  float max = (float)((int)entry[6] || 300);
  float downloaded = 1.0;
#endif
  string data = "", read= fd->read(1024);
  if(!read) return 0;
  do
  {
    alarm(20);
    data += read;
#if defined(USE_THREADS)
    downloaded += strlen(read) / 1024.0;
    set_status(sprintf("Decoded %d comments (%d KB).",
		       sizeof(messages), (int)downloaded));
    label->set(sprintf("Downloaded %d KB\nDecoded %d comments.",
		       (int)downloaded, sizeof(messages)));
    progress->update(min(sizeof(messages)/ max, 1.0));
#endif
    string subject, author, email, message, comment;
    while(sscanf(data, "%*s<TR><TD bgcolor=cccccc>\n<A name=\"%*d\"><B>%s</B>"
		 "</A>%*sby %s on%*s<TD bgcolor=ffffff>%s</TD>%*s[%*s<A href=%s>%s",
		 subject, author, message, comment, data) == 11) 
    {
      if(!sscanf(author, "<A href=\"mailto:%s\">%s</A>", email, author))
	email = 0;
      message = replace(message - "\r", "\n", " ");
      messages += ({ ({ author, email, subject, comment, message }) });
    }
  } while((read = fd->read(1300)) && strlen(read));
#if defined(USE_THREADS)
  destruct(progress);
  destruct(win);
#endif
  set_status("Downloaded "+sizeof(messages)+" comments.");
  call_out(set_status, 20);
  destruct(fd);
  work(0);
  if(sizeof(messages))
  {
    ctable[ entry[1] ] = messages;
    save_pslashdata();
    mapping hl = htable[ entry[1] ];
    hl->lines[6] = (string)sizeof(messages);
    htable[ entry[1] ] = hl;
    save_pslashdata();
  }
  return messages;
}

array get_article(string data)
{
  string msg;
  sscanf(data, "%*s<B>Posted by%*s<FONT size=2><B>from the%*s</B></FONT><BR>%s</P> &lt;&nbsp;%s", msg, data);
  return ({ msg, data });
}

int|string get_slashdot_description(array entry, int fetch)
{
  if(fetch != -1 &&
     htable[ entry[1] ]->data &&
     strlen(htable[ entry[1] ]->data))
    return htable[ entry[1] ]->data;

  if(!fetch)
    return 0;
  
  array path = array_sscanf(entry[1], "http://%*s/%s");
  object fd = open_url("slashdot.org", "/"+path[0], 1);
  if(!fd) 
    return work(0);
  string data, read, article;
  while((read = fd->read(1000)) && strlen(read)) {
    alarm(20);
    data += read;
    [article,data] = get_article(data);
    //    werror(".");
    if(article)
      break;
  }
  if(!article)
    return work(0);
  mapping hl = htable[ entry[1] ];
  hl->data = article;
  htable[ entry[1] ] = hl;
  save_pslashdata();
  set_status();
  work(0);
  return hl->data;
}

void low_load_headlines()
{
#ifndef DEBUG
  string s = get_url("slashdot.org", "/ultramode.txt");
#else
  string s = Stdio.read_file("ultramode.txt");
#endif
  selected = -1;
  if(s)
  {
    new_headlines = (<>);
    array articles = (s / "\n%%\n")[1..];
    foreach(articles, string article)
    {
      array lines = article / "\n" - ({""});
      if(lines && sizeof(lines) >= 9) {
	lines[4] = replace(lines[4], "-", " ");
	lines[0] = decode_html(lines[0]);
	mapping hl = htable[ lines[1] ] || ([]);
	hl->lines = lines;
	hl->added = slashdate_to_int(lines[2]);
	htable[ lines[1] ] = hl;
	new_headlines[lines[1]] = 1;
      }
    }
  } else
    new_headlines = (<>);
  save_pslashdata();
  set_status();
  headlines = ({});
  foreach(indices(htable), string h) {
    mapping entry = htable[h];
    
    if(!(int)settings["max_display_age"] ||
       ((int)settings["max_display_age"] * 86400
	> (time() -  entry->added )))
      headlines += ({ entry->lines });
    if(h != (h = decode_html(h)))
    {
      htable->delete(entry->lines[1]);
      entry->lines[1] = h;
      htable[h] = entry;
    }
  }
  sort_order = -2;
  f_sort_headlines();
  work(0);
  call_out(load_headlines, 1800);
}


void load_headlines()
{
  work(1);
  remove_call_out(load_headlines);
  set_status("Loading headlines...");
#if defined(USE_THREADS)
  thread_create(low_load_headlines);
#else
  call_out(low_load_headlines, 1);
#endif
}

object comments_window, comments_child, comment_urls;
int comment; // selected comment
string comment_url; // URL of selected comment.

void f_wheel_scroll(array data, object w, mapping event)
{
  int old = comment;
  switch(event->button)
  {
   case 4:
    if(comment > 0)
      comment--;
    break;
  case 5:
    if(comment < sizeof(message_list))
      comment++;
    break;
  }
  if(old != comment) {
    data[2] = comment;
    //    catch(w -> signal_emit_stop(event->type+"_event"));
    f_read_comments(data);

  }
}

void f_read_comments(array data)
{
  if(working)  return;
  array entry = data[1];
  int num = data[2];
  if(!data[0] || !message_list)
    message_list = get_slashdot_comments_threaded(entry, data[0]);

  if(!message_list && !data[0])
  {
    set_status("Downloading Comments...");
    data[0] = 1;
#if defined(USE_THREADS)
    thread_create(f_read_comments, data);
#else
    remove_call_out(f_read_comments);
    call_out(f_read_comments, 1, data);
#endif
    return ;
  }
  call_out(set_status, 15);
  if(!message_list || !sizeof(message_list))
    return;
  if(!comments_window) {
    comments_window = GTK.Window(GTK.WINDOW_TOPLEVEL)
      -> set_policy(1,1,0);
  }
  object bchild, butt2, butt3, butt7, article, comment_list;
  comment_url = "http://slashdot.org"+message_list[num][3];

  if(sizeof(data) < 4) {
    catch(comments_window->remove(comments_child));
    object hbox;
    comments_child = GTK.Frame(" "+entry[0]+" ");
    comment_urls = GTK.Frame()->set_shadow_type(GTK.ShadowNone);
    
    object butt = GTK.Button(" Reply ");
    object butt5 = GTK.Button(" Refetch Comments ");
    butt2 = GTK.Button(" Previous Comment ");
    butt3 = GTK.Button(" Next Comment ");
    object butt4 = GTK.Button(" Close Window ");
    object ad1 = GTK.Adjustment(0.0,0.0,0.0,0.0,0.0,0.0);
    bchild = GTK.Label("");
    butt7 = GTK.Button() -> add(bchild);    
    article = GTK.Text(GTK.Adjustment(),ad1)
      -> set_editable(0)
      -> set_word_wrap(1);
    object scroll = GTK.Vscrollbar(ad1);
    comment_list = GTK.SClist(4);
    comment_list ->set_policy(GTK.PolicyAutomatic, GTK.PolicyAutomatic)
      -> set_column_title(0, "Nr")
      -> set_column_width(0, 20)
      -> set_column_title(1, " N ")
      -> set_column_width(1, 16)
      -> set_column_title(2, "Author")
      -> set_column_width(2, 140)
      -> set_column_title(3, "Subject")
      -> set_column_width(3, 300)
      //      -> column_title_active(1)
      //      -> column_titles_show()
      -> set_usize(600, 160);

    array needed_data = ({ 0, entry, comment, bchild, butt7, butt2,
			   butt3, article, comment_list });
    
    article -> signal_connect("button_release_event", f_wheel_scroll,
				   needed_data);
    comment_list->signal_connect("select_row",
				 lambda(array data, object w, int row) {
				   if(row != comment) {
				     comment = row;  data[2] = row;
				     f_read_comments(data);
				   }
				 },needed_data);
    comment_list -> signal_connect("event", f_wheel_scroll,
				   needed_data);
    
    butt->signal_connect("clicked", lambda(array data) {
				      f_open_url(comment_url);
				    }, needed_data);
    butt2->signal_connect("clicked", lambda(array data) {
				       data[2] = --comment;
				       f_read_comments(data);
				     }, needed_data);
    butt3->signal_connect("clicked", lambda(array data) {
				       data[2] = ++comment;
				       f_read_comments(data);
				     }, needed_data);
    butt4->signal_connect("clicked", lambda(array objs) {
				       message_list = 0;
				       catch(destruct(objs[0]));
				       catch(destruct(objs[1]));
				       visible -= (< comment_urls >);
				     }, ({ comments_window,
					   comments_child }));
    butt5->signal_connect("clicked", lambda(array entry) {
				       catch(destruct(comments_child));
				       catch(destruct(comments_window));
				       visible -= (< comment_urls >);
				       ctable[ entry[1] ] = 0;
				       f_read_comments( ({ 0, entry, 0 }));
				     }, entry);
      
    comments_window -> set_title(entry[0]);
    comments_child  -> border_width(5);
    //    article->set_usize(600, 250);
    object buttbox = GTK.Hbox(0,0)
      -> set_spacing(4)
      -> pack_start(butt,0,1,0)
      -> pack_start(butt2,0,1,0)
      -> pack_start(butt3,0,1,0)
      -> pack_start(butt7,0,1,0)
      -> pack_start(butt5,0,1,0)
      -> pack_start(butt4,0,1,0);
    object vbox = GTK.Vpaned()
      -> border_width(4)
      -> add1(GTK.Vbox(0,0)
	      -> pack_start(comment_list, 1,1,0)
	      -> pack_start(buttbox, 0, 0, 5))
      ->add2(GTK.Vbox(0,0)
	     -> pack_start(GTK.Hbox(0,0)
			   -> pack_start(article, 1, 1 ,0)
			   -> pack_start(scroll, 0,0,0), 1,1,0)
	     -> add(comment_urls));
	     
    comments_window->add(comments_child->add(vbox))->show_all();
    comment_urls->hide();
    int count = 0;
    comment_list->freeze();
    foreach(message_list, array msg) {
      comment_list->append(({ (string)++count, "", msg[0], msg[2] }) );
      if(!crtable[ msg[3] ]) {
	comment_list->set_pixmap(count - 1, 1, check->img, check->alpha);
      }
    }
    comment_list->thaw();
    butt7->signal_connect("clicked", lambda(object butt) {
				       if(visible[comment_urls])
				       {
					 comment_urls->hide();
					 butt->set(" Show URL List ");
					 visible -= (< comment_urls >);
				       } else {
					 comment_urls->show_all();
					 butt->set(" Hide URL List ");
					 visible[comment_urls] = 1;
				       }
				     }, bchild);
  } else 
    [bchild,butt7, butt2, butt3, article, comment_list] = data[3..];

  if(num < 1)           butt2->set_sensitive(0);
  else                  butt2->set_sensitive(1);
  if((num+1) >= sizeof(message_list)) butt3->set_sensitive(0);
  else                                butt3->set_sensitive(1);
  
  if(visible[comment_urls])
    bchild->set(" Hide URL List ");
  else
    bchild->set(" Show URL List ");
  
  mapping data = ([]);
  paste_text(article,
	     sprintf("{{font{h3}Author: %s %s{br}"
		     "Subject: %s}}\n\n%s",
		     message_list[num][0],
		     message_list[num][1]?"("+message_list[num][1]+")":"",
		     replace(message_list[num][2], "\n", " "),
		     message_list[num][4]), data);
  if(data->urls && sizeof(data->urls)) {
    if(visible[comment_urls])
      comment_urls->show();
    make_urls(entry[0], data->urls, comment_urls);
    butt7 -> show();
  } else {
    butt7 -> hide();
    comment_urls -> hide();
  }
  comment_list->select_row(num, 0);
  comment_list->moveto(num, 0, 0.5, 0.5);
  mark_comment_read(message_list[num][3], num, comment_list);
}



void f_open_url(string url)
{
  url = replace(url, ({",", " ", "(", ")", "\"" }),
		({"%2C", "%20", "%28", "%28", "%22"}));
  //  string data = Process.popen("sh -c '"+
  //			      replace(settings["current_browser"], "%s", url)+
  //			      " 2>/dev/null;echo $?'");
  object proc = Process.create_process(Process.split_quoted_string(replace(settings["current_browser"], "%s", url)));
  if(proc->wait()) // Previous command failed
    Process.create_process(Process.split_quoted_string(replace(settings["new_browser"], "%s", url)));
}

constant titles = #"Title:  
URL:  
Time:  
Author:  
Department:  
Topic:  
Comments:  
Story Type:  ";

object load_topic_image(string img, int cache_only)
{
  int cached = 1;
  if(image_cache[img])
    return image_cache[img];
    
  string s = Stdio.read_file(IMGDIR+img);
  if(!s && cache_only)
    return 0;
  
  if(!s && !cache_only)
  {
#ifdef DEBUG
    werror("Fetching uncached image, "+img+".\n");
#endif
    cached = 0;
    s = get_url("slashdot.wolfe.net", "/images/topics/"+img);
    if(!s) {
#ifdef DEBUG
      werror("Failed to load image...");
#endif
      return 0;
    }
    if(!sscanf(s, "%*s\r\n\r\n%s", s))
    {
#ifdef DEBUG
      werror("Incorrect response from server.\n"+s+"\n");
#endif
      return 0;
    }
  }
  if( mapping image = GTK.Util.low_decode_image( s ) )
  {
    // Decode went ok, cache this image.
    rm(IMGDIR+img);
    Stdio.write_file(IMGDIR+img, s);
    image_cache[img] = GDK.Image(0)->set(image->img);
    return image_cache[img];
    return image->img;
  } else {
    if(cached) {
#ifdef DEBUG
      werror("Couldn't decode cached image. Refetching.\n");
#endif
      rm(IMGDIR+img);
      return load_topic_image(img, cache_only);
    }
#ifdef DEBUG
    werror("Couldn't decode downloaded image.\n");
#endif
  }
  return 0;
}

void f_nextprev_entry(int num)
{
  if(working)
    return ;
  if(num >= 0 && num < sizeof(headlines)) {
    selected = num;
    clist->select_row(num, 0);
    clist->moveto(num, 0, 0.5, 0.5);
    mark_read(headlines[num][1]);
    show_details(headlines[num]);
  }
}

object detail_child, detail_urls;
int toshrink;
void do_resize(int|void type)
{
#if 0
  int ysize = main_window->ysize();
  switch(type)
  {
   case -1:
    // Hide.
    ysize -= 340;
    if(detail_child)
      detail_frame->remove(detail_child);
    detail_frame->hide();
    detail_urls->hide();

    break;
   case 0:
    // Show first time
    toshrink = detail_frame->ysize();
    ysize += 340;
    break;
  }
  main_window->set_usize(main_window->xsize(),ysize);
#endif
}

// Two functions that does parsing / inserting of html in a
// widget. VERY KLUDGY HTML PARSING! You have been warned.  :)

void insert_section(object w, string t, mixed ... data)
{
  int nl;
  //  werror("<<< "+t+" >>>\n");
  foreach(t / "\n", string s)
  {
    if(!strlen(s)) {
      if(++nl > 1)
	continue; 
    } else
      nl = 0;
    w->insert(s+"\n", @data);
  }
  if(strlen(t) && t[-1] != "\n")
    w->backward_delete(1);
}
void paste_text(object w, string text, mapping|void data)
{
  int nl;
  w->freeze();
  w->set_text("");
  
  string tag;
  foreach(remove_html(text, data) / "\n", string text) {
    string pre, post;
    if(!strlen(text - " " - "\t")) {
      if(++nl > 2)
	continue; 
    } else
      nl = 0;
    sscanf(text, "{br}%s", text);
    string newtext = "";
    foreach(text /  "{p}", string p) {
      if(strlen(p- " " - "\t"))
	newtext += sprintf("%-=75s\n\n",
			   replace(replace((p / " " - ({""})) * " ", "{ }", "   "),
				   "{br}", "\n"));
    }
    text = newtext; newtext = "";
    do {
      post = pre = tag = 0;
      if(!sscanf(text, "%s{{font{%s}%s}}%s", pre, tag, text, post)) {
	insert_section(w, text, fonts->normal, 0, 0);
	break;
      } else {
	//	werror("Pre: %O, Tag: %O, Text: %O, Post: %O\n",
	//	       pre, tag, text, post);
	if(strlen(pre) && pre)
	  insert_section(w, pre, fonts->normal, 0, 0);
	if(tag) {
	  insert_section(w, (tag[0] == 'h' ? "\n" : "" )+text,
			 fonts[tag], 0, 0);
	}
	text = post;
      }
    } while(text);
  }
  
  w->thaw();
  w->show();
}



void make_urls(string title, mapping urls, object url_frame)
{
  foreach(url_frame->children(), object c)
    catch(url_frame->remove(c));
  object list = GTK.SClist(2);
  list
    -> set_policy(GTK.PolicyAutomatic, GTK.PolicyAutomatic)
    -> set_column_title(0, " Link Name ")
    -> set_column_width(0, 200)
    -> set_column_title(1, " URL ")
    -> set_column_width(1, 200)
    -> column_title_active(1)
    -> column_titles_hide();
  object close, open;

  close = GTK.Button("Close Window");
  
  string title = "URLs for "+title;
  url_frame
    //    -> set_label(title)
    -> set_label("Click on an url to open in your browser.")
    -> add(list->show());
  
  foreach(sort(indices(urls)), array url)
    list->append( ({ urls[url], url }) );
  
  close->signal_connect("clicked", lambda() { urls_window->hide(); }, 0);
  list->signal_connect("select_row", lambda(array urls, object w, int row)
				     {
				       f_open_url(urls[row]);
				     }, sort(indices(urls)));
}

void f_delete_headline(string hid, object w, mixed args)
{
  if(htable[hid]) htable->delete(hid);
  if(ctable[hid]) ctable->delete(hid);
  foreach(headlines, array hline)
  {
    if(hline[1] == hid)
      headlines -= ({ hline });
  }
  if(sizeof(headlines))  {
    headlines = reverse(headlines);
    f_sort_headlines();
    selected = sizeof(headlines) -1;
    clist->select_row(selected, 0);
    f_view_details();
  }
  save_pslashdata();
}

void show_details(array entry, int|void no_cache)
{
  object img = load_topic_image(entry[8], !no_cache);
  if(!img && !no_cache) {
    set_status("Loading topic image...");
#if defined(USE_THREADS)
    thread_create(show_details, entry, 1);
#else
    call_out(show_details, 1, entry, 1);
#endif
    return ;
  }
  string text = get_slashdot_description(entry, no_cache);
  if(!text && !no_cache){
    set_status("Loading headline data...");
#if defined(USE_THREADS)
    thread_create(show_details, entry, 1);
#else
    call_out(show_details, 1, entry, 1);
#endif
    return ;
  }
  //  detail_frame->set_label(" "+entry[0]+" ");

  object l,r, header;
  commentb = GTK.Button(" Read Comments ");
  prevb = GTK.Button(" Previous ");
  nextb = GTK.Button(" Next ");
  object butt4 = GTK.Button(" Hide ");
  object butt7 = GTK.Button();
  object bchild = GTK.Label("");
  object del = GTK.Button(" Delete ");
  butt7->add(bchild);

  if(visible[detail_urls])
    bchild->set(" Hide URL List ");
  else
    bchild->set(" Show URL List ");

  refetchb = GTK.Button(" Refetch ");
  object butt6 = GTK.Button(" Spawn Browser ");
  object icon;
  if(img)
    icon = GTK.Image(img);
  else 
   icon = GTK.Label("");
  object ad1 = GTK.Adjustment(0.0,0.0,0.0,0.0,0.0,0.0);
  object description = GTK.Text(GTK.Adjustment(),ad1)
    -> set_editable(0)
    -> set_word_wrap(1);
  object scroll = GTK.Vscrollbar(ad1);
  
  if(text && img)
    set_status();
  else
    call_out(set_status, 15);
  mapping data = ([]);
  paste_text(description, text, data);
  
  l = GTK.Label(titles) -> set_justify(GTK.JUSTIFY_RIGHT);
  r = GTK.Label(entry[0..7]*"\n")->set_justify(GTK.JUSTIFY_LEFT);
  
  butt6->signal_connect("clicked", f_open_url, entry[1]);
  commentb->signal_connect("clicked", f_read_comments, ({ 0, entry, 0 }));
  prevb->signal_connect("clicked", f_nextprev_entry, selected-1);
  nextb->signal_connect("clicked", f_nextprev_entry, selected+1);
  del->signal_connect("clicked", f_delete_headline, entry[1]);
  butt4->signal_connect("clicked", lambda() { do_resize(-1); }, 0);
  butt7->signal_connect("clicked", lambda(object butt) {
				       if(visible[detail_urls])
				       {
					 detail_urls->hide();
					 butt->set(" Show URL List ");
					 visible -= (< detail_urls >);
				       } else {
					 detail_urls->show_all();
					 butt->set(" Hide URL List ");
					 visible[detail_urls] = 1;
				       }
				     }, bchild);
  refetchb->signal_connect("clicked", lambda(array entry) {
					set_status("Refetching Article...");
#if defined(USE_THREADS)
					thread_create(show_details, entry, -1);
#else
				       call_out(show_details, 1, entry, -1);
#endif
				   }, entry);
  object textbox = GTK.Hbox(0,0)
    ->pack_start(description,1,1,0)
    ->pack_start(scroll,0,0,0);
  
  object infobox = GTK.Vbox(0,0)
    -> pack_start(GTK.Hbox(0,0)
		  ->pack_start(l, 0,0,0)
		  ->pack_start(r,0,0,0)
		  ->pack_end(icon, 0,0,10)
		  , 0,1,0)
    ->pack_start(GTK.Hbox(0,0)
		 -> set_spacing(4)
		 -> pack_start(commentb, 0,1,0)
		 -> pack_start(butt6, 0,1,0)
		 -> pack_start(prevb,0,1,0)
		 -> pack_start(nextb,0,1,0)
		 -> pack_start(butt7,0,1,0)
		 -> pack_start(refetchb,0,1,0)
		 -> pack_start(del,0,1,0)
		 -> pack_start(butt4,0,1,0)
		 , 0,0,5);

  if(detail_child) {
    detail_frame->remove(detail_child);
  } else 
    do_resize();

  detail_child = GTK.Vbox(0,0)
    -> border_width(5)
    -> pack_start(infobox,0,0,0)
    -> pack_start(textbox,1,1,0)
    ;
  detail_frame->add(detail_child)->show_all();
  if(data->urls && sizeof(data->urls)) {
    make_urls(entry[0], data->urls, detail_urls);
    if(visible[detail_urls])
      detail_urls -> show();
    butt7 -> show();
  } else {
    butt7 -> hide();
    detail_urls -> hide();
    //    visible -= (< detail_urls >);
  }
  gc();
  object tip = GTK.Tooltips()
    -> enable()
    -> set_tip(commentb, "Fetch and view comments to this article.")
    -> set_tip(prevb, "Show previous article.")
    -> set_tip(nextb, "Show next article.")
    -> set_tip(butt4, "Hide the detail display.")
    -> set_tip(del, "Delete this article from the database.")
    -> set_tip(butt7, "Toggle display of list with URLs from this article.")
    -> set_tip(refetchb, "Refetch article from slashdot.org.")
    -> set_tip(butt6, "Open this article with your configured browser.");

  work(0);
}

void mark_read(string headline)
{
  if(!htable[ headline ]->read) {
    mapping hl = htable[ headline ];
    hl->read = 1;
    htable[ headline ]= hl;
    save_pslashdata();
    clist->set_text(selected, 1, "");
  }
}

void mark_comment_read(string url, int num, object list)
{
  if(!crtable[ url ]) {
    crtable[ url ]= 1;
    save_pslashdata();
    list->set_text(num, 1, "");
  }
}

void f_button_press(mixed args, object w, mapping event)
{
  switch(event->button) {
  case 2:
    if(compact_mode) {
      clist->column_titles_hide();
      mbuttonbox->hide();
      selected = -1;
      compact_mode = 0;
    } else {
      clist->column_titles_show();
      mbuttonbox->show();
      selected = -1;
      compact_mode = 1;
    }
    break;
  case 3:
    f_read_entry();
    break;
  }
}

void f_view_details()
{
  if(!working && selected != -1) {
    show_details(headlines[selected]);
    mark_read(headlines[selected][1]);
    clist->moveto(selected, 0, 0.5, 0.5);
  }
}

void f_read_entry()
{
  if(selected != -1) {
    mark_read(headlines[selected][1]);
    f_open_url(headlines[selected][1]);
  }
}

void save_and_exit()
{
  save_pslashdata();
  _exit(1);
}

int slashdate_to_int(string d)
{
  array date = array_sscanf(d,"%d-%d-%d %d:%d:%d");
  date[0] -= 1900;
  date[1] -= 1;
  return  mktime(mkmapping( ({ "year", "mon", "mday",
			       "hour", "min", "sec" }), date));
}

void load_pslashdata()
{
  mapping pslashdata;
  mkdir(HOME);
  mkdir(IMGDIR);
  if(!file_stat(IMGDIR) ||
     catch(Stdio.write_file(IMGDIR+getpid(), "test")))
  {
    werror("Couldn't create storage directory or directory not writable.\n"
	   "Check permissions on "+HOME+" and "+IMGDIR+".\n");
    exit(1);
  }
  rm(IMGDIR+getpid());
  if(file_stat(HOME+"pid") && kill((int)Stdio.read_file(HOME+"pid"), 0))
  {
    werror("Slashdot Headlines is already running or exited abnormally.\n"
	   "If it isn't running, please delete "+HOME+"pid.\n");
    _exit(0);
  }
  rm(HOME+"pid");
  Stdio.write_file(HOME+"pid", getpid()+"\n");
  hdb = Yabu.db(HOME+"database", "rwcC");
  ctable = hdb["cdata"];
  htable = hdb["headlines"];
  crtable = hdb["read_comments"];
  settings = hdb["settings"];
  catch(pslashdata = decode_value(Stdio.read_file(HOME+"cached")));
  if(pslashdata) {
    werror("Old database found. Transferring to new system.\n");
    foreach(indices(pslashdata), string headline)
    {
      werror("Importing %s...\n", headline);
      htable[headline] = ([
	// Don't want data since parsing has been moved to display stage
	// instead of download stage.
	//	"data":  pslashdata[headline]->data, 
	"read":  pslashdata[headline]->read,
	"lines": pslashdata[headline]->lines
      ]);
      ctable[headline] = pslashdata[headline]->messages;
    }
  }
  if(!settings["font_normal"])
  {
    settings["font_normal"] = FONT(NORMAL, font_sizes->def);
    settings["font_i"]      = FONT(ITALIC, font_sizes->def);
    settings["font_bi"]     = FONT(BOLDITALIC, font_sizes->def);
    settings["font_b"]      = FONT(BOLD, font_sizes->def);
    foreach( ({ "h4", "h3", "h2", "h1" }), string header)
      settings["font_"+header] = FONT(BOLD, font_sizes[header]);
  }
  if(!settings["current_browser"])
    settings["current_browser"] = "netscape -remote \"OpenURL(%s)\"";
  if(!settings["new_browser"])
    settings["new_browser"] = "netscape \"%s\"";
  if(!settings["max_display_age"])
    settings["max_display_age"] = 7;

  if(settings["save_version"] < 2)
  {
    werror("Converting old headline save file format: ");
    foreach(indices(htable), string htitle)
    {
      mapping d = htable[htitle];
      d->added = slashdate_to_int(d->lines[2]);
      write("+");
      htable->delete(htitle);
      htable[d->lines[1] ] = d;
    }
    ctable->purge();
    ctable = hdb["cdata"];
    settings["save_version"] = 2;
    write("\nDone!\n");
  }

  //  exit(0);
  save_pslashdata();
  //  htable=newtable;
}

void save_pslashdata()
{
  hdb->sync();
  rm(HOME+"cached"); // Don't need this. Aigh!
}

void f_select_font(array args)
{
  string fn = args[0];
  object selector, win, ok, cancel;
  if(args[2][fn])  return 0; // Already open
  args[2][fn] = 1;
  win = GTK.Window( GTK.WindowDialog )
    -> set_title("Select Font")
    -> add(GTK.Frame("Select Font")
	   -> set_border_width(5)
	   -> add(GTK.Vbox(0,0)
		  -> set_spacing(5)
		  -> add(selector = GTK.FontSelection()
			 -> set_font_name(args[1]["font_"+fn] ||
					  settings["font_"+fn]))
		  -> add(GTK.HbuttonBox(0,0)
			 -> set_layout(GTK.BUTTONBOX_SPREAD)
			 -> set_spacing(5)
			 -> add(ok = GTK.Button("Select Font"))
			 -> add(cancel = GTK.Button("Cancel")))))
    -> show_all();

  ok->signal_connect("clicked", lambda(array args) {
				  args[3]["font_"+args[0]] =
				    args[1]->get_font_name();
				  destruct(args[2]);
				  args[4][ args[0] ] = 0;
				}, ({ fn, selector, win, args[1], args[2] }));
  cancel->signal_connect("clicked", lambda(array args) {
				      args[2][args[1]] = 0;
				      destruct(args[0]);
				    }, ({ win, fn, args[2] }));
}

#define LABEL(x) GTK.Label(x)->set_alignment(1.0, 0.5)->set_justify(GTK.JUSTIFY_RIGHT)
#define SELECT_FONT(x) GTK.HbuttonBox(0,0)->add(fonts->x = GTK.Button("Select Font"))
void f_open_settings(mapping is_open)
{
  if(is_open->indeed)
    return 0;
  is_open->indeed = 1;
  mapping new_settings = ([]);
  object new_browser, current_browser, ok, cancel, max_age;
  mapping fonts = ([ ]);
  object win = GTK.Window(GTK.WindowDialog)
    -> set_border_width(5)
    -> set_title("Reader: Change Settings");
  object frame = GTK.Frame(" Change Settings");
  object table = GTK.Table(5, 11, 0)
    -> set_col_spacings(4)
    -> set_row_spacings(4)
    
    -> attach_defaults(GTK.Label("Fonts to be used in the article "
				 "and comment textfields:"), 0, 4, 0, 1)

    -> attach_defaults(LABEL("Header 1:"), 0, 1, 2, 3)
    -> attach_defaults(SELECT_FONT(h1), 1, 2, 2, 3)

    -> attach_defaults(LABEL("Header 2:"), 0, 1, 3, 4)
    -> attach_defaults(SELECT_FONT(h2), 1, 2, 3, 4)

    -> attach_defaults(LABEL("Header 3:"), 0, 1, 4, 5)
    -> attach_defaults(SELECT_FONT(h3), 1, 2, 4, 5)

    -> attach_defaults(LABEL("Header 4:"), 0, 1, 5, 6)
    -> attach_defaults(SELECT_FONT(h4), 1, 2, 5, 6)

    -> attach_defaults(LABEL("Bold:"), 2, 4, 2, 3)
    -> attach_defaults(SELECT_FONT(b), 4, 5, 2, 3)

    -> attach_defaults(LABEL("Italic:"), 2, 4, 3, 4)
    -> attach_defaults(SELECT_FONT(i), 4, 5, 3, 4)

    -> attach_defaults(LABEL("Bold and Italic:"), 2, 4, 4, 5)
    -> attach_defaults(SELECT_FONT(bi), 4, 5, 4, 5)

    -> attach_defaults(LABEL("Normal:"), 2, 4, 5, 6)
    -> attach_defaults(SELECT_FONT(normal), 4, 5, 5, 6)

    -> attach_defaults(GTK.Label("Commands to spawn a browser. Current browser tries to use an already\n"
				 "running browser. If it fails, it will start a new one with the second command.\n"
				 "Please note that the first command must exit or the program will lock up.")->set_justify(GTK.JustifyLeft)->set_alignment(0.0, 0.5), 0, 5, 6, 7)

    -> attach_defaults(LABEL("Current Browser Command:"), 0, 2, 7, 8)
    -> attach_defaults(current_browser = GTK.Entry(), 3, 5, 7, 8)

    -> attach_defaults(LABEL("New Browser Command:"), 0, 2, 8, 9)
    -> attach_defaults(new_browser =
		       GTK.Entry()->set_usize(200, 0), 3, 5, 8, 9)

    -> attach_defaults(LABEL("Max headline age in days (for display only):"), 0, 2, 9, 10)
    -> attach_defaults(max_age =
		       GTK.SpinButton(GTK.Adjustment((float)settings["max_display_age"], 1.0,
						     10000.0,1.0,
						     1.0,1.0), 1.0, 0), 
		       3, 5, 9, 10)
    ;

  foreach( ({ "normal", "i", "h4", "h3", "h2", "h1", "b", "bi"}), string fn)
    fonts[fn]->signal_connect("clicked",f_select_font,
			      ({ fn, new_settings, is_open }));
  
  new_browser -> set_text(settings["new_browser"]);
  current_browser -> set_text(settings["current_browser"]);
  frame->add(GTK.Hbox(0,0) ->
	     pack_start(GTK.Vbox(0,0)
			-> pack_start(table, 1, 1, 10)
			-> add(GTK.HbuttonBox(0,0)
			       -> set_layout(GTK.BUTTONBOX_SPREAD)
			       -> set_spacing(5)
			       -> add(ok = GTK.Button("Save Settings"))
			       -> add(cancel = GTK.Button("Cancel"))),
			1, 1, 10));
    win->add(frame)->show_all();
  ok->signal_connect("clicked",
		     lambda(array args) {
		       settings["new_browser"] = args[0]->get_text();
		       settings["current_browser"] = args[1]->get_text();
		       settings["max_display_age"] = args[5]->get_text();
		       foreach(indices(args[2]), string set)
			 settings[set] = args[2][set];
		       settings->sync();
		       load_fonts();
		       destruct(args[3]);
		       args[4]->indeed = 0;
				}, ({ new_browser, current_browser,
				      new_settings, win, is_open, max_age }));
  cancel->signal_connect("clicked", lambda(array args) {
				      destruct(args[0]);
				      args[1]->indeed = 0;
				    }, ({ win, is_open }));
}

void reorganize(int doit)
{
  object nt, ot;
  save_pslashdata();
  work(1);
#if defined(USE_THREADS)
  if(!doit) {
    thread_create(reorganize, 1);
    return;
  }
#endif
  foreach(({ "cdata", "headlines" }), string n)
  {
    ot = hdb[n];
    nt = hdb["new_table"];
    int cnt = sizeof(indices(ot)) * 2;
    int num = 0;
    foreach(indices(ot), string e)
    {
      set_status(sprintf("Optimizing database: %d%% done.",
			 (++num * 100) / cnt));
      nt[e] = ot[e];
    }
    ot->purge();
    ot = hdb[n];
    foreach(indices(nt), string e) {
      set_status(sprintf("Optimizing database: %d%% done.",
			 (++num * 100) / cnt));
      ot[e] = nt[e];
    }
    nt->purge();
    ot->sync();
  }
  rm(HOME+"pid");
  set_status();
  load_pslashdata();
  work(0);
}

int main(int argc, array (string) argv)
{
  if(sizeof(argv) > 1 && (argv[1] == "-v" || argv[1] == "--version"))
  {
    write("\n"+version()+"\nWritten by david@hedbor.org. "
	  "Download the latest verson from\n"
	  "http://pike-community.org/code/show_single.html?id=33\n\n");
    _exit(0);
  }
  load_pslashdata();
  argv = GTK.setup_gtk(argv);
  if(argc > 1 && search(argv, "--test") != -1)    
  {
    // Test HTML parser...
    string data = Stdio.File("stdin")->read();
    load_fonts();
    object w = GTK.Window( GTK.WINDOW_TOPLEVEL );
    object t = GTK.Text(GTK.Adjustment(0.0,0.0,0.0,0.0,0.0,0.0),
			GTK.Adjustment(0.0,0.0,0.0,0.0,0.0,0.0));
    w->add(t)->show_all()->set_usize(600,400);
    paste_text(t, data);
    return -1;
  }
  
  main_window = GTK.Window( GTK.WINDOW_TOPLEVEL );
  refreshb = GTK.Button(" Refresh ");
  object butt4 = GTK.Button(" Quit ");
  compressb = GTK.Button(" Optimize Database ");
  object sett = GTK.Button(" Settings ");
  
  status = GTK.Label(version());
  main_window->realize()
    -> set_policy(1,1,0)
    -> set_title(version())    
    -> set_icon_name("Slashdot Reader")
    -> signal_connect( "destroy", save_and_exit, 0);
  
  mbuttonbox = GTK.Hbox(0, 5)
    -> pack_start(refreshb, 0,1,1)
    -> pack_start(sett, 0,1,1)
    -> pack_start(compressb, 0,1,1)
    -> pack_start(status, 1,0,1)
    -> pack_end(butt4,0,1,1);
  
  refreshb -> signal_connect("clicked", load_headlines, 0);
  sett     -> signal_connect("clicked", f_open_settings, ([]));
  butt4    -> signal_connect("clicked", save_and_exit, 0);
  compressb    -> signal_connect("clicked", reorganize, 0);
  
  clist = GTK.SClist(7)->set_policy(GTK.PolicyAutomatic, GTK.PolicyAutomatic);
  clist -> set_column_title(0, " N ")
    -> set_column_width(0, 16)
    -> set_column_title(1, " U ")
    -> set_column_width(1, 16)
    -> set_column_title(2, " Title")
    -> set_column_width(2, 220)
    -> set_column_title(3, " Date")
    -> set_column_width(3, 110)
    -> set_column_title(4, " Author")
    -> set_column_width(4, 66)
    -> set_column_title(5, " Topic")
    -> set_column_width(5, 100)
    -> set_column_title(6, "#C")
    -> set_column_width(6, 20)
    -> set_column_justification(0, GTK.JUSTIFY_CENTER)
    -> set_column_justification(1, GTK.JUSTIFY_CENTER)
    -> set_column_justification(6, GTK.JUSTIFY_CENTER)
    -> column_title_active(1)
    -> column_titles_show()
    -> set_shadow_type(GTK.ShadowIn) 
    -> set_usize(600, 270)
    -> signal_connect("select_row", lambda(mixed q, object w, int row) {
				      if(selected != row  || 
					 !detail_child) {
					selected = row;
					f_view_details();
				      }
				    });
  clist -> signal_connect("button_press_event", f_button_press,0 );
  clist -> signal_connect("click_column", f_sort_headlines, 1);
  detail_frame = GTK.Vbox(0,0);
  //    -> set_shadow_type(GTK.ShadowNone);
  //    -> set_usize(600, 340);
  detail_urls = GTK.Frame();

  object vbox = GTK.Vpaned()
    -> set_gutter_size(4)
    -> set_handle_size(10)
    -> add1(GTK.Vbox(0,0) 
	    -> pack_start(clist, 1, 1, 0)
	    -> pack_start(mbuttonbox, 0, 0, 5))
    -> add2(GTK.Vbox(0,0)
	    ->pack_start(detail_frame, 1,1,1, 0)
	    -> pack_start(detail_urls, 0, 0, 0));
  main_window -> add(vbox) -> show_all() -> set_usize(630, 270);
  
  detail_frame -> hide();
  detail_urls -> hide()->set_shadow_type(GTK.ShadowNone);
  check =  GTK.Util.decode_image( MIME.decode_base64(check_image ));
  load_headlines();
  signal(signum("SIGALRM"), fetch_timeout);
  signal(signum("SIGINT"), save_and_exit);
  signal(signum("SIGHUP"), save_and_exit);
  signal(signum("SIGKILL"), save_and_exit);
  load_fonts();
  object tip = GTK.Tooltips()
    -> enable()
    -> set_tip(butt4, "Quit the Slashdot reader.")
    -> set_tip(compressb, "Optimize the database to save "
	       "space on the harddrive.")
    -> set_tip(refreshb, "Fetch the current headline list from slashdot.org.")
    -> set_tip(sett, "Change fonts and other options.");
return -1;
}


// HTML entities to characters
constant h2c =
([
  "&lt;":"<", "&gt;":">", "&amp;":"&",
  "&nbsp;": " ", "&iexcl;": "¡", "&cent;": "¢", "&pound;": "£",
  "&curren;": "¤", "&yen;": "¥", "&brvbar;": "¦", "&sect;": "§",
  "&uml;": "¨", "&copy;": "©", "&ordf;": "ª", "&laquo;": "«",
  "&not;": "¬",  "&quot;": "\"", "&shy;": "­", "&reg;": "®", "&macr;": "¯",
  "&deg;": "°",  "&plusmn;": "±", "&sup2;": "²", "&sup3;": "³",
  "&acute;": "´",  "&micro;": "µ", "&para;": "¶", "&middot;": "·",
  "&cedil;": "¸",  "&sup1;": "¹", "&ordm;": "º", "&raquo;": "»",
  "&frac14;": "¼",  "&frac12;": "½", "&frac34;": "¾", "&iquest;": "¿",
  "&Agrave;": "À",  "&Aacute;": "Á", "&Acirc;": "Â", "&Atilde;": "Ã",
  "&Auml;": "Ä",  "&Aring;": "Å", "&AElig;": "Æ", "&Ccedil;": "Ç",
  "&Egrave;": "È",  "&Eacute;": "É", "&Ecirc;": "Ê", "&Euml;": "Ë",
  "&Igrave;": "Ì",  "&Iacute;": "Í", "&Icirc;": "Î", "&Iuml;": "Ï",
  "&ETH;": "Ð",  "&Ntilde;": "Ñ", "&Ograve;": "Ò", "&Oacute;": "Ó",
  "&Ocirc;": "Ô",  "&Otilde;": "Õ", "&Ouml;": "Ö", "&times;": "×",
  "&Oslash;": "Ø",  "&Ugrave;": "Ù", "&Uacute;": "Ú", "&Ucirc;": "Û",
  "&Uuml;": "Ü",  "&Yacute;": "Ý", "&THORN;": "Þ", "&szlig;": "ß",
  "&agrave;": "à",  "&aacute;": "á", "&acirc;": "â", "&atilde;": "ã",
  "&auml;": "ä",  "&aring;": "å", "&aelig;": "æ", "&ccedil;": "ç",
  "&egrave;": "è",  "&eacute;": "é", "&ecirc;": "ê", "&euml;": "ë",
  "&igrave;": "ì",  "&iacute;": "í", "&icirc;": "î", "&iuml;": "ï",
  "&eth;": "ð",  "&ntilde;": "ñ", "&ograve;": "ò", "&oacute;": "ó",
  "&ocirc;": "ô",  "&otilde;": "õ",   "&ouml;": "ö", "&divide;": "÷",
  "&oslash;": "ø", "&ugrave;": "ù",  "&uacute;": "ú", "&ucirc;": "û",
  "&uuml;": "ü", "&yacute;": "ý",  "&thorn;": "þ", "&yuml;": "ÿ"
]); 
