From 3de17f6e9c991e0c85bba39f03bf1b6b59facf07 Mon Sep 17 00:00:00 2001
From: Stephen Dranger <dranger@gmail.com>
Date: Thu, 3 Feb 2011 00:20:37 -0600
Subject: [PATCH] userlist

---
 TODO                       |   1 -
 generic.py                 |  60 ++++++-
 generic.pyc                | Bin 780 -> 3235 bytes
 logs/chums.js              |   2 +-
 oyoyo/helpers.py           |  13 +-
 oyoyo/helpers.pyc          | Bin 4044 -> 4359 bytes
 pesterchum.js              |   2 +-
 pesterchum.py              | 343 ++++++++++++++++---------------------
 pesterdata.py              | 125 ++++++++++++++
 pesterdata.pyc             | Bin 0 -> 7111 bytes
 pestermenus.py             |  86 +++++++++-
 pestermenus.pyc            | Bin 12551 -> 15832 bytes
 profiles/ghostDunk.js      |   2 +-
 themes/pesterchum/style.js |   4 +-
 themes/trollian/style.js   |   4 +-
 15 files changed, 435 insertions(+), 207 deletions(-)
 create mode 100644 pesterdata.py
 create mode 100644 pesterdata.pyc

diff --git a/TODO b/TODO
index f3ac15b..7c56be3 100644
--- a/TODO
+++ b/TODO
@@ -1,5 +1,4 @@
 Features:
-* User list/add from list
 * Turn quirks off
 * User commands/stop user from sending commands accidentally
 * Hyperlinks
diff --git a/generic.py b/generic.py
index e2e0e0b..173285e 100644
--- a/generic.py
+++ b/generic.py
@@ -1,4 +1,21 @@
-from PyQt4 import QtGui
+from PyQt4 import QtGui, QtCore
+
+class PesterIcon(QtGui.QIcon):
+    def __init__(self, *x, **y):
+        QtGui.QIcon.__init__(self, *x, **y)
+        if type(x[0]) in [str, unicode]:
+            self.icon_pixmap = QtGui.QPixmap(x[0])
+        else:
+            self.icon_pixmap = None
+    def realsize(self):
+        if self.icon_pixmap:
+            return self.icon_pixmap.size()
+        else:
+            try:
+                return self.availableSizes()[0]
+            except IndexError:
+                return None
+
 class RightClickList(QtGui.QListWidget):
     def contextMenuEvent(self, event):
         #fuckin Qt
@@ -7,3 +24,44 @@ class RightClickList(QtGui.QListWidget):
             self.setCurrentItem(listing)
             self.optionsMenu.popup(event.globalPos())
 
