Index: trunk/hiptop/cognet++/LICENSE
===================================================================
--- trunk/hiptop/cognet++/LICENSE	(revision 210)
+++ trunk/hiptop/cognet++/LICENSE	(revision 210)
@@ -0,0 +1,32 @@
+/*
+** Copyright 2003, Daniel Grobe Sachs
+** Copyright 2003-2005, Nicholas Riley
+** Copyright 2003, Jake Bordens
+** Copyright 2003, Leigh Klotz
+** Copyright 2002-2003, Brian J. Swetland
+** Copyright (c) 2002, 2003 Danger, Inc.
+** All rights reserved.
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions
+** are met:
+** 1. Redistributions of source code must retain the above copyright
+**    notice, this list of conditions, and the following disclaimer.
+** 2. Redistributions in binary form must reproduce the above copyright
+**    notice, this list of conditions, and the following disclaimer in the
+**    documentation and/or other materials provided with the distribution.
+** 3. The name of the authors may not be used to endorse or promote products
+**    derived from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
+** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+** IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
Index: trunk/hiptop/cognet++/README.txt
===================================================================
--- trunk/hiptop/cognet++/README.txt	(revision 210)
+++ trunk/hiptop/cognet++/README.txt	(revision 210)
@@ -0,0 +1,106 @@
+Cognet++ is a series of improvements made to Cognet including:
+
+- Smiley support.  /emoticons off|on to toggle.
+
+- Leigh Klotz's auto reconnect
+
+- On reconnect, show the window you were talking on rather than %server
+
+- if a new window is created while your in the app, it will display
+  a bubble notification
+
+- new messages in any window are indicated by a cog icon in the space
+  where the email notification goes
+
+- a new command /marquee <seconds> will allow you to set a window to 
+  display a marquee when your not in the application.  The seconds paramenter 
+  indicates the limit on how often a marquee is displayed.  for example /marquee 
+  30 will only display 1 marquee every 30 seconds.  Messages in between will be 
+  dropped.  /marquee 0 will disable marquees.
+
+- font support. see http://developer.danger.com/wiki/space/fonts for a list 
+  of fonts.  /font <fontname> is the command. 
+
+Enjoy,
+Jake Bordens
+jake@twodot.org
+
+***** Below is the original cognet client README file:
+
+Please direct any questions about the cognet client and server to <sachs@uiuc.edu>. 
+
+Cognet Chat Engine  v0.1
+------------------------
+
+This application is a front-end for a irc proxy server called cognetd.
+The server handles virtually all of the logic and all the client knows
+about is how to display messages in windows and send user commands to
+the server.
+
+The server caches all messages even if the client is not connected.
+
+The following commands are understood by the client:
+
+/query <name>    creates a new window named <name> and shows it
+/win <name>      switch to window <name> if it exists
+/zap             log out of the server
+/bind <number>   binds a quickkey (0-9) to this window
+/clear           erases the contents of the current window
+                 (on the server as well as the client)
+
+Any other commands are sent to the server for processing.
+
+The application menu lists all active windows (windows with
+messages that have arrived since you were there last are marked
+with an arrow glyph).  There are a few handy shortcuts not shown
+in the menu (for space reasons):
+
+MENU-n          cycle to the next window
+MENU-p          cycle to the previous window
+MENU-,          jump to a window with new messages
+
+Currently, server messages show up in the special "%server" window
+(which you can never delete). Modules running on the server create
+windows of their own. 
+
+Flags to the side of the display indicate traffic in other windows.
+Indicators like "[3]" mean that there is a new message in a window bound
+to the number in the brackets.	 A "[?]" indicator means that there are
+new messages in some unbound window (check the menu for details).
+
+Protocol Details
+----------------
+
+client protocol,  server->client:
+  <serial>:<tag>:<source>:<target>:<text>
+
+The first field (serial number) may be omitted or ignored.  It allows
+for resynchronization, so the client should keep track of the last
+serial number it has seen for when it reconnects
+
+  #:dmsg:nick:#channel:text          # display message in target window
+  #:smsg:nick:yournick:text          # display message in source window
+  #:dmsg:yournick:nick:text          # display message in target window
+  #:dmsg/act:nick:#channel:text      # display emote'd message in target window
+  #:clear:window:                    # erase all lines from window
+  #:info:window:text                 # display server message
+  #:fail:window:text                 # display error message
+  #:repl:window:text                 # display command reply
+  #:sync:token:                      # client should keep token for next serial
+
+special windows:
+  %server -- messages from the server
+
+client protocol,  client->server:
+  <serial>:<tag>:<target>:<text>
+
+  :serial:serialnumber:token         # indicates last serial number and token seen
+                                     # (send before logging in)
+  :auth:username:password            # log in
+
+  In all of these, 'ctxt' is the current window the command
+  was typed in.  Lines without a leading '/' get transformed into
+  a send command, all others lose the '/' and are split so that the
+  text between the '/' and the first whitespace become the 'tag',
+  the current window is the 'ctxt', and the rest of the line is the
+  'text'
Index: trunk/hiptop/cognet++/SAMPLE_CODE_LICENSE
===================================================================
--- trunk/hiptop/cognet++/SAMPLE_CODE_LICENSE	(revision 210)
+++ trunk/hiptop/cognet++/SAMPLE_CODE_LICENSE	(revision 210)
@@ -0,0 +1,34 @@
+   This  Software  is  subject  to  the  Danger,  Inc.  Developer License
+   Agreement,  a copy  of which is available by from developer.danger.com
+   In  addition to the rights granted in the Developer License Agreement,  
+   Danger  grants  you  the right to distribute this particular file under 
+   the following terms:
+
+   Copyright (c) 2002, 2003 Danger, Inc.
+   All rights reserved.
+
+   Redistribution  and  use  in  source and binary forms, with or without
+   modification, are permitted provided that the following conditions are
+   met:
+     * Redistributions  of  source  code  must retain the above copyright
+       notice, this list of conditions and the following disclaimer.
+     * Redistributions  in binary form must reproduce the above copyright
+       notice,  this  list  of conditions and the following disclaimer in
+       the   documentation  and/or  other  materials  provided  with  the
+       distribution.
+     * Danger, Inc.'s name may not be used to endorse or promote products
+       derived   from   this  software  without  specific  prior  written
+       permission.
+
+   THIS  SOFTWARE  IS PROVIDED BY DANGER, INC. "AS IS" AND ANY EXPRESS OR
+   IMPLIED  WARRANTIES,  INCLUDING,  BUT  NOT  LIMITED  TO,  THE  IMPLIED
+   WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+   DISCLAIMED.  IN  NO EVENT SHALL DANGER, INC. BE LIABLE FOR ANY DIRECT,
+   INDIRECT,  INCIDENTAL,  SPECIAL,  EXEMPLARY,  OR CONSEQUENTIAL DAMAGES
+   (INCLUDING,  BUT  NOT  LIMITED  TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+   SERVICES;  LOSS  OF  USE,  DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+   HOWEVER  CAUSED  AND  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+   STRICT  LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+   IN  ANY  WAY  OUT  OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+   POSSIBILITY OF SUCH DAMAGE.
+
Index: trunk/hiptop/cognet++/build.xml
===================================================================
--- trunk/hiptop/cognet++/build.xml	(revision 210)
+++ trunk/hiptop/cognet++/build.xml	(revision 210)
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+
+<project name="cognet" default="run">
+    <property environment="env"/>
+    <property name="DANGER_HOME"	value="${env.DANGER_HOME}"/>
+
+    <property name="APP_CLASS_NAME"	value="Cognet"/>
+    <property name="APP_PACKAGE_NAME"	value="org.twodot.cognet"/>
+    <property name="LOCALE"		value="en_US"/>
+
+    <import file="${DANGER_HOME}/tools/build_common.xml"/>
+</project>
Index: trunk/hiptop/cognet++/locale/default/assets/default/license.txt
===================================================================
--- trunk/hiptop/cognet++/locale/default/assets/default/license.txt	(revision 210)
+++ trunk/hiptop/cognet++/locale/default/assets/default/license.txt	(revision 210)
@@ -0,0 +1,17 @@
+This Software is subject to the Danger, Inc. Developer License Agreement, a copy of which is available from developer.danger.com.
+In addition to the rights granted in the Developer License Agreement, Danger grants you the right to distribute this particular file under the following terms:
+
+Copyright 2003, Jake Bordens
+Copyright 2003, Leigh Klotz
+Copyright 2003-2005, Nicholas Riley
+Copyright 2003, Daniel Grobe Sachs
+Copyright 2002-2003, Brian J. Swetland
+Copyright (c) 2002, 2003 Danger, Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+ * Danger, Inc.'s name may not be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY DANGER, INC. AND AUTHORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DANGER, INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Index: trunk/hiptop/cognet++/locale/en_US/cognet.rsrc
===================================================================
--- trunk/hiptop/cognet++/locale/en_US/cognet.rsrc	(revision 210)
+++ trunk/hiptop/cognet++/locale/en_US/cognet.rsrc	(revision 210)
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+package "org.twodot.cognet"
+
+interface "{source}org/twodot/cognet/Resources.java"
+events "{source}org/twodot/cognet/Events.java"
+
+string ID_APP_ID "Cognet Plus"
+string ID_APP_NAME "Cognet++"
+string ID_APP_CLASS "org.twodot.cognet.Cognet"
+
+string ID_LOCALE "en_US"
+
+bitmap ID_LARGE_ICON "{base}icon_large.png"
+bitmap ID_SMALL_ICON "{base}icon_small.png"
+bitmap ID_SPLASH_SCREEN "{base}s4x.png"
+
+bitmap kBlank "{base}blank.png"
+bitmap kMarker "{base}marker.png"
+bitmap kTag "{base}tag.png"
+bitmap kPending "{base}icons/cog.png"
+
+bitmapArray k11x11Icons
+    "{base}icons/innocent.png"
+    "{base}icons/smiling.png"
+    "{base}icons/frowning.png"
+    "{base}icons/winking.png"
+    "{base}icons/sticking-out-tongue.png"
+    "{base}icons/surprised.png"
+    "{base}icons/kissing.png"
+    "{base}icons/yelling.png"
+    "{base}icons/cool.png"
+    "{base}icons/money-mouth.png"
+    "{base}icons/foot-in-mouth.png"
+    "{base}icons/embarassed.png"
+    "{base}icons/indifferent.png"
+    "{base}icons/crying.png"
+    "{base}icons/lips-are-sealed.png"
+    "{base}icons/laughing.png"
+    "{base}icons/cog.png"
+    "{base}icons/irc.png"
+    "{base}icons/flag.png"
+    "{base}icons/info.png"
+    "{base}icons/bullet.png"
+endBitmapArray
+
+bitmapArray k5x6Icons
+    "{base}icons/small-innocent.png"
+    "{base}icons/small-smiling.png"
+    "{base}icons/small-frowning.png"
+    "{base}icons/small-winking.png"
+    "{base}icons/small-sticking-out-tongue.png"
+    "{base}icons/small-surprised.png"
+    "{base}icons/small-kissing.png"
+    "{base}icons/small-yelling.png"
+    "{base}icons/small-cool.png"
+    "{base}icons/small-money-mouth.png"
+    "{base}icons/small-foot-in-mouth.png"
+    "{base}icons/small-embarassed.png"
+    "{base}icons/small-indifferent.png"
+    "{base}icons/small-crying.png"
+    "{base}icons/small-lips-are-sealed.png"
+    "{base}icons/small-laughing.png"
+    "{base}icons/small-cog.png"
+    "{base}icons/small-irc.png"
+    "{base}icons/small-flag.png"
+    "{base}icons/small-info.png"
+    "{base}icons/small-bullet.png"
+endBitmapArray
+
+
+data compress kLicense 257 "{base}../license.txt"
+
+dialog kAboutDialog
+	title = "About Cognet++"
+	doneEvent = kID_DummyEvent
+	doneIsDefault
+	width = 240
+
+	scrollView
+		id = kID_AboutScroller
+    		alignWithParentTop = 0
+		alignWithParentLeft = 0
+		width = 228
+		height = 110
+		initialFocus
+
+		staticTextBox
+			id = kID_AboutText
+			alignWithParentTop = 0
+			alignWithParentLeft = 3
+			fillToRight = 0
+			doesNotHaveBorder
+	endScrollView
+endDialog
Index: trunk/hiptop/cognet++/org/twodot/cognet/ChatHistory.java
===================================================================
--- trunk/hiptop/cognet++/org/twodot/cognet/ChatHistory.java	(revision 210)
+++ trunk/hiptop/cognet++/org/twodot/cognet/ChatHistory.java	(revision 210)
@@ -0,0 +1,393 @@
+/*
+ * cognet chat app
+ *
+ * Copyright 2003, Brian Swetland <swetland@frotz.net>
+ * See LICENSE for redistribution terms
+ *  
+ * Derived from:
+ *
+ * @(#)ChatHistory.java	1.0 01/03/23
+ *
+ * Copyright 2000 by Danger Research, Inc.
+ * 165 University Avenue, Palo Alto, CA  94301
+ * All rights reserved.
+ * See SAMPLE_CODE_LICENSE for redistribution terms.
+ *
+ */
+
+package org.twodot.cognet;
+
+import danger.ui.*;
+import danger.util.LineBreaker;
+import java.util.Enumeration;
+
+public class ChatHistory extends View
+{
+	public
+	ChatHistory(int inSizeLimit, Engine e)
+	{
+	Rect	r;
+	
+		mPlainFont = Font.findSystemFont();
+		mBoldFont = Font.findSystemFont();
+
+		mAscent = mBoldFont.getAscent() + mBoldFont.getDescent();
+		mLineHeight = mAscent + kLeading - 2;
+
+		mLimit = inSizeLimit;
+
+		setSize(20, 10);
+		engine = e;
+
+		tag_bitmap = engine.app.bm_tag;
+		tag_width = tag_bitmap.getWidth();
+		tag_height = tag_bitmap.getHeight();
+		tag_font = Font.findFont("Bort7");
+	}
+
+	Bitmap tag_bitmap;
+	int tag_width, tag_height;
+	Font tag_font;
+	
+
+	public void
+            paint(Pen inPen)
+	{
+
+		Rect		r = getBounds();
+		int			i, x, y;
+		ChatLine	index = mBottomDisplayLine;
+	
+		//*	Clear the area
+		if(mCurrentSelection != 0){
+			inPen.setColor(Color.GRAY10);
+		} else {
+			inPen.setColor(Color.WHITE);
+		}
+		inPen.fillRect(r);
+		
+		inPen.setColor(Color.BLACK);
+
+		x = r.left;
+		y = mLineHeight * (mNumDisplayLines - 1);
+		y += mAscent;
+		y += r.top;
+
+        SmileyEngine smiley = new SmileyEngine(mPlainFont);
+        inPen.setFont(mPlainFont);
+		//*	Draw the individual lines
+		while ((y > r.top) && (null != index)) {
+//            if (engine.app.doSmileys)
+	                smiley.drawString(x,y,index.mText, inPen);
+//            else
+//			    inPen.drawText(x, y, index.mText);
+			 
+			y -= mLineHeight;
+			index = index.mPrev;
+		}
+
+		y = 0;
+		x = r.right - tag_width;
+		
+		
+		inPen.setColor(Color.BLACK);
+		
+		Enumeration enum = engine.alerts.keys();
+		while (enum.hasMoreElements()) {
+            inPen.setFont(tag_font);
+			inPen.drawBitmap(x, y, tag_bitmap);
+			inPen.drawText(x + 3, y + 9, ((Character) enum.nextElement()).toString());
+			y += tag_height;
+		}
+	}
+
+	public void
+            setSize(int inWidth, int inHeight)
+	{
+	Rect	r;
+	
+		super.setSize(inWidth, inHeight);
+
+		mPlainLineBreaker = new LineBreaker("", mPlainFont, inWidth - 8, 0);
+		mBoldLineBreaker = new LineBreaker("", mBoldFont, inWidth - 8, 0);
+
+		r = getBounds();
+		r.inset(kBorder);
+		
+		mNumDisplayLines = r.getHeight() / mLineHeight;
+	}
+
+	public void
+	AddChannel(String inText) { 
+		AddLines(inText, 0); 
+	}
+	
+	public void
+	AddSent(String inText) {
+		AddLines(inText, 3);
+	}
+
+	public void
+	AddPrivate(String inText) {
+		AddLines(inText, 1);
+	}
+
+	public void
+	AddServer(String inText) {
+		AddLines(inText, 2);
+	}
+
+
+    void AddLines(String inText, int glyph)  {
+         if (engine.app.doSmileys)
+            AddLinesIcon(inText, glyph);
+         else
+            AddLinesPlain(inText, glyph);
+    }
+
+    private void AddLinesPlain(String inText, int glyph)
+    {
+    int					firstChar = 0, lastChar = 0;
+    boolean				firstLine = true;
+    LineBreaker			lineBreaker;
+
+        //*	If we're scrolled up then we need to mark this line as the first
+        //*	incoming line that we missed;
+        if (0 == mCurrentSelection)
+            mFirstMissed = false;
+
+        //*	Pick the right line breaker depending on the font
+        lineBreaker = mPlainLineBreaker;
+
+        lineBreaker.setText(inText);
+
+        while (lastChar < inText.length())
+        {
+        String		s;
+        ChatLine	line;
+
+            lastChar = lineBreaker.nextLineBreak(firstChar);
+
+            s = inText.substring(firstChar, lastChar);
+            line = new ChatLine(s, glyph, firstLine, mFirstMissed);
+            AddLine(line, glyph);
+
+            firstLine = false;
+            mFirstMissed = false;
+
+            firstChar = lastChar;
+        }
+
+        TrimForLength();
+    }
+
+
+	private void AddLinesIcon(String inText, int glyph)
+	{
+	int					firstChar = 0, lastChar = 0;
+	boolean				firstLine = true;
+	//LineBreaker			lineBreaker;
+    SmileyEngine        smileyEngine;
+
+		//*	If we're scrolled up then we need to mark this line as the first
+		//*	incoming line that we missed;
+		if (0 == mCurrentSelection)
+			mFirstMissed = false;
+
+		//*	Pick the right line breaker depending on the font
+		//lineBreaker = mPlainLineBreaker;
+	
+		//lineBreaker.setText(inText);
+        if (inText.startsWith("<::::>")) {
+            // server flag
+            inText = SmileyEngine.ICON_FLAG + inText.substring(6);
+        }
+        if (inText.startsWith("<*>")) {
+            // cognet++ message
+            inText = SmileyEngine.ICON_COG + inText.substring(3);
+        }
+        if (inText.startsWith("* ")) {
+            // action flag
+            inText = SmileyEngine.ICON_BULLET + inText.substring(1);
+        }
+        if (inText.startsWith("<-> ")) {
+        	// info flag
+        	inText = SmileyEngine.ICON_INFO + inText.substring(3);
+        }
+
+		smileyEngine = new SmileyEngine(mPlainFont, inText);
+        String strLine;
+
+
+		while ((strLine = smileyEngine.getNext(getWidth() - 8)) != null)
+		{
+		    String		s;
+		    ChatLine	line;
+
+            line = new ChatLine(strLine, glyph, firstLine, mFirstMissed);
+			AddLine(line, glyph);
+
+			firstLine = false;
+			mFirstMissed = false;
+			
+		}
+		
+		TrimForLength();
+	}
+
+	public void
+	AddLine(ChatLine inLine, int glyph)
+	{
+	Rect		r = getBounds();
+	int			numLines;
+	ChatLine	line;
+	boolean		atBottom = false;
+	
+		if (mBottomDisplayLine == mTail)
+			atBottom = true;
+
+		mNumLines++;
+		mTextSize += inLine.mText.length();
+
+		r.inset(kBorder);
+		numLines = r.getHeight() / mLineHeight;
+	
+		if (null == mHead)
+		{
+			mHead = inLine;
+			mBottomDisplayLine = inLine;
+			mTail = inLine;
+			
+			return;
+		}
+		
+		mTail.mNext = inLine;
+		inLine.mPrev = mTail;
+		mTail = inLine;
+		
+		if (atBottom /* || output */)
+		{
+			mCurrentSelection = 0;
+			mBottomDisplayLine = mTail;
+		}
+		else
+		{
+			mCurrentSelection++;
+		}
+	}
+	
+	void
+	TrimForLength()
+	{
+		while (mTextSize > mLimit)
+		{
+			if (mNumDisplayLines >= (mNumLines - mCurrentSelection))
+			{
+				mCurrentSelection--;
+				mBottomDisplayLine = mBottomDisplayLine.mNext;
+			}
+
+			mTextSize -= mHead.mText.length();
+			
+			mHead.mNext.mPrev = null;
+			mHead = mHead.mNext;
+			
+			mNumLines--;
+		}
+	}
+
+	void
+	ScrollUp()
+	{
+		if ((mNumLines - mCurrentSelection) <= mNumDisplayLines)
+			return;
+			
+		mCurrentSelection++;
+		mBottomDisplayLine = mBottomDisplayLine.mPrev;
+		
+		mFirstMissed = true;
+	}
+	
+	void
+	ScrollDown()
+	{
+		if (mCurrentSelection == 0)
+			return;
+			
+		mCurrentSelection--;
+		mBottomDisplayLine = mBottomDisplayLine.mNext;
+		
+		if (0 == mCurrentSelection)
+			mFirstMissed = false;
+	}
+
+	public void
+	Clear() {
+		mTextSize = 0;
+		mNumLines = 0;
+		mCurrentSelection = 0;
+		mHead = null;
+		mTail = null;
+		mBottomDisplayLine = null;
+		mFirstMissed = false;
+	}
+	
+    void setPlainFont(Font f) {
+        Clear();
+        mPlainFont = f;
+        mAscent = mPlainFont.getAscent() + mPlainFont.getDescent();
+        mLineHeight = mAscent + kLeading - 2;
+        mNumDisplayLines = getHeight() / mLineHeight;
+    }
+
+    void setEmoticons(boolean onOff) {
+        Clear();
+        engine.app.doSmileys = onOff;
+    }
+
+	private int			mLimit;
+	private int			mTextSize;
+	private int			mNumDisplayLines;
+
+	private int			mNumLines;
+	private int			mCurrentSelection;
+
+	private int			mAscent;
+	private int			mLineHeight;
+
+	private ChatLine	mHead;
+	private ChatLine	mBottomDisplayLine;
+	private ChatLine	mTail;
+	
+	private Font		mPlainFont;
+	private Font		mBoldFont;
+
+	private LineBreaker	mPlainLineBreaker;
+	private LineBreaker	mBoldLineBreaker;
+
+	private boolean		mFirstMissed;
+	
+	static final int	kBorder		=	2;
+	static final int	kLeading	=	2;
+	Engine engine;
+}
+
+class ChatLine
+{
+	public
+	ChatLine(String inText, int glyph, boolean inFirstLine, boolean inFirstMissed)
+	{
+		mText = inText;
+		mGlyph = glyph;
+		mFirstLine = inFirstLine;
+		mFirstMissed = inFirstMissed;
+	}
+	
+	String		mText;
+	int		mGlyph;
+	boolean		mFirstLine;
+	boolean		mFirstMissed;
+
+	ChatLine	mPrev;
+	ChatLine	mNext;
+}
Index: trunk/hiptop/cognet++/org/twodot/cognet/ChatWindow.java
===================================================================
--- trunk/hiptop/cognet++/org/twodot/cognet/ChatWindow.java	(revision 210)
+++ trunk/hiptop/cognet++/org/twodot/cognet/ChatWindow.java	(revision 210)
@@ -0,0 +1,244 @@
+/*
+ * cognet chat app
+ *
+ * Copyright 2003, Brian Swetland <swetland@frotz.net>
+ * See LICENSE for redistribution terms
+ *
+ */
+package org.twodot.cognet;
+
+import danger.ui.ScreenWindow;
+import danger.ui.EditText;
+import danger.ui.Layout;
+import danger.app.Event;
+import danger.ui.Font;
+import java.util.Date;
+import danger.util.format.DateFormat;
+
+import danger.ui.MarqueeAlert;
+import danger.ui.NotificationManager;
+import danger.ui.Shortcut;
+
+public class ChatWindow extends ScreenWindow
+{
+	public ChatWindow(Engine e, String ctxt, String key) {
+		super(ctxt);
+
+		history = new ChatHistory(8192, e);
+		history.setSize(getWidth() - 1, getHeight() - 20);
+		history.setPosition(1, 0);
+		this.addChild(history);
+		history.show();
+
+		line = new EditText(true, true);
+		Layout.positionBelow(line, history, 0, 2);
+		line.setSize(getWidth(), getHeight() - line.getTop());
+		line.show();
+		addChild(line);
+
+		setFocusedChild(line);
+		context = ctxt;
+		this.key = key;
+		engine = e;
+		app = e.app;
+
+		if(key.charAt(0) == '%') special = true;
+		if(key.charAt(0) == '#') channel = true;
+		if(key.charAt(0) == '&') channel = true;
+	}
+
+	public void show() {
+		engine.MakeActive(this);
+		super.show();
+	}
+	
+	public void adjustActionMenuState() {
+		app.MakeMenu(getActionMenu(),this);
+	}
+	
+	public boolean eventKeyUp(char key, Event event) {
+		if((key == '\n') || (key == '\r')){
+			engine.Command(this.key,line.toString());
+			line.clear();
+			return true;
+		} else {
+			return super.eventKeyUp(key, event);
+		}
+	}
+	
+	public void ScrollList(int count) {
+		if(count < 0) {
+			count = -count;
+			for(int i = 0; i < count; i++) history.ScrollUp();
+		} else {
+			for(int i = 0; i < count; i++) history.ScrollDown();
+		}
+		history.invalidate();
+		
+	}
+	
+	// no autorepeat
+	public boolean eventWidgetUp(int widget, Event event) {
+		switch(widget) {
+		case Event.DEVICE_WHEEL:
+			ScrollList(-1);
+			return true;
+		case Event.DEVICE_WHEEL_PAGE_UP:
+			ScrollList(-8);
+			return true;
+		case Event.DEVICE_WHEEL_PAGE_DOWN:
+			ScrollList(8);
+			return true;
+		case Event.DEVICE_BUTTON_BACK: 
+			getApplication().returnToLauncher();
+			return true;
+		default:
+			return super.eventWidgetUp(widget,event);
+		}
+	}
+
+	// autorepeat
+	public boolean eventWidgetDown(int widget, Event event) {
+		switch(widget) {
+		case Event.DEVICE_WHEEL:
+			ScrollList(1);
+			return true;
+		case Event.DEVICE_WHEEL_PAGE_DOWN:
+			ScrollList(8);
+			return true;
+		default:
+			return super.eventWidgetDown(widget,event);
+		}
+	}
+
+	public boolean receiveEvent(Event e) {
+			//e.DebugPrint();
+		switch(e.type){
+		case kMessage:
+			String s = (String) e.argument;
+			history.AddChannel(s);
+			history.invalidate();
+			if(!active) {
+				if(!mailbox) {
+					mailbox = true;
+					engine.UpdateAlerts();
+				}
+				//if(!channel && !special){
+				//}
+			}
+
+            if (!app.fgapp) {
+                long timeNow = System.currentTimeMillis();
+                if ((app.doMarquee) && (timeNow >= (lastMarquee + (1000 * marqueeTimeout)))) {
+                    NotificationManager.marqueeAlertNotify(new MarqueeAlert(s, app.bm_notify,1));
+                    lastMarquee = timeNow;
+                }
+                app.Notify();
+            }
+
+            Date date = new Date();
+            setSubTitle("Last Message " + DateFormat.withFormat("h:mma", date));
+
+			return true;
+		case kClear:
+			history.Clear();
+			history.invalidate();
+			return true;
+		}
+
+		return super.receiveEvent(e);
+	}
+
+	void MSG(String s) {
+		Event e = new Event(this, kMessage);
+		e.argument = s;
+		getListener().sendEvent(e);
+	}
+	
+	void CLR() {
+		getListener().sendEvent(new Event(this, kClear));
+	}
+	
+
+	void URL(String URL, String Category, String Name)
+	{
+		links.AddLink(URL, Category, Name);
+		app.MakeMenu(getActionMenu(),this);
+	}
+	
+	public boolean eventShortcut(char c, Event e) {
+		if ((e.modifiers & Event.EVENT_MODIFIER_CAPS) != 0) {
+			if (engine.saveQuickKey(key, (char)e.data))
+				return true;
+		}
+		switch (c) {
+		case '@':
+			engine.login.show();
+			return true;
+		case ',':
+			ChatWindow cw = engine.NextMailbox(this);
+			if(cw != null) cw.show();
+			return true;
+		case Shortcut.ARROW_LEFT:
+			prev.show();
+			return true;
+		case Shortcut.ARROW_RIGHT:
+			next.show();
+			return true;
+		default:
+		    System.err.println("Got shortcut " + c);
+			return super.eventShortcut(c, e);
+		}
+	}
+	
+	static final int kMessage = 1;
+	static final int kClear = 2;
+	
+	Cognet app;
+	ChatHistory history;
+	EditText line;
+	Engine engine;
+
+	String context;
+	String key;
+	
+	void Remove() {
+		next.prev = prev;
+		prev.next = next;
+		next = this;
+		prev = this;
+		hide();
+	}
+
+	void SetQuickKey(char quickkey) {
+		this.quickkey = quickkey;
+		if(quickkey != '\0'){
+			setTitle(Shortcut.makeShortcutLabel(quickkey, true) + ": " + context);
+		} else {
+			setTitle(context);
+		}
+	}
+
+    void setFont(Font f) {
+        if (f != null) {
+            history.setPlainFont(f);
+            history.invalidate();
+        }
+    }
+
+    void setEmoticons(boolean onOff) {
+        history.setEmoticons(onOff);
+    }
+
+	char quickkey;
+	boolean active;
+	boolean mailbox;
+	boolean special;
+	boolean channel;
+    	long lastMarquee;
+	public final int marqueeTimeout = 60;		// one minute
+
+    	public LinkMenu links = new LinkMenu();
+
+	ChatWindow next, prev;
+}
Index: trunk/hiptop/cognet++/org/twodot/cognet/Cognet.java
===================================================================
--- trunk/hiptop/cognet++/org/twodot/cognet/Cognet.java	(revision 210)
+++ trunk/hiptop/cognet++/org/twodot/cognet/Cognet.java	(revision 210)
@@ -0,0 +1,240 @@
+/*
+ * cognet chat app
+ *
+ * Copyright 2003, Brian Swetland <swetland@frotz.net>
+ * See LICENSE for redistribution terms
+ *
+ */
+package org.twodot.cognet;
+
+import danger.app.Application;
+import danger.app.Timer;
+import danger.ui.Menu;
+import danger.ui.MenuItem;
+import danger.ui.Bitmap;
+import danger.app.Event;
+import danger.app.EventType;
+import danger.app.SettingsDB;
+import danger.ui.NotificationManager;
+import danger.util.DEBUG;
+
+public class Cognet extends Application implements Resources
+{
+	public Cognet() {
+
+		timer = new Timer(300*1000); // 5 minutes;
+
+		bm_plain = getBitmap(kBlank);
+		bm_mail = getBitmap(kMarker);
+		bm_tag = getBitmap(kTag);
+        	bm_notify = getBitmap(kPending);
+
+        try {
+            NotificationManager.registerPendingIcon("cognet", getBitmap(kPending), getBitmap(kPending));
+        } catch (Throwable t){
+            System.out.println(t.getMessage());
+        }
+
+        cogSettings = new SettingsDB("prefs");
+
+		DEBUG.p("cognet: setReceiveConnectivityEvents(true");
+		setReceiveConnectivityEvents(true);
+	}
+
+	void Notify() {
+		synchronized (this) {
+			//System.err.println("cognet: notify");
+			if(!fgapp&&doActivity) 
+				NotificationManager.setPendingIconVisible("cognet", true);
+		}
+	}
+	
+	public void resume() {
+		timer.stop();
+
+		if(engine == null) {
+			engine = new Engine(this);
+		}
+		synchronized (this) {
+			fgapp = true;
+			System.err.println("cognet: no notify");
+			NotificationManager.setPendingIconVisible("cognet", false);
+			engine.sendResume();
+		}
+	}
+	
+	public void quit() {
+		try {
+			synchronized (this) {
+				fgapp = true;
+				System.err.println("cognet: no notify");
+				NotificationManager.setPendingIconVisible("cognet", false);
+			}
+			engine.logout();
+		} catch (Throwable t){
+		}
+		unload();
+	}
+	
+	public void suspend() {
+		synchronized (this) {
+			fgapp = false;
+			timer.start();
+		}
+	}
+
+	public void MakeMenu(Menu a, ChatWindow w) {
+		MenuItem mi;
+
+		a.removeAllItems();
+		engine.AddWindows(a);
+
+		a.addDivider();
+
+		if( w != null )
+		{
+			Menu links;
+
+			links = w.links.makeLinkMenu(kURL);
+
+			if( links != null ) 
+			{
+				mi = a.addItem("Links");
+				mi.addSubMenu(links);
+			}
+
+			menuWindow = w;
+		}
+
+		Menu options = new Menu("Options");
+
+		mi = options.addItem("Icons and Smileys",kSmileys);
+		mi.setChecked(doSmileys);
+
+		mi = options.addItem("Activity Indicator",kActivity);
+		mi.setChecked(doActivity);
+
+		mi = options.addItem("Marquee", kMarquee);
+		mi.setChecked(doMarquee);
+
+		mi = options.addItem("Auto-Reconnect",kAutoConnect);
+		mi.setChecked(doAutoConnect);
+
+		mi = a.addItem("Options");
+		mi.addSubMenu(options);
+
+		if(w != null){
+			mi = a.addItem("Close Tab",kCloseWindow,0,w.key,this);
+			mi.setShortcut('.');
+		}
+		if(engine.state == Engine.ONLINE){
+			a.addItem("Disconnect",kDisconnect);
+		} else {
+			a.addItem("Connect",kConnect);
+		}
+
+	}
+	
+	public boolean receiveEvent(Event e) {
+		if( (e.type >= kURL) && (e.type <= kURLend) ) 
+		{
+			menuWindow.links.dispatchLink(e.type,kURL);
+			return true;
+		}
+
+		switch(e.type){
+		case EventType.EVENT_ENTERING_KEY_GUARD:
+			timer.start();
+			return true;
+		case EventType.EVENT_TIMER:
+			engine.sendSuspend();
+			timer.stop();
+			return true;
+
+		case kGotoLogin:
+			engine.login.show();
+			return true;
+		case kGotoWindow:
+			engine.Select((String)e.argument,false);
+			return true;
+		case kCloseWindow:
+			engine.Close((String)e.argument);
+			return true;
+		case Event.EVENT_CONNECTION_UP:
+			engine.login.SendEvent(5040);
+			return true;
+		case kConnect:
+			engine.login();
+			return true;
+		case kDisconnect:
+			engine.logout();
+			return true;
+
+		case kSmileys:
+			doSmileys = !doSmileys;
+			savePrefs();
+			return true;
+
+		case kActivity:
+			doActivity = !doActivity;
+			savePrefs();
+			return true;
+
+		case kMarquee:
+			doMarquee = !doMarquee;
+			savePrefs();
+			return true;
+
+		case kAutoConnect:
+			doAutoConnect = !doAutoConnect;
+			savePrefs();
+			return true;
+
+		default:
+			return super.receiveEvent(e);
+		}
+	}
+
+	public void savePrefs()
+	{
+		cogSettings.setIntValue("doSmileys",doSmileys ? 1 : 0);
+		cogSettings.setIntValue("doActivity",doActivity ? 1 : 0);
+		cogSettings.setIntValue("doMarquee",doMarquee ? 1 : 0);
+		cogSettings.setIntValue("doAutoConnect",doAutoConnect ? 1 : 0);
+	}
+
+	static final int kGotoLogin = 1;
+	static final int kGotoWindow = 2;
+	static final int kCloseWindow = 3;
+	static final int kDisconnect = 4;
+	static final int kConnect = 5;
+	static final int kPrefs = 6;
+	static final int kLinks = 7;
+	static final int kSmileys = 8;
+	static final int kActivity = 9;
+	static final int kMarquee = 10;
+	static final int kAutoConnect = 11;
+
+	static final int kURL = 100;
+	static final int kURLend = kURL + LinkMenu.maxLinkTargets;
+
+	ChatWindow menuWindow;
+
+	Engine engine;
+
+	Timer timer;
+
+	Bitmap bm_plain;
+	Bitmap bm_mail;
+	Bitmap bm_tag;
+    Bitmap bm_notify;
+
+    SettingsDB cogSettings;
+
+	boolean fgapp;
+	
+	boolean doActivity;
+	boolean doMarquee;
+	boolean doSmileys;
+	boolean doAutoConnect;
+}
Index: trunk/hiptop/cognet++/org/twodot/cognet/Engine.java
===================================================================
--- trunk/hiptop/cognet++/org/twodot/cognet/Engine.java	(revision 210)
+++ trunk/hiptop/cognet++/org/twodot/cognet/Engine.java	(revision 210)
@@ -0,0 +1,687 @@
+/*
+ * cognet chat app
+ *
+ * Copyright 2003, Brian Swetland <swetland@frotz.net>
+ * See LICENSE for redistribution terms
+ *
+ */
+package org.twodot.cognet;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.util.Hashtable;
+
+import danger.ui.MarqueeAlert;
+import danger.ui.Menu;
+import danger.ui.MenuItem;
+import danger.ui.NotificationManager;
+import danger.ui.Shortcut;
+
+public class Engine implements Resources, Runnable
+{
+	public Engine(Cognet theapp) {
+		app = theapp;
+
+        initSettings();
+
+		out_lock = new Object();
+		in_buffer = new byte[512];
+		state = OFFLINE;
+		login = new LoginWindow(this);
+		first = new ChatWindow(this,"Cognet Server","%server");
+		first.next = first;
+		first.prev = first;
+		windows = new Hashtable();
+		windows.put("%server",first);
+
+		login.show();
+
+	}
+
+    void initSettings() {
+        String settingsHost, settingsUser, settingsPasswd;
+        int settingsPort;
+
+        //get Host
+        settingsHost = app.cogSettings.getStringValue("host");
+        if (settingsHost != null) HOST = settingsHost;
+
+        try {
+        //get port
+            	PORT = app.cogSettings.getIntValue("port");
+
+        //get user
+        	settingsUser = app.cogSettings.getStringValue("user");
+        	if (settingsUser != null) USER = settingsUser;
+
+        //get pass
+        	settingsPasswd = app.cogSettings.getStringValue("password");
+        	if (settingsPasswd != null) PASSWD = settingsPasswd;
+
+		boolean settingsSmiley, settingsMarquee;
+		boolean settingsActivity, settingsAutoConnect;
+
+	//get prefs
+		settingsSmiley = 1 == app.cogSettings.getIntValue("doSmileys");
+		settingsMarquee = 1 == app.cogSettings.getIntValue("doMarquee");
+		settingsActivity = 1 == app.cogSettings.getIntValue("doActivity");
+		settingsAutoConnect = 1 == app.cogSettings.getIntValue("doAutoConnect");
+
+		app.doSmileys = settingsSmiley;
+		app.doMarquee = settingsMarquee;
+		app.doActivity = settingsActivity;
+		app.doAutoConnect = settingsAutoConnect;
+        } catch (Throwable e) {}
+    }
+
+	void AddWindows(Menu a) {
+		synchronized(lock) {
+			ChatWindow w = first;
+			do {
+				if(!w.special){
+					Character c = (Character) windowQuickKeys.get(w.key);
+					MenuItem mi = a.addItem(w.context, Cognet.kGotoWindow, 0, w.key, app);
+					mi.setIcon(w.mailbox ? app.bm_mail : app.bm_plain);
+					if(c != null) mi.setShortcut(c.charValue());
+					mi.setChecked(w == active);
+				}
+				w = w.next;
+			} while(w != first);
+			w = first;
+			do {
+				if(w.special){
+					Character c = (Character) windowQuickKeys.get(w.key);
+					MenuItem mi = a.addItem(w.context, Cognet.kGotoWindow, 0, w.key, app);
+					mi.setIcon(w.mailbox ? app.bm_mail : app.bm_plain);
+					if(c != null) mi.setShortcut(c.charValue());
+					mi.setChecked(w == active);
+				}
+				w = w.next;
+			} while(w != first);
+		}
+	}
+
+	ChatWindow NextMailbox(ChatWindow start) {
+		synchronized(lock) {
+			ChatWindow w = start.next;
+			while(w != start){
+				if(w.mailbox) return w;
+				w = w.next;
+			}
+		}
+		return null;
+	}
+
+	void sendSuspend() {
+		try {
+			//Write("suspend","%server","");
+			isSuspended = true;
+		}
+		catch( Exception e ) {};
+	}
+
+	void sendResume() {
+		if( isSuspended ) {
+			try {
+				//Write("resume","%server","");
+			}
+			catch( Exception e ) {};
+		}
+
+		isSuspended = false;
+	}
+	
+	
+	void nuke() {
+		ChatWindow w,n;
+		
+		synchronized(lock){
+			w = first;
+			w.CLR();
+			w = w.next;
+			while(w != first){
+				w.hide();
+				w = w.next;
+			}
+
+			first.next = first;
+			first.prev = first;
+			windows = new Hashtable();
+			windows.put("%server",first);
+		}
+		RX_SERIAL="0";
+	}
+	
+	public void run() {
+		System.err.println("cognet: starting...");
+		login.SendEvent(5039);
+
+        // if we don't have a last window selected
+        // or the select fails to find a window, then show
+        // the server window.
+		if (lastWindowSelected == null || !Select(lastWindowSelected,false))
+            Select("%server",true);
+
+		try {
+			synchronized(out_lock) {
+				s = new Socket(HOST, PORT);
+				System.err.println("cognet: Connected to "+HOST+":"+PORT);
+				in = s.getInputStream();
+				out = s.getOutputStream();
+			}
+			System.err.println("cognet: Signing On");
+			state = ONLINE;
+			SignOn();
+			System.err.println("cognet: IO Loop Starting");
+			login.SendEvent(5039);
+			HandleIO();
+		} catch (Throwable e){
+			System.err.println("cognet: whoops: " + e);
+			e.printStackTrace();
+			try {
+				s.close();
+			} catch (Exception x){
+			}
+		}
+			
+		state = OFFLINE;
+		login.SendEvent(5039);
+		login.show();
+		
+		System.err.println("cognet: Network Thread Exiting");
+	}
+	
+	void login() {
+		synchronized(lock){
+			if(state == OFFLINE){
+				state = CONNECTING;
+				try {
+					(new Thread(this,"cognet: engine")).start();
+				} catch (Throwable t){
+					state = OFFLINE;
+					System.err.println("cognet: can't start engine thread");
+				}
+			}
+		}
+	}
+	
+	
+	void SignOn() throws IOException {
+		Write("serial",RX_SERIAL,TOKEN);
+		Write("test",USER,PASSWD);
+		/*
+		if( isSuspended )
+			Write("suspend","%server","");
+		else
+			Write("resume","%server",""); */
+	}
+
+	void logout() {
+		System.err.println("cognet: logout");
+		state = DISCONNECT;
+		PASSWD="";
+		try {
+			s.close();
+		} catch (Throwable t){
+		}
+		try {
+			in.close();
+		} catch (Throwable t){
+		}
+		try {
+			out.close();
+		} catch (Throwable t){
+		}
+		System.err.println("cognet: logged out");
+	}
+	
+	void Close(String context) {
+		String key = context.toLowerCase();
+		if(key.equals("%server")) return;
+
+		ChatWindow w = null;
+		ChatWindow nextup = null;
+
+		synchronized(lock){
+			w = (ChatWindow) windows.remove(key);
+			if( w == null )
+				return;
+			nextup = w.next;
+			if(w != null){
+				w.Remove();
+			}
+		}
+
+		if(w != null) {
+			nextup.show();
+			try {
+				Write("clear",context,"");
+			} catch (IOException iox) {
+			}
+		}
+	}
+
+	void Command(String context, String str) {
+		int l = str.length();
+		if((l > 0) && (str.charAt(l - 1) < ' ')){
+			/* Why textfield likes to tack a return on the end,
+			   I'll never know... */
+			str = str.substring(0, l - 1);
+		}
+
+		try {
+
+			if(str.charAt(0) == '/'){
+				if(str.startsWith("/clear")){
+					Write("clear",context,"");
+					return;
+				}
+				if(str.startsWith("/bind ")){
+					char c = str.charAt(6);
+					System.err.println("cognet: bind '"+c+"' to '"+context+"'");
+					saveQuickKey(context, c);
+					return;
+				}
+				if(str.startsWith("/win ")){
+					Select(str.substring(5), false);
+					return;
+				}
+				if(str.startsWith("/zap")) {
+					logout();
+					return;
+				}
+//				if(str.startsWith("/query ")) {
+//					Select(str.substring(7),true);
+//					return;
+//				}
+			}
+/*
+			if (str.startsWith("/font ")) {
+				String fontName = str.substring(6);
+				ChatWindow w = (ChatWindow)windows.get(context.toLowerCase());
+                    	if (w != null) {
+                        Font font = Font.findFont(fontName);
+                        w.setFont(font);
+                        MSG(context, "<*> Font set to " + fontName);
+                    } else {
+                        MSG(context, "<*> Unknown font.");
+                    }
+		   	}
+*/
+		/* all other commands are handled on the proxy */
+			if( str.startsWith("/") ){
+				int n = str.indexOf(' ');
+				if(n != -1){
+					Write(str.substring(1,n), context, str.substring(n+1));
+				} else {
+					Write(str.substring(1), context, "");
+				}
+			} else {
+				Write("send",context,str);
+			}
+
+			return;
+		} catch (Exception e) {
+		}
+	}
+	
+	
+	void HandleIO() throws IOException {
+		byte[] b = in_buffer;
+		int i, j, n, p, x;
+		byte mark = (byte) ':';
+		
+		for(;;) {
+				/* read one line from the server */
+			if(state != ONLINE){
+				throw new IOException("not online");
+			}
+
+			p = 0;
+			for(;;) {
+				x = in.read();
+				if(x < 0) throw new IOException("EOF");
+				if((x == 10) || (x == 13)){
+					if(p == 0) {
+						continue;
+					} else {
+						break;
+					}
+				}
+				
+					/* truncate lines that are too long */
+				if(p == 510) {
+					continue;
+				} else {
+					b[p++] = (byte) x;
+				}
+			}
+			b[p] = 0;
+		
+				// System.err.println("LINE: " + new String(b,0,p));
+			
+			String sn, tag, src, dst, txt;
+			
+			sn = null;
+			tag = null;
+			src = null;
+			
+			i = 0;
+			while(i < p){
+				if(b[i] == mark) {
+					sn = new String(b,0,i);
+					break;
+				}
+				i++;
+			}
+
+			j = i = i + 1;
+			while(i < p){
+				if(b[i] == mark) {
+					tag = new String(b,j,i-j);
+					break;
+				}
+				i++;
+			}
+
+			j = i = i + 1;
+			while(i < p){
+				if(b[i] == mark) {
+					src = new String(b,j,i-j);
+					break;
+				}
+				i++;
+			}
+
+			j = i = i + 1;
+			while(i < p){
+				if(b[i] == mark) {
+					dst = new String(b,j,i-j);
+					i++;
+					txt = new String(b,i,p-i);
+
+					if(sn.length() > 0) RX_SERIAL = sn;
+					
+						// System.err.println("["+sn+":"+tag+":"+src+":"+dst+":"+txt+"]");
+
+					Process(tag,src,dst,txt);
+					break;
+				}
+				i++;
+			}
+
+		}
+	}
+	
+
+	void Process(String tag, String src, String dst, String txt) {
+		//System.err.println(tag+":"+src+":"+dst+":"+txt);
+		if(tag.equals("fmsg")){
+			MSG(dst, txt);
+		} else if(tag.equals("dmsg")){
+			MSG(dst,"<"+src+"> "+txt);
+		} else if(tag.equals("dmsg/act")){
+			MSG(dst, "* "+src+" "+txt);
+		} else if(tag.equals("smsg")){
+			MSG(src,"<"+src+"> "+txt);
+		} else if(tag.equals("smsg/act")){
+			MSG(src, "* "+src+" "+txt);
+		} else if(tag.equals("show")){
+			Select(dst, true);
+		} else if(tag.equals("info")){
+			MSG(dst, "<-> " + txt);
+		} else if(tag.equals("repl")){
+			MSG(dst, "<::::> " + txt);
+		} else if(tag.equals("fail")){
+			MSG(dst, "<::::> " + txt);
+		} else if(tag.equals("clear")){
+			CLR(dst);
+		} else if(tag.equals("kill")){
+			Close(dst);
+		} else if(tag.equals("url")){
+			URL(dst, src, txt);
+		} else if(tag.equals("set")){
+			if (dst.startsWith("quick/"))
+				bindQuickKey(txt, dst.charAt(6));
+		} else if(tag.equals("sync")){
+			System.err.println("Got sync token '"+dst+"'");
+			System.err.println("Have token '"+TOKEN+"'");
+			if(!TOKEN.equals(dst)){
+				System.err.println("Token mismatch. Clearing");
+				nuke();
+			}
+			TOKEN = dst;
+		}
+	}
+
+	ChatWindow Create(String context, String key) {
+		ChatWindow w = new ChatWindow(this, context, key);
+		windows.put(key, w);
+
+		Character qk = (Character) windowQuickKeys.get(key);
+		if(qk != null) w.SetQuickKey(qk.charValue());
+		
+		w.next = first;
+		w.prev = first.prev;
+		
+		first.prev.next = w;
+		first.prev = w;
+		
+		return w;
+	}
+	
+	boolean Select(String context, boolean create) {
+		ChatWindow w;
+		String key = context.toLowerCase();
+		
+		synchronized (lock) {
+			w = (ChatWindow) windows.get(key);
+			if(w != null){
+				w.show();
+                lastWindowSelected = context;
+                return true;
+			} else {
+				if(create){
+					w = Create(context, key);
+					w.show();
+                    lastWindowSelected = context;
+                    return true;
+				}
+			}
+		}
+        return false;
+	}
+			
+	void MSG(String context, String text) {
+		ChatWindow w;
+		String key = context.toLowerCase();
+		
+		synchronized (lock) {
+			w = (ChatWindow) windows.get(key);
+			if (w == null) {
+				w = Create(context, key);
+				if (!app.fgapp && app.doMarquee) {
+					String msg = text;
+					NotificationManager.marqueeAlertNotify(new MarqueeAlert(msg, app.bm_notify,1));
+				}
+				app.Notify();
+			}
+			if(text != null) w.MSG(text);
+		}
+	}
+	
+	void CLR(String context) {
+		ChatWindow w;
+		String key = context.toLowerCase();
+		
+		synchronized (lock) {
+			w = (ChatWindow) windows.get(key);
+			if(w != null){
+				w.CLR();
+			}
+		}
+	}
+
+	void URL(String context, String src, String text)
+	{
+		ChatWindow w;
+		String key = context.toLowerCase();
+
+		synchronized (lock) {
+			w = (ChatWindow) windows.get(key);
+			if( w == null ){
+				w = Create(context, key);
+			}
+			if( text != null )
+			{
+				String URL, Name, Category;
+				int splitLoc;
+
+				splitLoc = text.indexOf(' ');
+
+				if( -1 == splitLoc )
+				{
+					URL = text;
+					Name = text;
+					Category = src;
+				}
+				else
+				{
+					URL = text.substring(0,splitLoc);
+					Name = text.substring(splitLoc+1);
+					Category = src;
+				}
+				w.URL(URL,Category,Name);
+			}
+		}
+	}
+
+	void Write(String tag, String dst, String txt) throws IOException {
+        String mark = ":";
+        String line = TX_SERIAL++ + mark + tag + mark + dst + mark + txt + "\n";
+
+		if(state == ONLINE){
+			byte[] bytes = line.getBytes();
+
+			synchronized (out_lock) {
+				try {
+					out.write(bytes, 0, bytes.length);
+				} catch (IOException io) {
+					s.close();
+				}
+			}
+		}
+	}
+	
+	private void bindQuickKey(String context, char quickKey) {
+		String quickKeyWindow = context.toLowerCase();
+		Character qkChar = new Character(quickKey);
+		synchronized (lock) {
+			String previouslyBoundWindow = (String) quickKeyWindows.get(qkChar);
+			if (previouslyBoundWindow != null) {
+				windowQuickKeys.remove(previouslyBoundWindow);
+				ChatWindow cw = (ChatWindow) windows.get(previouslyBoundWindow);
+				if (cw != null) cw.SetQuickKey('\0');
+			}
+			quickKeyWindows.put(qkChar, quickKeyWindow);
+			windowQuickKeys.put(quickKeyWindow, qkChar);
+			ChatWindow cw = (ChatWindow) windows.get(quickKeyWindow);
+			if (cw != null) cw.SetQuickKey(quickKey);
+		}
+	}
+	
+	boolean saveQuickKey(String context, char quickKey) {
+		if (!Character.isLetterOrDigit(quickKey))
+			return false;
+		else {
+			quickKey = Shortcut.validateShortcut(quickKey);
+			bindQuickKey(context, quickKey);
+			try {
+				Write("save", "quick/" + quickKey, context);
+			} catch (Exception e) {
+				// ignore save error
+			}
+			return true;
+		}
+	}
+	
+		/* always called from the ui thread */
+	void MakeActive(ChatWindow cw) {
+		if(active != null) {
+			active.active = false;
+		}	
+		active = cw;
+		if(cw != null){
+			cw.active = true;
+			cw.mailbox = false;
+		}
+		UpdateAlerts();
+	}
+
+	void UpdateAlerts() {
+		boolean any = false;
+		StringBuffer sb = new StringBuffer(64);
+			
+		alerts.clear();
+		
+		synchronized(lock){
+			ChatWindow w = first;
+			do {
+				if(w.mailbox) {
+					any = true;
+					if (w.quickkey != '\0'){
+						alerts.put(new Character(Character.toUpperCase(w.quickkey)), w);
+					} else {
+						alerts.put(UNBOUND_QUICKKEY, w);
+					}
+				}
+				w = w.next;
+			} while(w != first);
+		}
+		if(active != null) active.invalidate();
+		if(any) app.Notify();
+	}
+	
+	
+	String HOST = "";
+	int PORT = 3000;
+	String USER = "";
+	String PASSWD = "";
+
+	String TOKEN = "";
+	String RX_SERIAL = "0";
+    long TX_SERIAL = 0;
+
+	Socket s;
+	InputStream in;
+	OutputStream out;
+	byte in_buffer[];
+	Object out_lock;
+			
+	Hashtable windows;
+	LoginWindow login;
+
+	static final int OFFLINE = 1;
+	static final int ONLINE = 2;
+	static final int CONNECTING = 3;
+	static final int DISCONNECT = 4;
+	
+	static final Character UNBOUND_QUICKKEY = new Character('?');
+	
+	Cognet app;
+	int state;
+	
+	Hashtable windowQuickKeys = new Hashtable();
+	Hashtable quickKeyWindows = new Hashtable();
+	Hashtable alerts = new Hashtable();
+	ChatWindow first;
+	ChatWindow active;
+
+	boolean isSuspended;
+
+    String lastWindowSelected;
+
+	Object lock = new Object();
+}
Index: trunk/hiptop/cognet++/org/twodot/cognet/Events.java
===================================================================
--- trunk/hiptop/cognet++/org/twodot/cognet/Events.java	(revision 210)
+++ trunk/hiptop/cognet++/org/twodot/cognet/Events.java	(revision 210)
@@ -0,0 +1,8 @@
+/* this file is auto-generated */
+
+package org.twodot.cognet;
+
+public interface Events
+{
+	int kID_DummyEvent = 32768;
+}
Index: trunk/hiptop/cognet++/org/twodot/cognet/LinkMenu.java
===================================================================
--- trunk/hiptop/cognet++/org/twodot/cognet/LinkMenu.java	(revision 210)
+++ trunk/hiptop/cognet++/org/twodot/cognet/LinkMenu.java	(revision 210)
@@ -0,0 +1,160 @@
+/*
+ * cognet chat app
+ *
+ * Copyright 2003, Daniel Grobe Scahs <sachs@uiuc.edu>
+ * See LICENSE for redistribution terms
+ *
+ */
+package org.twodot.cognet;
+
+import danger.ui.Menu;
+import danger.ui.MenuItem;
+
+class LinkQueue 
+{
+	public static final int maxQueueLen = 9;
+	
+	private String LinkURLs[] = new String[maxQueueLen];
+	private String LinkNames[] = new String[maxQueueLen]; 
+	private int queueLen = 0;
+
+	public void addLink(String URL, String Name)
+	{
+		System.err.println("Adding "+URL);
+		if( queueLen < maxQueueLen )
+			queueLen++;
+		else
+		{
+			int i;
+
+			for( i = 0; i < queueLen-1; i++ )
+			{
+				LinkURLs[i] = LinkURLs[i+1];
+				LinkNames[i] = LinkNames[i+1];
+			}
+		}
+
+		if( Name.length() == 0 )
+			Name = URL;
+
+		LinkURLs[queueLen-1] = URL;
+		LinkNames[queueLen-1] = Name;
+	}
+
+	public String[] getNames()
+	{
+		return LinkNames;
+	}
+
+	public String[] getURLs()
+	{
+		return LinkURLs;
+	}
+
+	public int getQueueLen()
+	{
+		return queueLen;
+	}
+}
+
+
+public class LinkMenu
+{
+	public static final int maxLinkMenus = 9;
+	public static final int maxLinkTargets = maxLinkMenus * LinkQueue.maxQueueLen;
+
+	int numMenus;
+
+	public String Categories[] = new String[maxLinkMenus];
+	public LinkQueue Queue[] = new LinkQueue[maxLinkMenus];
+
+	public String LinkTarget[] = new String[maxLinkTargets];
+
+	public void AddLink(String URL, String Category, String Name)
+	{
+		synchronized(this)
+		{
+			int catIndex = findCategory(Category);
+			Queue[catIndex].addLink(URL, Name);
+		}
+	}
+
+	public Menu makeLinkMenu(int base)
+	{
+		Menu Linkmenu;
+		int i,j;
+		int index = 0; 
+
+                Menu links = new Menu("Links");
+		MenuItem mi;
+
+		synchronized(this)
+		{
+			if( numMenus == 0 )
+				return null;
+
+			for( i = 0; i < numMenus; i++ )
+			{
+				int size;
+				String Names[], URLs[];
+
+				Menu submenu = new Menu(Categories[i]);
+
+	                	size = Queue[i].getQueueLen();
+				Names = Queue[i].getNames();
+                		URLs = Queue[i].getURLs();
+
+                		for( j = size-1; j >= 0;  j-- )
+				{
+                			submenu.addItem(Names[j],index+base);
+					LinkTarget[index++] = URLs[j];
+				}
+
+				mi = links.addItem(Categories[i]);
+				mi.addSubMenu(submenu);
+			}
+		}
+
+		return links;
+	}
+
+	public void dispatchLink(int target, int base)
+	{
+		System.err.println("Dispatch to "+LinkTarget[target-base]);
+
+		try 
+		{
+			danger.net.URL.gotoURL(LinkTarget[target-base]);
+		}
+		catch( Exception e )
+		{
+		}
+	}
+
+	private int findCategory(String Category)
+	{
+		int i;
+
+		for( i=0; i < numMenus; i++ )
+		{
+			if( 0 == Category.compareTo(Categories[i]) )
+				return i;
+		}
+
+		if( numMenus == maxLinkMenus )
+		{
+			for( i=0; i < numMenus-1; i++ )
+			{
+				Categories[i] = Categories[i+1];
+				Queue[i] = Queue[i+1];
+			}
+		}
+		else
+			numMenus ++;
+
+		Categories[numMenus - 1] = Category;
+		Queue[numMenus - 1] = new LinkQueue();
+
+		return numMenus - 1;
+	}
+}
Index: trunk/hiptop/cognet++/org/twodot/cognet/LoginWindow.java
===================================================================
--- trunk/hiptop/cognet++/org/twodot/cognet/LoginWindow.java	(revision 210)
+++ trunk/hiptop/cognet++/org/twodot/cognet/LoginWindow.java	(revision 210)
@@ -0,0 +1,226 @@
+/*
+ * cognet chat app
+ *
+ * Copyright 2003, Brian Swetland <swetland@frotz.net>
+ * See LICENSE for redistribution terms
+ *
+ */
+package org.twodot.cognet;
+
+import danger.ui.ScreenWindow;
+import danger.ui.TextField;
+import danger.ui.StaticText;
+import danger.ui.StaticTextBox;
+import danger.ui.PasswordTextField;
+import danger.ui.Button;
+import danger.ui.DialogWindow;
+import danger.app.Resource;
+
+import danger.app.Event;
+
+public class LoginWindow extends ScreenWindow implements Resources, Events
+{
+	public LoginWindow(Engine e) {
+		super("Connect to Server", e.app.getBitmap(Resources.kPending));
+		StaticText st;
+		
+		engine = e;
+		app = e.app;
+		
+		int w = 60;
+		int h = 20;
+		int w2 = getWidth() - w - 20;
+		int y = 5;
+		int adj = 3;
+		
+		st = new StaticText("Server ");
+		st.setAlignment(StaticText.HALIGN_RIGHT);
+		st.setPosition(0, y + adj);
+		st.setSize(w, h);
+		st.show();
+		addChild(st);
+		
+		host = new TextField(false, false);
+		host.setPosition(w, y);
+		host.setSize(w2,h);
+		host.show();
+		addChild(host);
+
+		y += h + 2;
+
+		st = new StaticText("Port ");
+		st.setAlignment(StaticText.HALIGN_RIGHT);
+		st.setPosition(0, y + adj);
+		st.setSize(w, h);
+		st.show();
+		addChild(st);
+		
+		port = new TextField(false, false);
+		port.setPosition(w, y);
+		port.setSize(w2,h);
+		port.show();
+		addChild(port);
+
+		y += h + 2;
+		
+		st = new StaticText("Username ");
+		st.setAlignment(StaticText.HALIGN_RIGHT);
+		st.setPosition(0, y + adj);
+		st.setSize(w, h);
+		st.show();
+		addChild(st);
+		
+		user = new TextField(false, false);
+		user.setPosition(w, y);
+		user.setSize(w2,h);
+		user.show();
+		addChild(user);
+
+		y += h + 2;
+		
+		st = new StaticText("Password ");
+		st.setAlignment(StaticText.HALIGN_RIGHT);
+		st.setPosition(0, y + adj);
+		st.setSize(w, h);
+		st.show();
+		addChild(st);
+		
+		passwd = new PasswordTextField();
+		passwd.setPosition(w, y);
+		passwd.setSize(w2,h);
+		passwd.show();
+		addChild(passwd);
+					 
+		y += h + 2;
+		Button aboutBtn = new Button(" About ");
+		aboutBtn.setPosition(w + 35,y);
+		aboutBtn.show();
+		addChild(aboutBtn);
+		aboutBtn.setEvent(this, 6000);
+
+		button = new Button("  Connect  ");
+		button.setPosition(w,y);
+		button.setTop(y);
+		button.setRight(w + w2);
+		button.show();
+		addChild(button);
+		button.setEvent(this, 5038);
+
+		setFocusedChild(button);
+		
+		host.setText(engine.HOST);
+		port.setText(Integer.toString(engine.PORT));
+		user.setText(engine.USER);
+		passwd.setText(engine.PASSWD);
+
+	}
+	
+	public void show() {
+		engine.MakeActive(null);
+		super.show();
+	}
+	
+	public void adjustActionMenuState() {
+		app.MakeMenu(getActionMenu(),null);
+	}	
+
+	public boolean eventWidgetUp(int widget, Event event) {
+		switch(widget) {
+		case Event.DEVICE_BUTTON_BACK: 
+			getApplication().returnToLauncher();
+			return true;
+		default:
+			return super.eventWidgetUp(widget,event);
+		}
+	}
+
+
+	public boolean receiveEvent(Event e) {
+		if(e.type == 5038){
+			switch(engine.state){
+			case Engine.OFFLINE:
+				engine.HOST = new String(host.getChars());
+				engine.PORT = Integer.parseInt(new String(port.getChars()));
+				engine.USER = new String(user.getChars());
+				engine.PASSWD = new String(passwd.getChars());
+				engine.login();
+				return true;
+
+			case Engine.CONNECTING:
+				return true;
+
+			case Engine.ONLINE:
+				engine.logout();
+				return true;
+
+			default:
+				return true;
+			}
+		} 
+
+		if(e.type == 5039){
+			System.err.println("statecheck");
+			switch(engine.state){
+			case Engine.OFFLINE:
+				button.setTitle("Connect");
+				return true;
+			case Engine.CONNECTING:
+				button.setTitle("Cancel");
+				return true;
+			case Engine.ONLINE:
+                // save settings
+                System.out.println("cognet: commit settings");
+                app.cogSettings.setStringValue("host", engine.HOST);
+                app.cogSettings.setIntValue("port", engine.PORT);
+                app.cogSettings.setStringValue("user", engine.USER);
+                app.cogSettings.setStringValue("password", engine.PASSWD);
+
+				button.setTitle("Disconnect");
+				return true;
+			default:
+				return true;
+			}
+		}
+		if(e.type == 5040 && app.doAutoConnect){
+		  System.err.println("autologin");
+		  if (engine.PASSWD != null && engine.PASSWD.length()>0) {
+		    hide();
+		    engine.login();
+		  }
+		  return true;
+		}
+
+        if(e.type == 6000) {
+            DialogWindow w = app.getDialog(kAboutDialog, this);
+            StaticTextBox s = (StaticTextBox)w.getDescendantWithID(kID_AboutText);
+            Resource r = app.getResources().getResource(257, kLicense);
+            byte b[] = new byte[r.getSize()];
+            r.getBytes(b,0,r.getSize());
+            s.setText(new String(b));
+            w.show();
+        }
+
+		return super.receiveEvent(e);
+	}
+
+	void SendEvent(int e) {
+		sendEventToWindow(new Event(this,e));
+	}
+	
+	public boolean eventShortcut(char c, Event e) {
+		if(c == 'n') {
+			if(engine.first != null){
+				engine.first.show();
+			}
+			return true;
+		}
+		return super.eventShortcut(c, e);
+	}
+	
+	TextField host, port, user, passwd;
+	Button button;
+	
+	Engine engine;
+	Cognet app;
+	
+}
Index: trunk/hiptop/cognet++/org/twodot/cognet/Resources.java
===================================================================
--- trunk/hiptop/cognet++/org/twodot/cognet/Resources.java	(revision 210)
+++ trunk/hiptop/cognet++/org/twodot/cognet/Resources.java	(revision 210)
@@ -0,0 +1,20 @@
+/* this file is auto-generated */
+
+package org.twodot.cognet;
+
+public interface Resources
+{
+	int ID_APP_ID = 1;
+	int k11x11Icons = 9;
+	int k5x6Icons = 10;
+	int kAboutDialog = 8;
+	int kBlank = 2;
+	int kLicense = 7;
+	int kMarker = 3;
+	int kPending = 5;
+	int kTag = 4;
+
+	// Control and Menu Item ID's
+	int kID_AboutScroller = 1;
+	int kID_AboutText = 2;
+}
Index: trunk/hiptop/cognet++/org/twodot/cognet/SmileyEngine.java
===================================================================
--- trunk/hiptop/cognet++/org/twodot/cognet/SmileyEngine.java	(revision 210)
+++ trunk/hiptop/cognet++/org/twodot/cognet/SmileyEngine.java	(revision 210)
@@ -0,0 +1,156 @@
+package org.twodot.cognet;
+
+import danger.ui.Pen;
+import danger.ui.Font;
+import danger.ui.Bitmap;
+import danger.app.Application;
+import danger.app.ResourceDatabase;
+import danger.util.StringUtils;
+import org.twodot.cognet.Resources;
+
+public class SmileyEngine implements Resources {
+
+    private String text;
+
+    private static String emoticonsText[] = {"O:-)", ":-)", ":-(", ";-)", ":-P", "=-o", ":-*", ">:o", "8-)", ":-$", ":-!", ":-[", ":-\\", ":'(", ":-X", ":-D"};
+
+    private static int FIRST_ICON = 0xE000;
+    private int      iconWidth;
+    private int      iconCount;
+    private Bitmap[] iconSet;
+    Font             font;
+
+    public static final char ICON_INNOCENT = 0xE000;
+    public static final char ICON_SMILING  = 0xE001;
+    public static final char ICON_FROWNING = 0xE002;
+    public static final char ICON_WINKING  = 0xE003;
+    public static final char ICON_STICKING_OUT_TONGUE = 0xE004;
+    public static final char ICON_SURPRISED = 0xE005;
+    public static final char ICON_KISSING   = 0xE006;
+    public static final char ICON_YELLING   = 0xE007;
+    public static final char ICON_COOL      = 0xE008;
+    public static final char ICON_MONEY_MOUTH = 0xE009;
+    public static final char ICON_FOOT_IN_MOUTH = 0xE00A;
+    public static final char ICON_EMBARASSED = 0xE00B;
+    public static final char ICON_INDIFFERENT = 0xE00C;
+    public static final char ICON_CRYING      = 0xE00D;
+    public static final char ICON_LIPS_SEALED = 0xE00E;
+    public static final char ICON_LAUGHING    = 0xE00F;
+    public static final char ICON_COG         = 0xE010;
+    public static final char ICON_IRC         = 0xE011;
+    public static final char ICON_FLAG     = 0xE012;
+    public static final char ICON_INFO     = 0xE013;
+    public static final char ICON_BULLET  = 0xE014;
+
+    public static final int  LARGE_ICON_LIMIT = 11;
+
+    public SmileyEngine(Font f, String _text) {
+        this(f);
+        this.text = replaceWithUnicode(_text);
+    }
+
+    public SmileyEngine(Font f) {
+        font = f;
+        ResourceDatabase rdb = Application.getCurrentApp().getResources();
+        iconSet=null;
+        if (f.getAscent() + f.getDescent() >= LARGE_ICON_LIMIT)
+            iconSet = rdb.getBitmapArray(k11x11Icons);
+        else
+            iconSet = rdb.getBitmapArray(k5x6Icons);
+
+        iconWidth = iconSet.length;
+    }
+
+    public String getNext(int pixels) {
+        if (text.length() <= 0) return null;
+
+        int indexOfLastSpace = 0;
+        int totalWidth = 0;
+        String returnVal;
+
+        int x=0;
+        for (x=0; x<text.length(); x++) {
+            char c = text.charAt(x);
+            if (c == ' ') indexOfLastSpace = x;
+            totalWidth += charWidth(c);
+            if (totalWidth >= pixels) break;
+        }
+
+        // if indexOfLastSpace == 0, then we've got
+        // one big 'ol word that needs to be broken up
+        if (totalWidth < pixels && x == text.length()) {
+            returnVal = text;
+            text = "";
+        } else if (indexOfLastSpace == 0) {
+            returnVal = text.substring(0,x);
+            //if (x+1 < text.length())
+            text = text.substring(x);
+        } else {
+            // otherwise, break the line at indexOfLastSpace
+            returnVal = text.substring(0,indexOfLastSpace);
+            //if (indexOfLastSpace+1 < text.length())
+                text = text.substring(indexOfLastSpace+1);
+        }
+        return returnVal;
+    }
+
+    public static String replaceWithUnicode(String inText) {
+        String output = inText;
+
+        //replace all emoticons with corresponding character
+        for (int x=0; x < emoticonsText.length; x++) {
+            String thisUnicodeChar = "" + (char)(FIRST_ICON + x);
+
+            // replate both 'nosed' and 'noseless' versions.
+            output = replaceAll(output, emoticonsText[x], thisUnicodeChar);
+            output = replaceAll(output, StringUtils.stripChar(emoticonsText[x], '-'), thisUnicodeChar);
+        }
+        return output;
+    }
+
+    public static String replaceAll(String in, String what, String with) {
+        int pos = 0;
+        int whatLen = what.length();
+        int withLen = with.length();
+        while ((pos = in.indexOf(what, pos)) >= 0) {
+            in = in.substring(0,pos) + with + in.substring(pos+whatLen);
+            pos = pos + withLen;
+        }
+        return in;
+    }
+
+    public void drawString(int x, int y, String s, Pen p) {
+        int xpos = x;
+        p.setFont(font);
+        for (int c=0; c<s.length(); c++) {
+            xpos += drawChar(p, xpos, y, s.charAt(c));
+        }
+    }
+
+    public int charWidth(char c) {
+        if (isIcon(c)) {
+            return iconSet[0].getWidth();
+        } else {
+            return font.charWidth(c);
+        }
+    }
+
+    public int drawChar(Pen p, int xpos, int ypos, char chr) {
+        if (isIcon(chr)) {
+            p.drawBitmap(xpos, ypos-font.getAscent(), getIconBitmap(chr));
+            return charWidth(chr) + font.getGap();
+        } else {
+            p.drawChar(xpos, ypos, chr);
+            return font.charWidth(chr);
+        }
+    }
+
+    public boolean isIcon(char chr) {
+         return (chr >= FIRST_ICON && chr < FIRST_ICON + iconWidth);
+    }
+
+    public Bitmap getIconBitmap(char c) {
+        int ndx = c - 0xE000;
+        return iconSet[ndx];
+    }
+}
