<?xml version="1.0" encoding="windows-1252"?>
<node id="117372" title="Curses Chatterbox Client" created="2001-10-08 01:23:47" updated="2005-08-15 12:59:03">
<type id="1748">
sourcecode</type>
<author id="113194">
{NULE}</author>
<data>
<field name="doctext">
See also [http://www.nule.org/?tale=6] to download the latest and archival versions.
&lt;P&gt;
&lt;code&gt;
#! /usr/bin/perl -w
####################################################################
# ccb.pl - a Chatterbox client written in Perl Curses              #
####################################################################
# Abstract: ccb is a simple curses interface to the chatterbox     #
#         : feature of the PerlMonks.org website.                  #
#         : Some functionality is missing, but it is designed to   #
#         : to meet some very specific goals - and should be easy  #
#         : to enhance.                                            #
#                                                                  #
# Thanks  : Go to Shendal for his Perk/Tk client,                  #
#         : Zzamboni for the PerlMonks modules and the sample code #
#         : Vroom for creating PerlMonks.org                       #
#         : ybiC for saying "A curses CB client would be mondo++!" #
#         : and merlyn, for not turning me into a frog             #
#         : Lots of other people that deserve mention              #
#                                                                  #
# History : 20011004 - mbl - Initial version.                      #
#         : 20011006 - mbl - Some primitive functionality is here. #
#         : 20011007 - mbl - Created the chat buffer object.       #
#         :                - I guess I pretty much made the whole  #
#         :                - thing work - all options and stuff.   #
#         : 20011008 - mbl - Fixed a little big in the log buffer  #
#         :                - and some other screen bugs.           #
#         : 20011015 - mbl - Removed some of the lag by correcting #
#         :                - the timing and help in a window.      #
#         : 20011016 - mbl - Forking code added.                   #
#         : 20011018 - mbl - More things to make the behavior      #
#         :                - match the CB spec.                    #
#         : 20011021 - mbl - Added support for ignore/unignore     #
#         : 20011022 - mbl - Adjusted IPC timing to reduce errors. #
#         : 20011024 - mbl - Added support for timestamps.         #
#         : 20011025 - mbl - Added confirm odd message dialog.     #
#         : 20011026 - mbl - Added support to save message buffer. #
#         : 20011027 - mbl - Fixed a bug when the child lags.      #
#         :                - Some private message handling.        #
#         : 20011104 - mbl - Some more tweaks to IPC.              #
#                                                                  #
# To-do   : More fun stuff, like colors.                           #
#         : Get personal messages checkoff to work.                #
#         : Improvements to the IPC performance might be possible. #
#         : I *still* like the idea of a PM minibrowser...         #
####################################################################
# Boring, pointless crap:                                          #
# This is free software that may be distributed under the same     #
# license as Perl itself.  This software comes with no warranty.   #
#                                                                  #
# I suppose that it's Copyright (C) 2001 M. Litherland             #
#                                                                  #
# Get the latest version from http://www.nule.org/ which is also   #
# where the author can be reached.                                 #
####################################################################
my $VERSION = "1.0pre2a"; 

# change your @INC if you have weird module locations.
use lib "/home/litherm/lib/perl5/site_perl/5.6.1/PerlMonksChat2";

use strict;
use Curses;
use Curses::Widgets;
use IO::Handle qw(autoflush);
use Socket;
use Text::Wrap;

# PM Modules
use PerlMonks::Chat;
use PerlMonks::Users;

# Record separator for IPC
use constant RSEPARATOR =&gt; "MiKeIsDaUbErCoDeR"; #:)

#############
# Variables #
#############

# Widgets
my ($main, $input, $display, $dialog);

# Variables for dealing with curses widgets
my ($text, $test);

# Handles for our PM objects
my ($chat, $monks, $buffer, $pid);

# More variables
my ($username, $password);

###########################
# Initialize chat package #
###########################

# I'm a newbie when it comes to OO Perl too - be gentle...
{ package Chat;

# Object constructor
sub new
{
	my $self = {};
	shift; # Package name

	# Counter for updating the buffer
	$self-&gt;{COUNTER}   = time();
	$self-&gt;{MCOUNTER}  = 20; # Max time

	# Flag for time stamps
	$self-&gt;{TIMESTAMP} = 1;  # 0 is off

	# Variables for working with the buffer
	$self-&gt;{POSITION}  = 0;
	$self-&gt;{LENGTH}    = 500;
	$self-&gt;{BUFFER}    = []; # Message buffer

	bless($self);
	return $self;
}

# Provide a method for changing the buffer size
sub length 
{
	my $self = shift;
	
	if (@_)
	{
		$self-&gt;{LENGTH} = shift;
	}

	return $self-&gt;{LENGTH};
}

# Provide a method for toggling the time stamp
sub show_time
{
	my $self = shift;
	
	if (@_)
	{
		$self-&gt;{TIMESTAMP} = shift;
	}

	return $self-&gt;{TIMESTAMP};
}

# A simple counter to set the chat request interval.
# Passing a value instead sets the Max Counter interval,
# returns the time remaining (0 if done);
sub counter
{
	my $self = shift;
	my $delta;
	
	if (@_)
	{
		$self-&gt;{MCOUNTER} = shift;
	}

	$delta = ($self-&gt;{COUNTER} + $self-&gt;{MCOUNTER}) - time();

	if ($delta &lt;= 0)
	{
		$self-&gt;{COUNTER} = time();

		return 0;
	}
	else
	{
		return $delta;
	}
}

# Add lines into the scrollback buffer
sub addlines
{
	my $self = shift;
	my $line;

	if (@_)
	{
		foreach $line (@_)
		{
			# Attempt to remove empty lines.
			next if ($line =~ /^\s*$/);

			# Add the line in the next available position
			# in the buffer
			$self-&gt;{BUFFER}[$self-&gt;{POSITION}] = $line;

			# If we are at the last available position in
			# the buffer, reset and start over.
			if ($self-&gt;{POSITION} &gt;= $self-&gt;{LENGTH})
			{
				$self-&gt;{POSITION} = 0;
			}
			else
			{
				$self-&gt;{POSITION}++;
			}
		}
	}
}

# Return an array with the number of lines requested.
sub getlines
{
	my $self = shift;
	my ($rows, $i, $position);
	my (@return);

	if (@_)
	{
		$rows = shift;
		# POSITION is one past the current line (ahem, bug? :-)
		$position = $self-&gt;{POSITION} - $rows - 1;
		$position += $self-&gt;{LENGTH} if ($position &lt; 0);

		for ($i = 0; $i &lt; $rows; $i++)
		{
			# Set the position ahead, but rewind if hit the
			# end.
			$position++;
			$position = 0 if ($position &gt;= $self-&gt;{LENGTH});

			# When first initialized Much of the buffer may
			# be empty.
			if (defined($self-&gt;{BUFFER}[$position]))
			{
				push @return, $self-&gt;{BUFFER}[$position];
			}
			else
			{
				push @return, "";
			}
		}
	}

	return @return;
}

} # Back to the main package

################
# Main Routine #
################

# Create a new main window
$main = new Curses;

# Configure some options
noecho();         # Disable echoing from the console.
$main-&gt;keypad(1); # Map special keys.
halfdelay(10);    # This works like Poll when a function is passed
                  # to a widget. Redraws the chat window every 2
		  # seconds, and gets new messages per /freq
$main-&gt;erase();
select_colour($main, 'black');
$main-&gt;attrset(0);

&amp;header();

# Prompt for username and pass
($text, $test) = input_box(
	'title' =&gt; "Logon",
	'prompt' =&gt; "Enter your PM username",
	'border' =&gt; "white",
	'cursor_disable' =&gt; 1,
	'function' =&gt; \&amp;header
);

if (($test == 1) &amp;&amp; ($text ne ""))
{
	$username = $text;
}
else
{
	&amp;handle_error("Username required and not provided");
}

($text, $test) = input_box(
	'title' =&gt; "Logon",
	'prompt' =&gt; "Enter your PM password",
	'border' =&gt; "white",
	'cursor_disable' =&gt; 1,
	'password' =&gt; 1,
	'function' =&gt; \&amp;header
);

if (($test == 1) &amp;&amp; ($text ne ""))
{
	$password = $text;
}
else
{
	&amp;handle_error("Password required and not provided");
}

# Get rid of the login boxes while we wait
$main-&gt;erase();
$main-&gt;addstr(0, 1, "Attempting to log in, please wait...");
$main-&gt;refresh();

# Log in (all your password are belong to us)
$chat = PerlMonks::Chat-&gt;new();
$chat-&gt;add_cookies;

$chat-&gt;login($username, $password)
	or &amp;handle_error("Could not login: $!");

$monks = PerlMonks::Users-&gt;new();
$monks-&gt;add_cookies;

# Produce a child to handle the transactions.
$main-&gt;addstr(1, 1, "Starting child process...");
$main-&gt;refresh();

# Create a socketpair
socketpair(CHILD, PARENT, AF_UNIX, SOCK_STREAM, PF_UNSPEC)
	or &amp;handle_error("Socket could not be created.");
CHILD-&gt;autoflush(1);
PARENT-&gt;autoflush(1);

# Fork here
if ($pid = fork())
{
	close PARENT;

	# Create a chat buffer
	$buffer = new Chat;
	print CHILD $buffer-&gt;counter . "\n";
}
else
{
	close CHILD;

	&amp;child();

	exit 0;
}

# Clear the screen
$main-&gt;erase();
$main-&gt;refresh();

# From now on the &amp;message subroutine and the chat $buffer handle
# all screen draws.
$buffer-&gt;addlines("{CCB} - Curses Chatterbox client, Type '/help' for available commands.");

# Main loop
while (1)
{
	($test, $text) = txt_field(
		'window' =&gt; $main,
		'regex' =&gt; "\n",
		'xpos' =&gt; 0,
		'ypos' =&gt; $LINES - 3,
		'lines' =&gt; 1,
		'cols' =&gt; $COLS - 2,
		'border' =&gt; "white",
		'cursor_disable' =&gt; 1,
		'function' =&gt; \&amp;messages
	);

	chomp $text;
	
	# Begin processing the message entered
	if ($text =~ /^\/help/i)
	{
		&amp;help_popup($buffer);
	}
	elsif ($text =~ /^\/who/i)
	{
		&amp;list_users($buffer);
	}
	elsif ($text =~ /^\/xp/i)
	{
		&amp;show_xp($buffer);
	}
	elsif ($text =~ /^\/log/i)
	{
		&amp;show_log($buffer, "SHOW");
	}
	elsif ($text =~ /^\/save/i)
	{
		&amp;show_log($buffer, "SAVE");
	}
	elsif ($text =~ /^\/msgs/i)
	{
		&amp;show_msgs($buffer);
	}
	#elsif ($text =~ /^\/(?:checkoff|co)\s+/i)
	#{
		# Don't quite have this figured out...
	#}
	elsif ($text =~ /^\/freq\s*(\d*)/i)
	{
		# This is used to set an alarm and shouldn't be
		# too small.
		my $min = 5;
		if ($1 &gt; $min)
		{
			$buffer-&gt;counter($1);
			$buffer-&gt;addlines("{CCB} - Interval set to $1");
		}
		else
		{
			$buffer-&gt;addlines("{CCB} - Interval must be greater than $min");
		}
	}
	elsif ($text =~ /^\/time/i)
	{
		if ($buffer-&gt;show_time)
		{
			$buffer-&gt;show_time(0);
			$buffer-&gt;addlines("{CCB} - Time stamps disabled");
		}
		else
		{
			$buffer-&gt;show_time(1);
			$buffer-&gt;addlines("{CCB} - Time stamps enabled");
		}
	}
	elsif ($text =~ /^\/(?:msg|tell)\s+(\S+)\s+(.+)$/i)
	{
		# A private message is being sent
		$chat-&gt;send($text);

		$buffer-&gt;addlines(split "\n", wrap("", "\t", "{CCB} - private message sent to $1: $2"));
	}
	elsif ($text =~ /^\/(ignore|unignore)\s+(\S+)/i)
	{
		my $action = lc($1);
		my $user = $2;

		$action =~ s/.$/ing/;

		# A private message is being sent
		$chat-&gt;send($text);

		$buffer-&gt;addlines(split "\n", wrap("", "\t", "{CCB} - $action user $user"));
	}
	elsif ($text =~ /^\/me\s*/i)
	{
		# /me emotes.
		$chat-&gt;send($text);
	}
	elsif ($text =~ /^\/quit/i)
	{
		# Ask the child to exit then quit.
		$main-&gt;erase();
		$main-&gt;addstr(0, 1, "Ending child process...");
		$main-&gt;refresh();

		# Code to exit.
		print CHILD "-1\n";

		waitpid($pid, 0);
		exit 0;
	}
	elsif (($text =~ /^\s{0,1}\//) || ($text =~ /^.{0,2]msg/i))
	{
		# All valid forms of commands are accounted for,
		# here we can handle near misses.
		
		($text, $test) = input_box(
			'title' =&gt; "Vague response",
			'prompt' =&gt; "Please confirm, cancel or change your message:",
			'border' =&gt; "white",
			'content' =&gt; "$text",
			'cursor_disable' =&gt; 1
		);

		if (($test == 1) &amp;&amp; ($text ne ""))
		{
			# Text was confirmed to send.
			$chat-&gt;send($text);
		}
		else
		{
			$buffer-&gt;addlines("{CCB} - message not sent");
		}
	}
	else
	{
		# Try to send whatever message was entered.
		if ($text ne "")
		{
			$chat-&gt;send($text);
		}
	}
}

END
{
	# Ask Curses to exit cleanly.
	endwin();
}

exit 0;

###############
# Subroutines #
###############

sub header
{
	# Display a nice header whilst we perform various tasks
	$main-&gt;standout();
	$main-&gt;addstr(0, 1, "{CCB} - Curses ChatterBox Client by {NULE} v. $VERSION");
	$main-&gt;standend();
	$main-&gt;refresh();
}

sub child
{
	my ($request, $text);

	# Listen for a timing value from the parent.
	# Request the resource from
	while (1)
	{
		chomp($request = &lt;PARENT&gt;);

		if ($request == -1)
		{
			# We have been asked to exit
			return 1;
		}
		else
		{
			# The parent is expecting a response
			# in this many seconds or we will
			# send a lag error message
			eval
			{
				local $SIG{ALRM} = sub { die "ALARM\n" };
				alarm ($request - 1);
				$text = join "\n", $chat-&gt;getnewlines(1);
				alarm 0;
			};

			if ($@)
			{
				# Timeouts are different from other problems
				print PARENT "{CCB} - ERROR with child: $@" unless $@ eq "ALARM\n";
				# Request timed out
				print PARENT "{CCB} - Lag problem, try setting /freq higher..."
					. RSEPARATOR;
			}
			else
			{
				print PARENT "$text" . RSEPARATOR;
			}
		}
	}
}

sub messages
{
	# Note that our handles are global here because Curses::Widgets 
	# doesn't seem amenable to passing arguments ($buffer, $main)
	my ($chat, @chat, @prechat, $line, $length);
	my $i = 0;

	if ($buffer-&gt;counter == 0)
	{
		local $/ = RSEPARATOR;

		eval
		{
			local $SIG{ALRM} = sub { die "ALARM\n" };
			alarm 2;
			chomp ($chat = &lt;CHILD&gt;);
			alarm 0;
		};

		if ($@)
		{
			$buffer-&gt;addlines("{CCB} - ERROR with parent: $@") unless $@ eq "ALARM\n";
			$buffer-&gt;addlines("{CCB} - May have lost sync with child. (1)");
		}

		eval
		{
			local $SIG{ALRM} = sub { die "ALARM\n" };
			alarm 2;
			print CHILD $buffer-&gt;counter . "\n";
			alarm 0;
		};

		if ($@)
		{
			$buffer-&gt;addlines("{CCB} - ERROR with parent: $@") unless $@ eq "ALARM\n";
			$buffer-&gt;addlines("{CCB} - May have lost sync with child. (2)");
		}

		@chat = map { "$_\n" } split "\n", $chat;

		$Text::Wrap::columns = $COLS - 2;

		foreach $line (@chat)
		{
			if ($buffer-&gt;show_time)
			{
				$line = &amp;time_stamp . " $line";
			}

			$buffer-&gt;addlines(split "\n", wrap("", "\t", $line));
		}
	}

	@chat = $buffer-&gt;getlines($LINES - 3);

	# Start drawing from the bottom of the screen
	$i = $LINES - 4;
	foreach $line (reverse @chat)
	{
		# Not the best way to do this, but length()
		# doesn't handle tabs correctly. # TODO? #
		$line =~ s/^\t/        /;
		$length = $COLS - length($line) - 2;

		$line = " ".$line." "x$length;
		$main-&gt;addstr($i, 0, "$line");

		$i--;

		last if $i &lt; 0;
	}
	
	$main-&gt;refresh();

	return 1;
}

sub help_popup
{
	# Display a window with help information in it.
	my $buffer = shift;
	my ($line, $help);

	my @help = (
		"Curses ChatterBox Client v. $VERSION",
		" ",
		"Commands available:",
		"- /help     - displays this message",
		"- /who      - shows the monks logged on",
		"- /xp       - shows some quick information about you",
		"- /log      - show the chat log",
		"- /freq &lt;#&gt; - sets the refresh interval to # seconds",
		"- /time     - toggles time stamps on messages",
		"- /save     - dump buffer to a save file",
		"- /quit     - exits the Chatterbox client",
		"Supported Chatterbox Commands",
		"- /me ...          - emote a message.",
		"- /msg &lt;user&gt; ...  - send a message to &lt;user&gt;.",
		"- /tell &lt;user&gt; ... - same thing.",
		"- /msgs            - show all private messages.",
		#"- /co ###&lt;,###...&gt; - check off private messages.",
		#"- /checkoff ###    - same thing.",
		"- /ignore &lt;user&gt;   - Ignore &lt;user&gt;.",
		"- /unignore &lt;user&gt; - Unignore &lt;user&gt;.",
		"To-do:",
		"- Colors",
		"- I keep having imaginings of building a mini-browser in...",
		"  if winamp can do it, so can I!",
		"- Tell me what features you would like!"
	);

	foreach $line (@help)
	{
		chomp $line;

		if ($line ne "")
		{
			$help .= "$line\n";
		}
	}

	txt_field(
		'window' =&gt; $main,
		'title' =&gt; "{CCB} Help - PGUP and PGDOWN to scroll, ENTER to exit.",
		'regex' =&gt; "\n",
		'xpos' =&gt; 0,
		'ypos' =&gt; 0,
		'lines' =&gt; $LINES - 2,
		'cols' =&gt; $COLS - 2,
		'border' =&gt; "white",
		'edit' =&gt; 0,
		'cursor_disable' =&gt; 1,
		'content' =&gt; $help
	);

	# Clear the screen before returning.
	$main-&gt;erase();
	$main-&gt;refresh();
}

sub list_users
{
	my $buffer = shift;
	my (%users, $users);

	%users = $monks-&gt;users;

	if (%users)
	{
		$Text::Wrap::columns = $COLS - 2;

		$users  = "{CCB} - " . (scalar(keys(%users)) + 1);
		$users .= " users logged in: " . join " ",sort keys(%users);
		$buffer-&gt;addlines(split "\n", wrap("", "\t", $users));
	}
	else
	{
		$buffer-&gt;addlines("No users logged in (oddly enough)");
	}
}

sub show_msgs
{
	# Retrieve and display all personal messages
	my $buffer = shift;
	my (%msgs, $msg);

	%msgs = $chat-&gt;personal_messages;

	if (%msgs)
	{
		$Text::Wrap::columns = $COLS - 2;

		foreach $msg (sort keys(%msgs))
		{
			$buffer-&gt;addlines(split "\n", wrap("", "\t", "($msg) $msgs{$msg}"));
		}
	}
	else
	{
		$buffer-&gt;addlines("Could not get personal messages");
	}
}

sub show_xp
{
	# Print a display of current XP information
	my $buffer = shift;
	my (%xp, $xp);

	%xp = $monks-&gt;xp;

	if (%xp)
	{
		$Text::Wrap::columns = $COLS - 2;

		$xp  = "{CCB} - User: $xp{user}, Level: $xp{level}, XP: $xp{xp}, ";
		$xp .= "Votes: $xp{votesleft}, Next level in: $xp{xp2nextlevel} XP";
		$buffer-&gt;addlines(split "\n", wrap("", "\t", $xp));
	}
	else
	{
		$buffer-&gt;addlines("Could not get XP information");
	}
}

sub show_log
{
	my ($buffer, $mode) = @_;
	my (@chat, $line, $chat, $length, $filename);

	# If mode is SAVE then prompt for a file name
	if ($mode eq "SAVE")
	{
		my ($text, $test) = input_box(
			'title' =&gt; "Save log",
			'prompt' =&gt; "Please enter a path and filename to save your message:",
			'border' =&gt; "white",
			'edit' =&gt; 0,
			'cursor_disable' =&gt; 1
		);

		if (($test == 1) &amp;&amp; ($text ne ""))
		{
			$filename = $text;
		}
		else
		{
			$buffer-&gt;addlines("{CCB} - log not saved.");
			return 0;
		}
	}

	$length = $buffer-&gt;length;

	# Retrieve all the lines.
	@chat = $buffer-&gt;getlines($length);
	$chat = "";
	foreach $line (@chat)
	{
		chomp $line;

		if ($line ne "")
		{
			$chat .= "$line\n";
		}
	}

	if ($mode eq "SAVE")
	{
		open (FILEHANDLE, "&gt;$filename");
		print FILEHANDLE $chat;
		close FILEHANDLE;
		$buffer-&gt;addlines("{CCB} - log saved as $filename");
	}
	else
	{
		txt_field(
			'window' =&gt; $main,
			'title' =&gt; "{CCB} Chat log - PGUP and PGDOWN to scroll, ENTER to exit (max $length lines).",
			'regex' =&gt; "\n",
			'xpos' =&gt; 0,
			'ypos' =&gt; 0,
			'lines' =&gt; $LINES - 2,
			'cols' =&gt; $COLS - 2,
			'border' =&gt; "white",
			'cursor_disable' =&gt; 1,
			'content' =&gt; $chat
		);
	}

	# Clear the screen before returning.
	$main-&gt;erase();
	$main-&gt;refresh();
}

sub time_stamp
{
	# Produce a time stamp
	my ($sec, $min, $hour) = localtime;

	$sec  = "0".$sec if $sec &lt; 10;
	$min  = "0".$min if $min &lt; 10;
	$hour = "0".$hour if $hour &lt; 10;
	return ("$hour:$min:$sec")
}

sub handle_error
{
	my $message = shift;

	msg_box(
		'message' =&gt; "$message",
		'title' =&gt; "Error"
	);

	die "$message\n";
}
&lt;/code&gt;</field>
<field name="codedescription">
This is a client for the PerlMonks chatterbox which uses [ZZamboni]'s PerlMonksChat2 module.  You will also need Curses and Curses::Widgets.
&lt;P&gt;
Supports most basic PerlMonksChat features with a primitive help system (type /help for all commands).  Also uses a configurable buffer size and opens a separate sub-window to scroll through the old messages (type /log to see what I mean).  I want to add more fun features, so tell me what you want to see!
&lt;P&gt;
Note that it doesn't use colors in this version, because I want it to be "non-descript" on my desktop.  I also want it to be very simple so it is not nearly as fancy as some of the excellent clients out there.
&lt;P&gt;
Let me know what you think!&lt;BR&gt;
{NULE}
&lt;P&gt;
&lt;B&gt;Todo:&lt;/B&gt;&lt;BR&gt;
- Do some name high-lighting and other niceties.&lt;BR&gt;
- The way PerlMonksChat2 works this &lt;em&gt;requires&lt;/em&gt; you to have a ~/.netscape/cookies file. Must fix this behaviour.&lt;BR&gt;
- Must finish the personal messages handling as well.&lt;BR&gt;
&lt;P&gt;
&lt;B&gt;Update:&lt;/B&gt;&lt;BR&gt;
20011007 - There was a request to actually post the code here.  I will do so, but prefer that you get it from nule.org - it will be updated there first.
&lt;P&gt;
20011009 - Small bug squished where the buffer would display the line about to be deleted (which could be hours old). That's fixed, and some other visual stuff is, but I think you may see a line doubled when the end of the buffer is reached.  I'm off to Atlanta for a week, I'll fix it when I get back.
&lt;P&gt;
20011010 - Figured out the "Lag" Problem, but can't fix it until the 15th.  Curses::Widgets handles keystrokes differently than I thought.  For a workaround set /freq to something very high, like /freq 200
&lt;P&gt;
20011015 - The "Lag" problem is squished with a timer that uses "time()" not callback counts.  I still want to use "fork()" and do this the right way.  By the way, if you have trouble with the current version, my site has all previous versions.  Update - /me is fixed now.
&lt;P&gt;
20011022 - The lag is gone for good now with a version that forks! I've been testing this for about a week so I have confidence that it works well.  I could see this more complicated version not running on a system where it ran before so I'll keep a non-forking version on my web site.
&lt;P&gt;
20011031 - Fixed a bug with IPC.  Added timestamps, save chat buffer to a file, some handling private messages, and ccb will now try to guess when you have mis-typed a command and give you a chance to retype it before submitting.  The ability to delete personal messages will be added soon.
&lt;P&gt;
20011105 - More tweaks to IPC.  Hopefully it will work better with 5.6.0 now.</field>
<field name="codecategory">
Chatterbox Clients</field>
<field name="codeauthor">
{NULE}</field>
</data>
</node>