+class MultiTextDialog(QtGui.QDialog):
+    def __init__(self, title, parent, *queries):
+        QtGui.QDialog.__init__(self, parent)
+        self.setWindowTitle(title)
+        if len(queries) == 0:
+            return
+        self.inputs = {}
+        layout_1 = QtGui.QHBoxLayout()
+        for d in queries:
+            label = d["label"]
+            inputname = d["inputname"]
+            value = d.get("value", "")
+            l = QtGui.QLabel(label, self)
+            layout_1.addWidget(l)
+            self.inputs[inputname] = QtGui.QLineEdit(value, self)
+            layout_1.addWidget(self.inputs[inputname])
+        self.ok = QtGui.QPushButton("OK", self)
+        self.ok.setDefault(True)
+        self.connect(self.ok, QtCore.SIGNAL('clicked()'),
+                     self, QtCore.SLOT('accept()'))
+        self.cancel = QtGui.QPushButton("CANCEL", self)
+        self.connect(self.cancel, QtCore.SIGNAL('clicked()'),
+                     self, QtCore.SLOT('reject()'))
+        layout_ok = QtGui.QHBoxLayout()
+        layout_ok.addWidget(self.cancel)
+        layout_ok.addWidget(self.ok)
+
+        layout_0 = QtGui.QVBoxLayout()
+        layout_0.addLayout(layout_1)
+        layout_0.addLayout(layout_ok)
+
+        self.setLayout(layout_0)
+    def getText(self):
+        r = self.exec_()
+        if r == QtGui.QDialog.Accepted:
+            retval = {}
+            for (name, widget) in self.inputs.iteritems():
+                retval[name] = unicode(widget.text())
+            return retval
+        else:
+            return None
diff --git a/generic.pyc b/generic.pyc
index de25e623a3448e0e951e1561f4786f7fc9cb51aa..11d65839604bdfdf2f5f0a9f446b691c2291014a 100644
GIT binary patch
literal 3235
zcmbtW-BKJy6h1xs%d!x{k|?O9MlUvHm1wCqN?DSHis(Yvff7TR+M3LCmWE+wGt-kS
z)>35^Ui(r$fv@5V=yy&pESejG3*FP*r@PPZcTWHD=Uii}_1$(zlV2I%Z_sj$F2cW}
z6QWqrp`zHMLyuJCkyd0%^h{AyqF;$Fh#nu7DJl!0B818np-NFz2sI(pt_XFC>a^FM
z0Vneper*pUTS3>e4+~?fGU}w6what-b#8UG6{g9Rv<k})(DD^@kQhQjW=I4%Avq69
z<nPJFA#X0BmC+ud<%j4jctaVF#zbR9V~>7Qq-=>NxMBH1ku9@O1CNfYl&*k?Bh_nk
zob1;qX}~hLLnatFraJ~aNL2KB<g{ZQY#q~n-z3KN`(Odu*-(Qq1hbi~;aemoOd~Dh
z-mV#)217fGb*v`+p_}u;oix#HJ}B4ZI(}wZ*OByRZ461eocq<I^i;1#S&$5Lw)!&7
zPF9Cv*zl-0T^;B|XD0l3c;@rX+;SF_)Lr$CTJgk)d^vlmgO+cigVNA5ohRi+u;>LW
zpw1SB<nfG+xp$B(Q5iZOHz=)>!Y|@|KXNZln%IkP_c<<{M&@U2=P?Uj1SSri#(EDc
zxdg>l66w)qmZh1`G16w)d^XYz)fTs8I*4<y;2ZNMI#LZa@0HY|npbT$%EWs~aH`=w
z-q-J+rcn`NDwgwaV{Iyfj9$<F3<h>;&itxsrYa`z&EUv(ViTTpO>RSw<i~0Y5SZ=1
zfPnaCa(H<me>-UTV{`~}xDNh7umA->^|43kn)75?$Q3372j>D34t+QjAYotE$kb)i
z%w&^FKoNH`&z&}#Mt}^P=4s+Uz(V#qz<?gvZJiXGFLYuB8rx}+YuRtCK3%thhhdtF
z?x1vvEW_R{5Csb9(3&*K`4TxlOozqLa^wu+^l1?9rnw+PLPB<E;l=QZVL+1%rcQb-
z|JVgw!8pFI!xGjUUc6;>N6jh6d=blji@W#GO&Q)GDwqS`l(QGB%HG0fyNIpXha4Lw
zh||HVLGfR*V+j{=sMpaRqUFECf&a*k@H(<TdtI_4hXCL?E^#mb-5fU+=X-VJ5@AIy
zsB&nWmud71Z{Bz^qVo!kD}*|-e2!?ssDx3CM+m40g#;nz(Rp2tBfhW+5r~x;8kfnS
zN4PP2$kL!Ob_kh8!_75Xo9w!ra~4<6_vwHS%yK!pI?!+O*fhz^5rT|2^8D6h6<#*C
zDXK96>pWfLs)gxA*$IIAAkd`%{g#|`=UZ~%)$@((=bd?=Mng0M^Oh6iYTlGN%f2M0
zoTc$BnLBI?k=IS+QW5?x)0rYT<Yeg5s>DC?r;Y_7CK(o%GnlNu2;xG62bAQ{dh~tH
zC<-~#=%{^PqB;meJ+$rn0?^L-PG_?#>zO{*A=cY(;E01b1FYxbQS#v>fDcR(r7!o5
zja{vafo87rB1|rr?tS|p9d(1Vv~chrXe(QK-7C?9C^|3^6IjSz*Ccu~GS(g6E%KuW
zI16mDCh3XfXC#je{VV`fq^9g=(3XV+gcAtwbGi~Xds`28*1NK4uls0U*1{kOVKp{o
z?}w>$%p9UjWaSySxLKRAG|f6z6lO(-0VgnF{k%Y}MIB?v#iDQjQ>odJe~(Xpz}<)F
zWYIZovJXmKJM~E$n{Ha;-^S7WOTIw0kg-~-qGnW6)sS5p_*=x^oLW|IBA+dwZ{f>*
z6SD=c3EG@{6>!bwQq@7rkpKh-fCaD*?BE%7y}+y!=Ll4k(Py#`sFpIb@#G%;1fZ8V
zVZihZWPlIX$lDD0W9|7Dbbt!9C((Va(y;kmh9Bv$FHz{TUBpo961h6^KjIbMiu#gq
zjmdqE0&zE|a`%dm_Xm4hET2gX;1vpt$VLGTJM*Ok;SH@XF@PB6%>5NE%ekTu4BJvw
zwLnd^qAH%eHvDM;VGH|RbW^4>dac^!zmE&vLc4rhyt^Y$R59P~S<im%%*UAUOWZM#
lT=?^|!(q=~VhVGY`~HtW;-fY9fcl!b=4{=&-txk7{cqjxZ&v^S

delta 210
zcmZ21*~6A{@e?nX#WzpiWCkc;0n!dYT&xQuQh<n&A)A3Ahmj$Qk%2LVi6NDdA(M$A
zjgcXRnW2S&p_zdpGK!fYg@qwlgB7SF^FI)1FalX6AZ}oZduit8HOw}QoFyQWi&Jyb
zCckA#Vq~5iz$!Y~f?1Z4Z*n24gBeH%6G#DwU}N;t0I3EW5l|UeViE-8F_drvi9nys
a;*#*pl=Re+AV#28`^oJ*LTn&i0*nB(sVJNP

diff --git a/logs/chums.js b/logs/chums.js
index 3fa738f..7cc3baf 100644
--- a/logs/chums.js
+++ b/logs/chums.js
@@ -1 +1 @@
-{"macruralAlchemist": {"color": "#700000", "handle": "macruralAlchemist", "mood": "offline"}, "agogPorphyry": {"color": "#522d80", "handle": "agogPorphyry", "mood": "offline"}, "fireSwallow": {"color": "#80bb9a", "handle": "fireSwallow", "mood": "offline"}, "aquaMarinist": {"color": "#00caca", "handle": "aquaMarinist", "mood": "offline"}, "nitroZealist": {"color": "#ff3737", "handle": "nitroZealist", "mood": "offline"}, "superGhost": {"color": "#800564", "handle": "superGhost", "mood": "offline"}, "tentacleTherapist": {"color": "#cc66ff", "handle": "tentacleTherapist", "mood": "offline"}, "captainCaveman": {"color": "#7c414e", "handle": "captainCaveman", "mood": "offline"}, "mechanicalSpectacle": {"color": "#0000ff", "handle": "mechanicalSpectacle", "mood": "offline"}, "gamblingGenocider": {"color": "#00ff00", "handle": "gamblingGenocider", "mood": "offline"}, "schlagzeugGator": {"color": "#61821f", "handle": "schlagzeugGator", "mood": "offline"}, "unknownTraveler": {"color": "#006666", "handle": "unknownTraveler", "mood": "offline"}, "marineAquist": {"color": "#00caca", "handle": "marineAquist", "mood": "offline"}}
\ No newline at end of file
+{"macruralAlchemist": {"color": "#700000", "handle": "macruralAlchemist", "mood": "offline"}, "agogPorphyry": {"color": "#522d80", "handle": "agogPorphyry", "mood": "offline"}, "fireSwallow": {"color": "#80bb9a", "handle": "fireSwallow", "mood": "offline"}, "aquaMarinist": {"color": "#00caca", "handle": "aquaMarinist", "mood": "offline"}, "nitroZealist": {"color": "#ff3737", "handle": "nitroZealist", "mood": "offline"}, "superGhost": {"color": "#800564", "handle": "superGhost", "mood": "offline"}, "tentacleTherapist": {"color": "#cc66ff", "handle": "tentacleTherapist", "mood": "offline"}, "captainCaveman": {"color": "#7c414e", "handle": "captainCaveman", "mood": "offline"}, "mechanicalSpectacle": {"color": "#0000ff", "handle": "mechanicalSpectacle", "mood": "offline"}, "gamblingGenocider": {"color": "#00ff00", "handle": "gamblingGenocider", "mood": "offline"}, "centaursTesticle": {"color": "#000056", "handle": "centaursTesticle", "mood": "offline"}, "schlagzeugGator": {"color": "#61821f", "handle": "schlagzeugGator", "mood": "offline"}, "unknownTraveler": {"color": "#006666", "handle": "unknownTraveler", "mood": "offline"}, "marineAquist": {"color": "#00caca", "handle": "marineAquist", "mood": "offline"}}
\ No newline at end of file
diff --git a/oyoyo/helpers.py b/oyoyo/helpers.py
index 21973a4..48c15db 100644
--- a/oyoyo/helpers.py
+++ b/oyoyo/helpers.py
@@ -23,8 +23,17 @@ def msg(cli, user, msg):
     for line in msg.split('\n'):
         cli.send("PRIVMSG", user, ":%s" % line)
 
-def nick(cli, nick):
-    cli.send("NICK", nick)
+def names(cli, *channels):
+    tmp = __builtins__['list'](channels)
+    msglist = []
+    while len(tmp) > 0:
+        msglist.append(tmp.pop())
+        if len(",".join(msglist)) > 490:
+            tmp.append(msglist.pop())
+            cli.send("NAMES %s" % (",".join(msglist)))
+            msglist = []
+    if len(msglist) > 0:
+        cli.send("NAMES %s" % (",".join(msglist)))
 
 def msgrandom(cli, choices, dest, user=None):
     o = "%s: " % user if user else ""
diff --git a/oyoyo/helpers.pyc b/oyoyo/helpers.pyc
index 89ddc32e0cd6dc88371e9adf1eaa78565bca880a..b75c0b000458de10ef34314801316d6bd296134a 100644
GIT binary patch
delta 988
zcmaJ<O-~b16uoykZGqB`rYS-tfP9Vt-3S!LubLQ)0Su2AA}pHHRvawT;tYu-MHVdF
z`!>3Csfi2YR@ZJwgv5ovL3g@vsrS4RgNZTiyK~<;=YG!Y_de~9AAV1^IX$=E#$9Wd
ziO4=WGx7}f#e_s<rmhCl9@CCRYeYK{di;b!riKNuY|*++wRPe~gggtV7oj}@$D%3=
zYLjqshuk*uoXABea-ofU7y1T~7p1cNk41HWX%^n>BF0%ld6V*}e9!-4eTgp<ExIw8
zXg<V{RWArVd14NtG4Cty%ET?=&*$fEF7^!tMjW;aQYfr$dDYPKgF+#U2vDu~AyO6V
z^@?9MP`y?+dDm;6uPotQR8dfe$Xa?(^!-XTFye5d9>xXS2-eVtD=xYEt}`_f70)kj
zRD!HHK?evv!7+|dn$z4ZC(90}^?~(^hxI{ZySGy$F3Ol%3Ly%mUCgC2dcnEeiu?U7
zzci-b*gcsHh%QLv{L#IP<@Bj&2aoFW(dSW>6iR=JF7bpOi7oW@Ly55zIy7waYAO%t
zStqaiVzyHXri2fmBvxnix7Y`s*016p`KsP*`8;?7EZQONWRu2htzLW-8proa4|S#W
z2M_3bZHuFKAa5pXHfvfK7gSA%IX1w!#sH~AlIL}IVmX$BW>Pm2D?Iz|OJbBIqn(@^
zN7#y-zp|Pk@`(6X>byz)KAAeVjKF61j^(x>0Chs_q_!<TbsZR1pf2j4$)W7DxaLDL
z!X}L!s4IFhwaQESMJmS^^kFKKO#?RDJqZsVfWpgFr=K|kgM;vfVa~#g!Jrd$3FaEi
OEj{C`a=U)*^#2Bu2B7W$

delta 666
zcmZuuO-NKx6uxJi_ue@3-n^%2*l13U3QxzshHQ)qslh@*!6PCRIH<E31IM4)2<}?6
zmeZnTOSj^pRf|@&h(K^(5JBr&w5;>piEI&%^KtGu-}%mW?|WYOkoV5M2k9?!_fNby
zv63*xAlfu48v9)ehsM;6sgXG(^DJSgX3TO7)?zaWoUc5JNfSoRHW>4^xn19UDrer^
zkW*U9Y3ze#B$NC_S0&>d<U>8i7_d_uAcJm8)0el&JMzg%(V%>D`n!F!BeXUCa*m?1
zuzQ&bQg>gr+s#{z`nI^<DvimSx0JsEHdeC(fRhWW+$$fwhry&4TlR>1q>y?;CHb6s
zN7HiLetzv1x-e#_hpd3zv$MX}w3m}t=^xZ9FEY!;2Nyyun|5(gt30OgkM*L!`n(^|
zoUHnf-3f5U<+cBWDyNr%BB|nRcEQ0I%TcyFH-qedQZrWSXEr;&q?A8S!$gdSXXrL@
z^TvUGTt*Go<tZtJ!?}{Cv5T`cVB5Ga-;mvKgBImHEKiMs)Tz+)6spx)W3yW2G-&os
t?V=7G@k_Z=ZbyS7eMtKO1Arj_9+9sDZUSZ%VxxcHp1hCxs7<~`h2KexXNLd)

diff --git a/pesterchum.js b/pesterchum.js
index 75d9d03..0f7a8d6 100644
--- a/pesterchum.js
+++ b/pesterchum.js
@@ -1 +1 @@
-{"tabs": true, "chums": ["aquaMarinist", "marineAquist", "unknownTraveler", "tentacleTherapist", "macruralAlchemist", "vaginalEngineer", "mechanicalSpectacle", "carcinoGeneticist", "schlagzeugGator", "gamblingGenocider", "fireSwallow", "gardenGnostic", "superGhost"], "defaultprofile": "ghostDunk", "block": []}
\ No newline at end of file
+{"tabs": true, "chums": ["aquaMarinist", "marineAquist", "unknownTraveler", "tentacleTherapist", "macruralAlchemist", "vaginalEngineer", "mechanicalSpectacle", "carcinoGeneticist", "schlagzeugGator", "gamblingGenocider", "gardenGnostic", "superGhost", "centaursTesticle", "arachnidsGrip", "fireSwallow", "grimAuxiliatrix"], "defaultprofile": "ghostDunk", "block": []}
\ No newline at end of file
diff --git a/pesterchum.py b/pesterchum.py
index 0ed01bd..8628fca 100644
--- a/pesterchum.py
+++ b/pesterchum.py
@@ -15,36 +15,11 @@ import pygame
 
 from pestermenus import PesterChooseQuirks, PesterChooseTheme, \
     PesterChooseProfile, PesterOptions, PesterUserlist
-from generic import RightClickList
+from pesterdata import PesterProfile, Mood, pesterQuirk, pesterQuirks
+from generic import PesterIcon, RightClickList, MultiTextDialog
 
 logging.basicConfig(level=logging.INFO)
 
-class Mood(object):
-    moods = ["chummy", "rancorous", "offline", "pleasant", "distraught", 
-             "unruly", "smooth", "ecstatic", "relaxed", "discontent", 
-             "devious", "sleek", "detestful", "mirthful", "manipulative",
-             "vigorous", "perky", "acceptant", "protective"]
-    def __init__(self, mood):
-        if type(mood) is int:
-            self.mood = mood
-        else:
-            self.mood = self.moods.index(mood)
-    def value(self):
-        return self.mood
-    def name(self):
-        try:
-            name = self.moods[self.mood]
-        except IndexError:
-            name = "chummy"
-        return name
-    def icon(self, theme):
-        
-        try:
-            f = theme["main/chums/moods"][self.name()]["icon"]
-        except KeyError:
-            return PesterIcon(theme["main/chums/moods/chummy/icon"])
-        return PesterIcon(f)
-
 _ctag_begin = re.compile(r'<c=(.*?)>')
 _ctag_rgb = re.compile(r'\d+,\d+,\d+')
 
@@ -210,53 +185,10 @@ class PesterProfileDB(dict):
         dict.__setitem__(self, key, val)
         self.save()
 
-class PesterProfileList(list):
+class PesterList(list):
     def __init__(self, l):
         self.extend(l)
 
-class PesterProfile(object):
-    def __init__(self, handle, color=None, mood=Mood("offline"), chumdb=None):
-        self.handle = handle
-        if color is None:
-            if chumdb:
-                color = chumdb.getColor(handle, QtGui.QColor("black"))
-            else:
-                color = QtGui.QColor("black")
-        self.color = color
-        self.mood = mood
-    def initials(self):
-        handle = self.handle
-        caps = [l for l in handle if l.isupper()]
-        if not caps:
-            caps = [""]
-        return (handle[0]+caps[0]).upper() 
-    def colorhtml(self):
-        return self.color.name()
-    def colorcmd(self):
-        (r, g, b, a) = self.color.getRgb()
-        return "%d,%d,%d" % (r,g,b)
-    def plaindict(self):
-        return (self.handle, {"handle": self.handle,
-                              "mood": self.mood.name(),
-                              "color": unicode(self.color.name())})
-    def blocked(self, config):
-        return self.handle in config.getBlocklist()
-
-    def pestermsg(self, otherchum, syscolor, verb):
-        return "<c=%s>-- %s <c=%s>[%s]</c> %s %s <c=%s>[%s]</c> at %s --</c>" % (syscolor.name(), self.handle, self.colorhtml(), self.initials(), verb, otherchum.handle, otherchum.colorhtml(), otherchum.initials(), datetime.now().strftime("%H:%M"))
-
-    @staticmethod
-    def checkLength(handle):
-        return len(handle) <= 256
-    @staticmethod
-    def checkValid(handle):
-        caps = [l for l in handle if l.isupper()]
-        if len(caps) != 1 or handle[0].isupper():
-            return False
-        if re.search("[^A-Za-z0-9]", handle) is not None:
-            return False
-        return True
-
 class pesterTheme(dict):
     def __init__(self, name):
         self.path = "themes/%s" % (name)
@@ -278,55 +210,6 @@ class pesterTheme(dict):
                 d[k] = s.safe_substitute(path=self.path)
         return d
 
-class pesterQuirk(object):
-    def __init__(self, quirk):
-        if type(quirk) != dict:
-            raise ValueError("Quirks must be given a dictionary")
-        self.quirk = quirk
-        self.type = self.quirk["type"]
-    def apply(self, string):
-        if self.type == "prefix":
-            return self.quirk["value"] + string
-        if self.type == "suffix":
-            return string + self.quirk["value"]
-        if self.type == "replace":
-            return string.replace(self.quirk["from"], self.quirk["to"])
-        if self.type == "regexp":
-            return re.sub(self.quirk["from"], self.quirk["to"], string)
-    def __str__(self):
-        if self.type == "prefix":
-            return "BEGIN WITH: %s" % (self.quirk["value"])
-        elif self.type == "suffix":
-            return "END WITH: %s" % (self.quirk["value"])
-        elif self.type == "replace":
-            return "REPLACE %s WITH %s" % (self.quirk["from"], self.quirk["to"])
-        elif self.type == "regexp":
-            return "REGEXP: %s REPLACED WITH %s" % (self.quirk["from"], self.quirk["to"])
-
-class pesterQuirks(object):
-    def __init__(self, quirklist):
-        self.quirklist = []
-        for q in quirklist:
-            if type(q) == dict:
-                self.quirklist.append(pesterQuirk(q))
-            elif type(q) == pesterQuirk:
-                self.quirklist.append(q)
-    def plainList(self):
-        return [q.quirk for q in self.quirklist]
-    def apply(self, string):
-        presuffix = [q for q in self.quirklist if 
-                     q.type=='prefix' or q.type=='suffix']
-        replace = [q for q in self.quirklist if
-                   q.type=='replace' or q.type=='regexp']
-        for r in replace:
-            string = r.apply(string)
-        for ps in presuffix:
-            string = ps.apply(string)
-        return string
-
-    def __iter__(self):
-        for q in self.quirklist:
-            yield q
 
 class userConfig(object):
     def __init__(self):
@@ -371,8 +254,12 @@ class userConfig(object):
         self.set('block', l)
     def set(self, item, setting):
         self.config[item] = setting
+        try:
+            jsonoutput = json.dumps(self.config)
+        except ValueError, e:
+            raise e
         fp = open("pesterchum.js", 'w')
-        json.dump(self.config, fp)
+        fp.write(jsonoutput)
         fp.close()
     def availableThemes(self):
         themes = []
@@ -432,8 +319,12 @@ class userProfile(object):
         return self.theme
     def save(self):
         handle = self.chat.handle
+        try:
+            jsonoutput = json.dumps(self.userprofile, fp)
+        except ValueError, e:
+            raise e
         fp = open("profiles/%s.js" % (handle), 'w')
-        json.dump(self.userprofile, fp)
+        fp.write(jsonoutput)
         fp.close()
     @staticmethod
     def newUserProfile(chatprofile):
@@ -444,47 +335,6 @@ class userProfile(object):
             newprofile.save()
         return newprofile
 
-class MultiTextDialog(QtGui.QDialog):
-    def __init__(self, title, parent, *queries):
-        QtGui.QDialog.__init__(self, parent)
-        self.setWindowTitle(title)
-        if len(queries) == 0:
-            return
-        self.inputs = {}
-        layout_1 = QtGui.QHBoxLayout()
-        for d in queries:
-            label = d["label"]
-            inputname = d["inputname"]
-            value = d.get("value", "")
-            l = QtGui.QLabel(label, self)
-            layout_1.addWidget(l)
-            self.inputs[inputname] = QtGui.QLineEdit(value, self)
-            layout_1.addWidget(self.inputs[inputname])
-        self.ok = QtGui.QPushButton("OK", self)
-        self.ok.setDefault(True)
-        self.connect(self.ok, QtCore.SIGNAL('clicked()'),
-                     self, QtCore.SLOT('accept()'))
-        self.cancel = QtGui.QPushButton("CANCEL", self)
-        self.connect(self.cancel, QtCore.SIGNAL('clicked()'),
-                     self, QtCore.SLOT('reject()'))
-        layout_ok = QtGui.QHBoxLayout()
-        layout_ok.addWidget(self.cancel)
-        layout_ok.addWidget(self.ok)
-
-        layout_0 = QtGui.QVBoxLayout()
-        layout_0.addLayout(layout_1)
-        layout_0.addLayout(layout_ok)
-
-        self.setLayout(layout_0)
-    def getText(self):
-        r = self.exec_()
-        if r == QtGui.QDialog.Accepted:
-            retval = {}
-            for (name, widget) in self.inputs.iteritems():
-                retval[name] = unicode(widget.text())
-            return retval
-        else:
-            return None
 
 class WMButton(QtGui.QPushButton):
     def __init__(self, icon, parent=None):
@@ -510,10 +360,16 @@ class chumListing(QtGui.QListWidgetItem):
         mood = self.chum.mood
         self.mood = mood
         self.setIcon(self.mood.icon(self.mainwindow.theme))
-        self.setTextColor(QtGui.QColor(self.mainwindow.theme["main/chums/moods"][self.mood.name()]["color"]))
+        try:
+            self.setTextColor(QtGui.QColor(self.mainwindow.theme["main/chums/moods"][self.mood.name()]["color"]))
+        except KeyError:
+            self.setTextColor(QtGui.QColor(self.mainwindow.theme["main/chums/moods/chummy/color"]))
     def changeTheme(self, theme):
         self.setIcon(self.mood.icon(theme))
-        self.setTextColor(QtGui.QColor(theme["main/chums/moods"][self.mood.name()]["color"]))
+        try:
+            self.setTextColor(QtGui.QColor(self.mainwindow.theme["main/chums/moods"][self.mood.name()]["color"]))
+        except KeyError:
+            self.setTextColor(QtGui.QColor(self.mainwindow.theme["main/chums/moods/chummy/color"]))
     def __lt__(self, cl):
         h1 = self.handle.lower()
         h2 = cl.handle.lower()
@@ -734,9 +590,12 @@ class PesterMoodHandler(QtCore.QObject):
     @QtCore.pyqtSlot(int)
     def updateMood(self, m):
         oldmood = self.mainwindow.profile().mood
-        oldbutton = self.buttons[oldmood.value()]
+        try:
+            oldbutton = self.buttons[oldmood.value()]
+            oldbutton.setSelected(False)
+        except KeyError:
+            pass
         newbutton = self.buttons[m]
-        oldbutton.setSelected(False)
         newbutton.setSelected(True)
         newmood = Mood(m)
         self.mainwindow.userprofile.chat.mood = newmood
@@ -765,21 +624,6 @@ class PesterMoodButton(QtGui.QPushButton):
         self.moodUpdated.emit(self.mood.value())
     moodUpdated = QtCore.pyqtSignal(int)
 
-class PesterIcon(QtGui.QIcon):
-    def __init__(self, *x, **y):
-        QtGui.QIcon.__init__(self, *x, **y)
-        if type(x[0]) in [str, unicode]:
-            self.icon_pixmap = QtGui.QPixmap(x[0])
-        else:
-            self.icon_pixmap = None
-    def realsize(self):
-        if self.icon_pixmap:
-            return self.icon_pixmap.size()
-        else:
-            try:
-                return self.availableSizes()[0]
-            except IndexError:
-                return None
 
 class MovingWindow(QtGui.QFrame):
     def __init__(self, *x, **y):
@@ -915,7 +759,10 @@ class PesterTabWindow(QtGui.QFrame):
         self.mainwindow.waitingMessages.addMessage(handle, func)
         # set system tray
     def clearNewMessage(self, handle):
-        i = self.tabIndices[handle]
+        try:
+            i = self.tabIndices[handle]
+        except KeyError:
+            pass
         self.tabs.setTabTextColor(i, self.defaultTabTextColor)
         self.mainwindow.waitingMessages.messageAnswered(handle)
     def changeTheme(self, theme):
@@ -1080,6 +927,7 @@ class PesterConvo(QtGui.QFrame):
             parent.addChat(self)
         if initiated:
             msg = self.mainwindow.profile().pestermsg(self.chum, QtGui.QColor(self.mainwindow.theme["convo/systemMsgColor"]), self.mainwindow.theme["convo/text/beganpester"])
+            self.setChumOpen(True)
             self.textArea.append(convertColorTags(msg))
             self.mainwindow.chatlog.log(self.chum.handle, convertColorTags(msg, "bbcode"))
         self.newmessage = False
@@ -1224,11 +1072,16 @@ class PesterWindow(MovingWindow):
         self.exitaction = exitaction
         self.connect(exitaction, QtCore.SIGNAL('triggered()'),
                      self, QtCore.SLOT('close()'))
+        userlistaction = QtGui.QAction(self.theme["main/menus/client/userlist"], self)
+        self.userlistaction = userlistaction
+        self.connect(userlistaction, QtCore.SIGNAL('triggered()'),
+                     self, QtCore.SLOT('showAllUsers()'))
         self.menu = QtGui.QMenuBar(self)
-
+        
         filemenu = self.menu.addMenu(self.theme["main/menus/client/_name"])
         self.filemenu = filemenu
         filemenu.addAction(opts)
+        filemenu.addAction(userlistaction)
         filemenu.addAction(exitaction)
 
         changetheme = QtGui.QAction(self.theme["main/menus/profile/theme"], self)
@@ -1269,6 +1122,7 @@ class PesterWindow(MovingWindow):
         self.connect(self.miniButton, QtCore.SIGNAL('clicked()'),
                      self, QtCore.SLOT('showMinimized()'))
 
+        self.namesdb = {}
         self.chumdb = PesterProfileDB()
 
         chums = [PesterProfile(c, chumdb=self.chumdb) for c in set(self.config.chums())]
@@ -1380,7 +1234,6 @@ class PesterWindow(MovingWindow):
         self.connect(convoWindow, QtCore.SIGNAL('windowClosed(QString)'),
                      self, QtCore.SLOT('closeConvo(QString)'))
         self.convos[chum.handle] = convoWindow
-        convoWindow.setChumOpen(True)
         self.newConvoStarted.emit(QtCore.QString(chum.handle), initiated)
         convoWindow.show()
     def createTabWindow(self):
@@ -1419,9 +1272,11 @@ class PesterWindow(MovingWindow):
         self.menu.move(*theme["main/menu/loc"])
         self.opts.setText(theme["main/menus/client/options"])
         self.exitaction.setText(theme["main/menus/client/exit"])
+        self.userlistaction.setText(theme["main/menus/client/userlist"])
         self.filemenu.setTitle(theme["main/menus/client/_name"])
         self.changetheme.setText(theme["main/menus/profile/theme"])
         self.changequirks.setText(theme["main/menus/profile/quirks"])
+        self.loadslum.setText(theme["main/menus/profile/block"])
         self.changecoloraction.setText(theme["main/menus/profile/color"])
         self.switch.setText(theme["main/menus/profile/switch"])
         self.profilemenu.setTitle(theme["main/menus/profile/_name"])
@@ -1469,6 +1324,11 @@ class PesterWindow(MovingWindow):
             self.alarm = pygame.mixer.Sound(theme["main/sounds/alertsound"]
 )
 
+    def addChum(self, chum):
+        self.chumList.addChum(chum)
+        self.config.addChum(chum)
+        self.moodRequest.emit(chum)
+        
     def changeTheme(self, theme):
         self.theme = theme
         # do self
@@ -1482,6 +1342,8 @@ class PesterWindow(MovingWindow):
             c.changeTheme(theme)
         if hasattr(self, 'trollslum') and self.trollslum:
             self.trollslum.changeTheme(theme)
+        if hasattr(self, 'allusers') and self.allusers:
+            self.allusers.changeTheme(theme)
         # system tray icon
         self.updateSystemTray()
 
@@ -1554,6 +1416,44 @@ class PesterWindow(MovingWindow):
         m = unicode(msg)
         self.newMessage(h, m)
 
+    @QtCore.pyqtSlot(QtCore.QString, PesterList)
+    def updateNames(self, channel, names):
+        c = unicode(channel)
+        # update name DB
+        self.namesdb[c] = names
+        # warn interested party of names
+        self.namesUpdated.emit()
+    @QtCore.pyqtSlot(QtCore.QString, QtCore.QString, QtCore.QString)
+    def userPresentUpdate(self, handle, channel, update):
+        c = unicode(channel)
+        n = unicode(handle)
+        if update == "quit":
+            for c in self.namesdb.keys():
+                try:
+                    i = self.namesdb[c].index(n)
+                    self.namesdb[c].pop(i)
+                except ValueError:
+                    pass
+                except KeyError:
+                    self.namesdb[c] = []
+        elif update == "left":
+            try:
+                i = self.namesdb[c].index(n)
+                self.namesdb[c].pop(i)
+            except ValueError:
+                pass
+            except KeyError:
+                self.namesdb[c] = []
+        elif update == "join":
+            try:
+                i = self.namesdb[c].index(n)
+            except ValueError:
+                self.namesdb[c].append(n)
+            except KeyError:
+                self.namesdb[c] = [n]
+
+        self.userPresentSignal.emit(handle, channel, update)
+
     @QtCore.pyqtSlot()
     def addChumWindow(self):
         if not hasattr(self, 'addchumdialog'):
@@ -1570,9 +1470,7 @@ class PesterWindow(MovingWindow):
                     self.addchumdialog = None
                     return
                 chum = PesterProfile(handle, chumdb=self.chumdb)
-                self.chumList.addChum(chum)
-                self.config.addChum(chum)
-                self.moodRequest.emit(chum)
+                self.addChum(chum)
             self.addchumdialog = None
     @QtCore.pyqtSlot(QtGui.QListWidgetItem)
     def removeChum(self, chumlisting):
@@ -1614,6 +1512,29 @@ class PesterWindow(MovingWindow):
         self.unblockedChum.emit(handle)
 
     @QtCore.pyqtSlot()
+    def showAllUsers(self):
+        if not hasattr(self, 'allusers'):
+            self.allusers = None
+        if not self.allusers:
+            self.allusers = PesterUserlist(self.config, self.theme, self)
+            self.connect(self.allusers, QtCore.SIGNAL('accepted()'),
+                         self, QtCore.SLOT('userListClose()'))
+            self.connect(self.allusers, QtCore.SIGNAL('rejected()'),
+                         self, QtCore.SLOT('userListClose()'))
+            self.connect(self.allusers, QtCore.SIGNAL('addChum(QString)'),
+                         self, QtCore.SLOT('userListAdd(QString)'))
+            self.requestNames.emit("#pesterchum")
+            self.allusers.show()
+
+    @QtCore.pyqtSlot(QtCore.QString)
+    def userListAdd(self, handle):
+        h = unicode(handle)
+        chum = PesterProfile(h, chumdb=self.chumdb)
+        self.addChum(chum)
+    @QtCore.pyqtSlot()
+    def userListClose(self):
+        self.allusers = None
+    @QtCore.pyqtSlot()
     def openQuirks(self):
         if not hasattr(self, 'quirkmenu'):
             self.quirkmenu = None
@@ -1744,7 +1665,7 @@ class PesterWindow(MovingWindow):
         self.connect(self.trollslum, 
                      QtCore.SIGNAL('unblockChumSignal(QString)'),
                      self, QtCore.SLOT('unblockChum(QString)'))
-        self.moodsRequest.emit(PesterProfileList(trolls))
+        self.moodsRequest.emit(PesterList(trolls))
         self.trollslum.show()
     @QtCore.pyqtSlot()
     def closeTrollSlum(self):
@@ -1803,8 +1724,11 @@ class PesterWindow(MovingWindow):
     convoClosed = QtCore.pyqtSignal(QtCore.QString)
     profileChanged = QtCore.pyqtSignal()
     moodRequest = QtCore.pyqtSignal(PesterProfile)
-    moodsRequest = QtCore.pyqtSignal(PesterProfileList)
+    moodsRequest = QtCore.pyqtSignal(PesterList)
     moodUpdated = QtCore.pyqtSignal()
+    requestNames = QtCore.pyqtSignal(QtCore.QString)
+    namesUpdated = QtCore.pyqtSignal()
+    userPresentSignal = QtCore.pyqtSignal(QtCore.QString,QtCore.QString,QtCore.QString)
     mycolorUpdated = QtCore.pyqtSignal()
     trayIconSignal = QtCore.pyqtSignal(int)
     blockedChum = QtCore.pyqtSignal(QtCore.QString)
@@ -1823,7 +1747,7 @@ class PesterIRC(QtCore.QObject):
     @QtCore.pyqtSlot(PesterProfile)
     def getMood(self, *chums):
         self.cli.command_handler.getMood(*chums)
-    @QtCore.pyqtSlot(PesterProfileList)
+    @QtCore.pyqtSlot(PesterList)
     def getMoods(self, chums):
         self.cli.command_handler.getMood(*chums)
         
@@ -1865,6 +1789,10 @@ class PesterIRC(QtCore.QObject):
     def unblockedChum(self, handle):
         h = unicode(handle)
         helpers.msg(self.cli, h, "PESTERCHUM:UNBLOCK")
+    @QtCore.pyqtSlot(QtCore.QString)
+    def requestNames(self, channel):
+        c = unicode(channel)
+        helpers.names(self.cli, c)
 
     def updateIRC(self):
         self.conn.next()
@@ -1872,7 +1800,10 @@ class PesterIRC(QtCore.QObject):
     moodUpdated = QtCore.pyqtSignal(QtCore.QString, Mood)
     colorUpdated = QtCore.pyqtSignal(QtCore.QString, QtGui.QColor)
     messageReceived = QtCore.pyqtSignal(QtCore.QString, QtCore.QString)
+    namesReceived = QtCore.pyqtSignal(QtCore.QString, PesterList)
     nickCollision = QtCore.pyqtSignal(QtCore.QString, QtCore.QString)
+    userPresentUpdate = QtCore.pyqtSignal(QtCore.QString, QtCore.QString,
+                                   QtCore.QString)
 
 class PesterHandler(DefaultCommandHandler):
     def privmsg(self, nick, chan, msg):
@@ -1929,13 +1860,16 @@ class PesterHandler(DefaultCommandHandler):
         self.parent.nickCollision.emit(nick, newnick)
     def quit(self, nick, reason):
         handle = nick[0:nick.find("!")]
+        self.parent.userPresentUpdate.emit(handle, "", "quit")
         self.parent.moodUpdated.emit(handle, Mood("offline"))        
     def part(self, nick, channel, reason="nanchos"):
         handle = nick[0:nick.find("!")]
+        self.parent.userPresentUpdate.emit(handle, channel, "left")
         if channel == "#pesterchum":
             self.parent.moodUpdated.emit(handle, Mood("offline"))
     def join(self, nick, channel):
         handle = nick[0:nick.find("!")]
+        self.parent.userPresentUpdate.emit(handle, channel, "join")
         if channel == "#pesterchum":
             self.parent.moodUpdated.emit(handle, Mood("chummy"))
     def nick(self, oldnick, newnick):
@@ -1944,6 +1878,19 @@ class PesterHandler(DefaultCommandHandler):
         self.parent.moodUpdated.emit(oldhandle, Mood("offline"))        
         if newnick in self.mainwindow.chumList.chums:
             self.getMood(newchum)
+    def namreply(self, server, nick, op, channel, names):
+        namelist = names.split(" ")
+        logging.info("---> recv \"NAMES %s: %d names\"" % (channel, len(namelist)))
+        if not hasattr(self, 'channelnames'):
+            self.channelnames = {}
+        if not self.channelnames.has_key(channel):
+            self.channelnames[channel] = []
+        self.channelnames[channel].extend(namelist)
+    def endofnames(self, server, nick, channel, msg):
+        namelist = self.channelnames[channel]
+        pl = PesterList(namelist)
+        del self.channelnames[channel]
+        self.parent.namesReceived.emit(channel, pl)
         
     def getMood(self, *chums):
         chumglub = "GETMOOD "
@@ -2042,6 +1989,12 @@ def main():
                 QtCore.SIGNAL('unblockedChum(QString)'),
                 irc,
                 QtCore.SLOT('unblockedChum(QString)'))
+    irc.connect(widget,
+                QtCore.SIGNAL('requestNames(QString)'),
+                irc,
+                QtCore.SLOT('requestNames(QString)'))
+
+# IRC --> Main window
     irc.connect(irc, 
                 QtCore.SIGNAL('moodUpdated(QString, PyQt_PyObject)'),
                 widget, 
@@ -2058,6 +2011,14 @@ def main():
                 QtCore.SIGNAL('nickCollision(QString, QString)'),
                 widget,
                 QtCore.SLOT('nickCollision(QString, QString)'))
+    irc.connect(irc,
+                QtCore.SIGNAL('namesReceived(QString, PyQt_PyObject)'),
+                widget,
+                QtCore.SLOT('updateNames(QString, PyQt_PyObject)'))
+    irc.connect(irc,
+                QtCore.SIGNAL('userPresentUpdate(QString, QString, QString)'),
+                widget,
+                QtCore.SLOT('userPresentUpdate(QString, QString, QString)'))
 
     ircapp = IRCThread(irc)
     ircapp.start()
diff --git a/pesterdata.py b/pesterdata.py
new file mode 100644
index 0000000..6afc9c7
--- /dev/null
+++ b/pesterdata.py
@@ -0,0 +1,125 @@
+from PyQt4 import QtGui, QtCore
+from datetime import *
+import re
+
+from generic import PesterIcon
+
+class Mood(object):
+    moods = ["chummy", "rancorous", "offline", "pleasant", "distraught", 
+             "unruly", "smooth", "ecstatic", "relaxed", "discontent", 
+             "devious", "sleek", "detestful", "mirthful", "manipulative",
+             "vigorous", "perky", "acceptant", "protective"]
+    def __init__(self, mood):
+        if type(mood) is int:
+            self.mood = mood
+        else:
+            self.mood = self.moods.index(mood)
+    def value(self):
+        return self.mood
+    def name(self):
+        try:
+            name = self.moods[self.mood]
+        except IndexError:
+            name = "chummy"
+        return name
+    def icon(self, theme):
+        
+        try:
+            f = theme["main/chums/moods"][self.name()]["icon"]
+        except KeyError:
+            return PesterIcon(theme["main/chums/moods/chummy/icon"])
+        return PesterIcon(f)
+
+class pesterQuirk(object):
+    def __init__(self, quirk):
+        if type(quirk) != dict:
+            raise ValueError("Quirks must be given a dictionary")
+        self.quirk = quirk
+        self.type = self.quirk["type"]
+    def apply(self, string):
+        if self.type == "prefix":
+            return self.quirk["value"] + string
+        if self.type == "suffix":
+            return string + self.quirk["value"]
+        if self.type == "replace":
+            return string.replace(self.quirk["from"], self.quirk["to"])
+        if self.type == "regexp":
+            return re.sub(self.quirk["from"], self.quirk["to"], string)
+    def __str__(self):
+        if self.type == "prefix":
+            return "BEGIN WITH: %s" % (self.quirk["value"])
+        elif self.type == "suffix":
+            return "END WITH: %s" % (self.quirk["value"])
+        elif self.type == "replace":
+            return "REPLACE %s WITH %s" % (self.quirk["from"], self.quirk["to"])
+        elif self.type == "regexp":
+            return "REGEXP: %s REPLACED WITH %s" % (self.quirk["from"], self.quirk["to"])
+
+class pesterQuirks(object):
+    def __init__(self, quirklist):
+        self.quirklist = []
+        for q in quirklist:
+            if type(q) == dict:
+                self.quirklist.append(pesterQuirk(q))
+            elif type(q) == pesterQuirk:
+                self.quirklist.append(q)
+    def plainList(self):
+        return [q.quirk for q in self.quirklist]
+    def apply(self, string):
+        presuffix = [q for q in self.quirklist if 
+                     q.type=='prefix' or q.type=='suffix']
+        replace = [q for q in self.quirklist if
+                   q.type=='replace' or q.type=='regexp']
+        for r in replace:
+            string = r.apply(string)
+        for ps in presuffix:
+            string = ps.apply(string)
+        return string
+
+    def __iter__(self):
+        for q in self.quirklist:
+            yield q
+
+class PesterProfile(object):
+    def __init__(self, handle, color=None, mood=Mood("offline"), chumdb=None):
+        self.handle = handle
+        if color is None:
+            if chumdb:
+                color = chumdb.getColor(handle, QtGui.QColor("black"))
+            else:
+                color = QtGui.QColor("black")
+        self.color = color
+        self.mood = mood
+    def initials(self):
+        handle = self.handle
+        caps = [l for l in handle if l.isupper()]
+        if not caps:
+            caps = [""]
+        return (handle[0]+caps[0]).upper() 
+    def colorhtml(self):
+        return self.color.name()
+    def colorcmd(self):
+        (r, g, b, a) = self.color.getRgb()
+        return "%d,%d,%d" % (r,g,b)
+    def plaindict(self):
+        return (self.handle, {"handle": self.handle,
+                              "mood": self.mood.name(),
+                              "color": unicode(self.color.name())})
+    def blocked(self, config):
+        return self.handle in config.getBlocklist()
+
+    def pestermsg(self, otherchum, syscolor, verb):
+        return "<c=%s>-- %s <c=%s>[%s]</c> %s %s <c=%s>[%s]</c> at %s --</c>" % (syscolor.name(), self.handle, self.colorhtml(), self.initials(), verb, otherchum.handle, otherchum.colorhtml(), otherchum.initials(), datetime.now().strftime("%H:%M"))
+
+    @staticmethod
+    def checkLength(handle):
+        return len(handle) <= 256
+    @staticmethod
+    def checkValid(handle):
+        caps = [l for l in handle if l.isupper()]
+        if len(caps) != 1 or handle[0].isupper():
+            return False
+        if re.search("[^A-Za-z0-9]", handle) is not None:
+            return False
+        return True
+
diff --git a/pesterdata.pyc b/pesterdata.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..c8d9f77b22d91cb5688a140d4a0d5050f6492f2d
GIT binary patch
literal 7111
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
H^OgSq-{T{y

literal 0
HcmV?d00001

diff --git a/pestermenus.py b/pestermenus.py
index 989d113..c3d46e5 100644
--- a/pestermenus.py
+++ b/pestermenus.py
@@ -1,6 +1,8 @@
 from PyQt4 import QtGui, QtCore
+import re
 
-from generic import RightClickList
+from generic import RightClickList, MultiTextDialog
+from pesterdata import pesterQuirk, PesterProfile
 
 class PesterQuirkItem(QtGui.QListWidgetItem):
     def __init__(self, quirk, parent):
@@ -120,6 +122,15 @@ class PesterChooseQuirks(QtGui.QDialog):
     def addRegexpDialog(self):
         vdict = MultiTextDialog("REGEXP REPLACE", self, {"label": "Regexp:", "inputname": "from"}, {"label": "Replace With:", "inputname": "to"}).getText()
         vdict["type"] = "regexp"
+        try:
+            re.compile(vdict["from"])
+        except re.error, e:
+            quirkWarning = QtGui.QMessageBox(self)
+            quirkWarning.setText("Not a valid regular expression!")
+            quirkWarning.setInformativeText("H3R3S WHY DUMP4SS: %s" % (e))
+            quirkWarning.exec_()
+            return
+            
         quirk = pesterQuirk(vdict)
         item = PesterQuirkItem(quirk, self.quirkList)
         self.quirkList.addItem(item)
@@ -296,21 +307,82 @@ class PesterUserlist(QtGui.QDialog):
         self.setModal(False)
         self.config = config
         self.theme = theme
+        self.mainwindow = parent
         self.setStyleSheet(self.theme["main/defaultwindow/style"])
-        self.resize([200, 600])
+        self.resize(200, 600)
 
         self.label = QtGui.QLabel("USERLIST")
         self.userarea = RightClickList(self)
         self.userarea.setStyleSheet(self.theme["main/chums/style"])
+        self.userarea.optionsMenu = QtGui.QMenu(self)
+
+        self.addChumAction = QtGui.QAction(self.mainwindow.theme["main/menus/rclickchumlist/addchum"], self)
+        self.connect(self.addChumAction, QtCore.SIGNAL('triggered()'),
+                     self, QtCore.SLOT('addChumSlot()'))
+        self.userarea.optionsMenu.addAction(self.addChumAction)
 
         self.ok = QtGui.QPushButton("OK", self)
         self.ok.setDefault(True)
         self.connect(self.ok, QtCore.SIGNAL('clicked()'),
                      self, QtCore.SLOT('accept()'))
 
-        layout_0 = QVBoxLayout()
-        layout.addWidget(self.label)
-        layout.addWidget(self.userarea)
-        layout.addWidget(self.ok)
+        layout_0 = QtGui.QVBoxLayout()
+        layout_0.addWidget(self.label)
+        layout_0.addWidget(self.userarea)
+        layout_0.addWidget(self.ok)
         
-        self.setLayout(layout)
+        self.setLayout(layout_0)
+
+        self.connect(self.mainwindow, QtCore.SIGNAL('namesUpdated()'),
+                     self, QtCore.SLOT('updateUsers()'))
+
+        self.connect(self.mainwindow, 
+                     QtCore.SIGNAL('userPresentSignal(QString, QString, QString)'),
+                     self, 
+                     QtCore.SLOT('updateUserPresent(QString, QString, QString)'))
+        self.updateUsers()
+    @QtCore.pyqtSlot()
+    def updateUsers(self):
+        names = self.mainwindow.namesdb["#pesterchum"]
+        self.userarea.clear()
+        for n in names:
+            item = QtGui.QListWidgetItem(n)
+            item.setTextColor(QtGui.QColor(self.theme["main/chums/moods/chummy/color"]))
+            self.userarea.addItem(item)
+        self.userarea.sortItems()
+    @QtCore.pyqtSlot(QtCore.QString, QtCore.QString, QtCore.QString)
+    def updateUserPresent(self, handle, channel, update):
+        h = unicode(handle)
+        c = unicode(channel)
+        if update == "quit":
+            self.delUser(h)
+        elif update == "left" and c == "#pesterchum":
+            self.delUser(h)
+        elif update == "join" and c == "#pesterchum":
+            self.addUser(h)
+    def addUser(self, name):
+        item = QtGui.QListWidgetItem(name)
+        item.setTextColor(QtGui.QColor(self.theme["main/chums/moods/chummy/color"]))
+        self.userarea.addItem(item)
+        self.userarea.sortItems()
+    def delUser(self, name):
+        matches = self.userarea.findItems(name, QtCore.Qt.MatchFlags(0))
+        for m in matches:
+            self.userarea.takeItem(self.userarea.row(m))
+
+    def changeTheme(self, theme):
+        self.setStyleSheet(theme["main/defaultwindow/style"])
+        self.userarea.setStyleSheet(theme["main/chums/style"])
+        self.addChumAction.setText(theme["main/menus/rclickchumlist/addchum"])
+        for item in [self.userarea.row(i) for i in range(0, self.userarea.count())]:
+            item.setTextColor(QtGui.QColor(self.theme["main/chums/moods/chummy/color"]))
+
+    @QtCore.pyqtSlot()
+    def addChumSlot(self):
+        cur = self.userarea.currentItem()
+        if not cur:
+            return
+        self.addChum.emit(cur.text())
+
+    addChum = QtCore.pyqtSignal(QtCore.QString)
+
diff --git a/pestermenus.pyc b/pestermenus.pyc
index b7364c6decfbe344ae94abbd3bfb2541c7a641c6..93bfb16da4878ce3d26a05da33a4cac0440d2f24 100644
GIT binary patch
delta 5737
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
NGh=1OGgmT0{{?&k0A2t9

delta 2837
zcmbVO+fQ6Y7@s-pF6^=_%d*^|g>vbE(n?#kR%uJ2v89C;hPEgKaoKa&J>{^w?hY;W
zhNfN~>O;visfn-ppoz69FUA-X-+i$09upJ)18t1@rt|xjE?Y|C1;USS=KE&8`*-I1
z7v7v}zH_%CF!jgqXq>J8c=i7Q{g?ZJtv0u<xE?m;VcaTVHfN5P&2wgX*lUbkVeIKC
zAG5r~C?$rkz$jx@DKY%SC@U}m%<>m80tH4nv&xGY!93%TRuW`ZMG+@dI8wo^$|6R%
zzz8v`icOdi)y8+!n_PP%LmGsWNXjACi7$*eIjL^&-?&e`Wt{ZFXP^4f_*k#6mvnN!
z`lMu%m#c{9Q6G4sTJ^OY^Ok7RyLQ8KgmbU@&U=cNsCHj_xgQb@fPfnJou=Dr)VscG
z0dOSLJ8HG6Up-%1b)Z$Ray|{r2=WSUf)r6e4S=+f!DV`xx%`}*aMMzBkp5b|6<sSY
zYxJIkoP<cumZNDa=87up;I?==HSZ)ONhIw>J8xPe*a(6?FzKYM^rf?obZt?q(iL}k
zSj~k7!sJM=mJz!F0d+f+Aa8c6ZIyR+KZGlAZ9Kh{lA;wdP-r49f_(L8YIb_>jA&P@
z%}2b=dO4+@4S%~*4v|U?W~l}VYIMn!&RKg|9&=)DIw8SG*b+Mu;u$+9?d<rHlU)#`
z0s2J?V4DULy7F3KQ^s=QQs5k!m~o`NXzCRsP>a63>Wiu<pH)9seL8(y?~4<FVF1-e
z!8maed#3;+fPdC~k#tigTeaVZwWcz4xH`xaT3d@sR)4qvjS?2mXVZ(MH3zFF9hrPw
z8epbHQ2*PA(SI^SjNxpN1)oKmf@!^%!==nPpw3snr|#CYE|TjLkcR8I3Q%^&jm7O&
zGDe)m4sHLxh&*754#qiQC+y`+KGBz;cik)=;6WbYVQvPXc2ggqd<2?7bOO!*#sTL5
z=cy;UG}*P*TK|>7O*PiLop-Bf(=*$9Ab|*rUO>K{L7@-C9<|yu!dui&O>NVC*mwfa
z4+sPH0Z8$FP!9tR0UiS!1RMq&0Ssu!oBu%nOf`2^wQDxb04W9`K17`X(_U5IG>?@R
z`AiN?YR)Tbms_6jHluWN=<ACy*Rf*K9?PcZ9oIIyMYsBL$LB>}HfgE6m*g)+vr*k@
zf28p#&7ppaCzlpa#8Q@PkHqF|SM;gB+Ut3}s_)o8yp_WRUnNY4U5q(yY|g!BLrzTL
zVkpYwtr|=(ixzdg<HXFSXj0?~@uI0^XS3<-XfBZt9`zpukKDLFV-k#b+I5|rlTL}F
z>R@M&Z#|Mu=RD1)57xf!JkR&j>7*7H7XS$W<$DIy0suW>qTgt8hzr!N@9d27U)FBz
zH2C<UW=Rxhd?}l?Q*t<E*~_8~2Q0AA!V(h-9Wq7}YCS15v*Jlxe_ZTTLMBBHhf3A&
zX1kAkOOdXb?&y<$YECYqff;SQMpu8BdkOvgv`M-)iUJ02d=>JI@G|yZr)rtg<omaK
z&TLeFrnjkMqxyG26!|Oa0}4q-ebn2%Le)+c0<}`-mrA3MdeMiY8~NO-PVP@{KBJ{L
zwz(a!cNBmLAci!k>&^X?K(Bgx_uvDq@P-!gs6BhSRCkXVLd#&n;b3l;n(Y~0c~O5^
z^-IHG`gxKt*cHyW)LVW$QS?HyX9G+pMicAHAZRDZoKhm_{nBvnoPJOENY(((hDapD
zoJuCcbUDset?$YROFGqf=H#!|YbVOAGAys?7n7{P6X_)nleNsLqeB6@WMduNv<}f4
zO6qY6X6d4vF#YOgW1Bj#x7QGuaoY}>^xDvIVB%k-x*l>!vN*1;?W>vDoKz};tul+A
zr6+|OyPRH<`N21!H%*$XTjiIg+aTCodVc0QG{Jbae(3o1Z<JRX5njpb3>1C6Uiqo)
z!$$rIu~GG26nEsRKHQ0G(6Bz;8<pzU{<?wnmqN<UI&ne0jIPXor|UrBos<s@z77Ht
kRiIG>3QAnncMqwXhayopUTcJnhRTLW!&F0e!*oO2-`26&LI3~&

diff --git a/profiles/ghostDunk.js b/profiles/ghostDunk.js
index 338c689..43ce8df 100644
--- a/profiles/ghostDunk.js
+++ b/profiles/ghostDunk.js
@@ -1 +1 @@
-{"color": "#ff00ff", "theme": "pesterchum", "quirks": [], "handle": "ghostDunk"}
\ No newline at end of file
+{"color": "#ff00ff", "theme": "trollian", "quirks": [], "handle": "ghostDunk"}
\ No newline at end of file
diff --git a/themes/pesterchum/style.js b/themes/pesterchum/style.js
index 0880907..5657a8b 100644
--- a/themes/pesterchum/style.js
+++ b/themes/pesterchum/style.js
@@ -17,6 +17,7 @@
   "sounds": { "alertsound": "$path/alarm.wav" },
   "menus": {"client": {"_name": "CLIENT",
                        "options": "OPTIONS",
+                       "userlist": "USERLIST",
                        "exit": "EXIT"},
             "profile": {"_name": "PROFILE",
                         "switch": "SWITCH",
@@ -27,7 +28,8 @@
             "rclickchumlist": {"pester": "PESTER",
                                "removechum": "REMOVE CHUM",
                                "blockchum": "BLOCK",
-                               "unblockchum": "UNBLOCK"
+                               "unblockchum": "UNBLOCK",
+                               "addchum": "Add Chump"
                               }
            },
   "chums": { "style": "border:2px solid yellow; background-color: black;color: white;font: bold;font-family: 'Courier';selection-background-color:#646464; ",
diff --git a/themes/trollian/style.js b/themes/trollian/style.js
index fac09e0..c492853 100644
--- a/themes/trollian/style.js
+++ b/themes/trollian/style.js
@@ -25,6 +25,7 @@
   "sounds": { "alertsound": "$path/alarm.wav" },
   "menus": {"client": {"_name": "Trollian",
                        "options": "Options",
+                       "userlist": "Target List",
                        "exit": "Abscond"},
             "profile": {"_name": "View",
                         "switch": "Trolltag",
@@ -35,7 +36,8 @@
             "rclickchumlist": {"pester": "Troll",
                                "removechum": "Trash",
                                "blockchum": "Block",
-                               "unblockchum": "Mercy"}
+                               "unblockchum": "Mercy",
+                               "addchum": "Add Chump"}
            },
   "chums": { "style": "border: 0px; background-color: white; padding: 5px; font-family: 'Arial';selection-background-color:rgb(200,200,200); ",
              "loc": [476, 90],