Added pgconsole. It is my older project, a GUI query console. It uses GTK+ and asynchronous queries.
authorRadek Brich <radek.brich@devl.cz>
Tue, 16 Aug 2011 23:53:54 +0200
changeset 10 f3a1b9792cc9
parent 9 2fcc8ef0b97d
child 11 bc69eca59041
Added pgconsole. It is my older project, a GUI query console. It uses GTK+ and asynchronous queries.
data/Throbber.gif
pgconsole.py
pgconsole/__init__.py
pgconsole/config.py
pgconsole/database.py
pgconsole/dataview.py
pgconsole/editor.py
pgconsole/panedext.py
pgconsole/settings.py
pgtoolkit/config.py
tests/testprettysize.py
Binary file data/Throbber.gif has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pgconsole.py	Tue Aug 16 23:53:54 2011 +0200
@@ -0,0 +1,500 @@
+#!/usr/bin/env python
+
+import time
+import gobject, gtk, pango, cairo
+import gtksourceview2 as gtksourceview
+
+import psycopg2
+import psycopg2.extensions
+import psycopg2.extras
+
+from pgconsole.config import cfg
+from pgconsole.editor import Editor
+from pgconsole.dataview import DataView
+from pgconsole.database import Database, BadConnectionError, DatabaseError
+from pgconsole.settings import Settings
+from pgconsole.panedext import HPanedExt, VPanedExt
+
+
+class PgConsoleApp:
+    def __init__(self):
+        self.db = Database()
+        self.conn = None
+
+        win = gtk.Window(gtk.WINDOW_TOPLEVEL)
+        self.win = win
+        win.set_title('PostgreSQL Console')
+        win.set_size_request(300, 200)  # minimal size
+        win.set_default_size(800, 600)
+        self.restore_window_size()
+        win.connect("destroy", self.destroy)
+        win.connect("key_press_event", self.keypress)
+        win.connect('configure-event', self.on_configure)
+
+        # toolbar
+        toolbar = gtk.Toolbar()
+        toolbar.set_style(gtk.TOOLBAR_ICONS)
+        toolbar.set_property("icon-size", gtk.ICON_SIZE_SMALL_TOOLBAR)
+
+        tb = gtk.ToolButton(gtk.STOCK_PREFERENCES)
+        tb.set_label('Settings')
+        tb.set_tooltip_text('Settings')
+        tb.connect('clicked', self.settings)
+        self.tb_settings = tb
+        toolbar.add(tb)
+
+        self.cb_server = gtk.combo_box_entry_new_text()
+        self.cb_server.set_tooltip_text('Server')
+        self.cb_server.get_child().set_property("editable", False)
+        self.cb_server.set_property("add_tearoffs", True)
+        #self.cb_server.set_property("focus-on-click", False)
+        self.cb_server.set_property("can-focus", True)
+        self.cb_server.connect('changed', self.on_server_changed)
+        self.cb_server.connect("key_press_event", self.toolbar_server_keypress)
+        ti = gtk.ToolItem()
+        ti.add(self.cb_server)
+        toolbar.add(ti)
+
+        self.cb_dbname = gtk.combo_box_entry_new_text()
+        self.cb_dbname.set_tooltip_text('Database')
+        self.cb_dbname.get_child().set_property("editable", False)
+        self.cb_dbname.set_property("add_tearoffs", True)
+        #self.cb_dbname.set_property("focus-on-click", False)
+        self.cb_dbname.set_property("can-focus", True)
+        ti = gtk.ToolItem()
+        ti.add(self.cb_dbname)
+        toolbar.add(ti)
+
+        tb = gtk.ToolButton(gtk.STOCK_CONNECT)
+        tb.set_label('Connect')
+        tb.set_tooltip_text('Connect')
+        tb.connect('clicked', self.connect)
+        toolbar.add(tb)
+        self.tb_connect = tb
+
+        sep = gtk.SeparatorToolItem()
+        toolbar.add(sep)
+
+        tb = gtk.ToolButton(gtk.STOCK_EXECUTE)
+        tb.set_label('Execute')
+        tb.set_tooltip_text('Execute')
+        tb.connect('clicked', self.execute)
+        toolbar.add(tb)
+        tb.set_sensitive(False)
+        self.tb_execute = tb
+        tb = gtk.ToolButton(gtk.STOCK_NEW)
+        tb.set_label('Begin transaction')
+        tb.set_tooltip_text('Begin transaction')
+        tb.connect('clicked', self.begin)
+        tb.set_sensitive(False)
+        toolbar.add(tb)
+        self.tb_begin = tb
+        tb = gtk.ToolButton(gtk.STOCK_APPLY)
+        tb.set_label('Commit')
+        tb.set_tooltip_text('Commit')
+        tb.connect('clicked', self.commit)
+        tb.set_sensitive(False)
+        self.tb_commit = tb
+        toolbar.add(tb)
+        tb = gtk.ToolButton(gtk.STOCK_CANCEL)
+        tb.set_label('Rollback')
+        tb.set_tooltip_text('Rollback')
+        tb.connect('clicked', self.rollback)
+        tb.set_sensitive(False)
+        self.tb_rollback = tb
+        toolbar.add(tb)
+
+        sep = gtk.SeparatorToolItem()
+        toolbar.add(sep)
+
+        # editor
+        self.editor = Editor()
+
+        # data view
+        self.dataview = DataView()
+
+
+        vbox = gtk.VBox(False, 2)
+
+        sep = gtk.SeparatorToolItem()
+        sep.set_expand(True)
+        sep.set_draw(False)
+        toolbar.add(sep)
+
+        self.throbber_anim = gtk.gdk.PixbufAnimation('data/Throbber.gif')
+        self.throbber = gtk.Image()
+        tb = gtk.ToolItem()
+        tb.add(self.throbber)
+        toolbar.add(tb)
+        sep = gtk.SeparatorToolItem()
+        sep.set_draw(False)
+        toolbar.add(sep)
+
+        vbox.pack_start(toolbar, False, False, 0)
+
+        vpaned = VPanedExt()
+        vpaned.set_border_width(5)
+
+        hpaned = HPanedExt()
+        hpaned.set_border_width(0)
+        hpaned.add1(self.editor)
+        hpaned.child_set_property(self.editor, 'shrink', False)
+        hpaned.set_snap2(80)
+        hpaned.set_property('position', 500)
+
+        vpaned.add1(hpaned)
+        vpaned.set_snap1(80)
+        vpaned.set_property('position', 300)
+
+        vpaned.add2(self.dataview)
+        vpaned.set_snap2(80)
+
+        self.vpaned = vpaned
+        self.hpaned = hpaned
+
+        # log
+        self.logbuf = gtk.TextBuffer()
+        view = gtk.TextView(self.logbuf)
+        view.set_editable(False)
+        font_desc = pango.FontDescription('monospace')
+        if font_desc:
+            view.modify_font(font_desc)
+
+        sw = gtk.ScrolledWindow()
+        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+        sw.set_shadow_type(gtk.SHADOW_ETCHED_IN)
+        sw.add(view)
+        hpaned.add2(sw)
+
+        vbox.pack_start(vpaned, padding=0)
+
+        win.add(vbox)
+
+        self.editor.view.grab_focus()
+
+        self.reload_server_list(0)
+
+        win.show_all()
+
+        self.restore_win_state()
+
+
+    def main(self):
+        gtk.main()
+
+
+    def destroy(self, widget, data=None):
+        self.save_win_state()
+        cfg.save()
+        gtk.main_quit()
+
+
+    def on_configure(self, w, ev):
+        cfg.root.window.size.width = ev.width
+        cfg.root.window.size.height = ev.height
+
+
+    def restore_window_size(self):
+        self.win.resize(cfg.root.window.size.width, cfg.root.window.size.height)
+
+
+    def save_win_state(self):
+        cfg.root.window.dividers.verticaldivider = self.vpaned.get_position()
+        cfg.root.window.dividers.horizontaldivider = self.hpaned.get_position()
+        cfg.root.window.dividers.editordivider = self.editor.get_position()
+
+
+    def restore_win_state(self):
+        pos = cfg.root.window.dividers.verticaldivider
+        if pos >= 0:
+            self.vpaned.set_position(pos)
+        pos = cfg.root.window.dividers.horizontaldivider
+        if pos >= 0:
+            self.hpaned.set_position(pos)
+        pos = cfg.root.window.dividers.editordivider
+        if pos >= 0:
+            self.editor.set_position(pos)
+
+
+    def get_typename(self, oid, size):
+        self.curs.execute('SELECT typname FROM pg_type WHERE oid=%s', [oid])
+        psycopg2.extras.wait_select(self.curs.connection)
+        row = self.curs.fetchone()
+
+        typname = None
+        if row:
+            typname = row[0]
+            if typname == 'int4':
+                return 'integer'
+            if typname in ('timestamp', 'interval', 'date'):
+                return typname
+            if size and size > 0 and size < 65535:
+                typname += '(%s)' % size
+        return typname
+
+
+    def get_conninfo(self, nodb=False):
+        sel = self.cb_server.get_active()
+        srv = cfg.servers.server[sel]
+        if nodb:
+            dbname = 'postgres'
+        else:
+            dbname = self.cb_dbname.get_active_text()
+        conninfo = 'host=%s port=%s dbname=%s user=%s password=%s' \
+            % (srv.host, srv.port, dbname, srv.user, srv.password)
+        return conninfo
+
+
+    def connect(self, w):
+        conninfo = self.get_conninfo()
+        if self.conn:
+            # disconnect
+            self.db.put_conn(conninfo, self.conn)
+            self.conn = None
+            self.cb_server.set_sensitive(True)
+            self.cb_dbname.set_sensitive(True)
+            self.tb_connect.set_stock_id(gtk.STOCK_CONNECT)
+            self.tb_connect.set_label('Connect')
+            self.tb_connect.set_tooltip_text('Connect')
+            self.tb_execute.set_sensitive(False)
+            self.tb_begin.set_sensitive(False)
+        else:
+            # connect
+            self.logbuf.insert(self.logbuf.get_end_iter(), 'Connect %s\n' % conninfo)
+            try:
+                self.conn = self.db.get_conn(conninfo)
+            except DatabaseError, e:
+                self.logbuf.insert(self.logbuf.get_end_iter(), 'Error:\n%s\n' % e)
+                return
+            self.cb_server.set_sensitive(False)
+            self.cb_dbname.set_sensitive(False)
+            self.tb_connect.set_stock_id(gtk.STOCK_DISCONNECT)
+            self.tb_connect.set_label('Disconnect')
+            self.tb_connect.set_tooltip_text('Disconnect')
+            self.tb_execute.set_sensitive(True)
+            self.tb_begin.set_sensitive(True)
+
+
+    def begin(self, w):
+        self.logbuf.insert(self.logbuf.get_end_iter(), 'Begin transaction\n')
+        curs = self.conn.cursor()
+        curs.execute('BEGIN')
+        psycopg2.extras.wait_select(curs.connection)
+
+        self.tb_connect.set_sensitive(False)
+        self.tb_begin.set_sensitive(False)
+        self.tb_commit.set_sensitive(True)
+        self.tb_rollback.set_sensitive(True)
+
+
+    def commit(self, w):
+        self.logbuf.insert(self.logbuf.get_end_iter(), 'Commit\n')
+        curs = self.conn.cursor()
+        curs.execute('COMMIT')
+        psycopg2.extras.wait_select(curs.connection)
+
+        self.tb_connect.set_sensitive(True)
+        self.tb_begin.set_sensitive(True)
+        self.tb_commit.set_sensitive(False)
+        self.tb_rollback.set_sensitive(False)
+
+
+    def rollback(self, w):
+        self.logbuf.insert(self.logbuf.get_end_iter(), 'Rollback\n')
+        curs = self.conn.cursor()
+        curs.execute('ROLLBACK')
+        psycopg2.extras.wait_select(curs.connection)
+
+        self.tb_connect.set_sensitive(True)
+        self.tb_begin.set_sensitive(True)
+        self.tb_commit.set_sensitive(False)
+        self.tb_rollback.set_sensitive(False)
+
+
+    def execute(self, widget):
+        query = self.editor.get_selection() or self.editor.get_text()
+
+        self.tb_connect.set_sensitive(False)
+        self.tb_execute.set_sensitive(False)
+        self.tb_begin.set_sensitive(False)
+        self.tb_commit.set_sensitive(False)
+        self.tb_rollback.set_sensitive(False)
+        self.throbber.set_from_animation(self.throbber_anim)
+
+        self.curs = self.conn.cursor()
+
+        self.t1 = time.time()
+        try:
+            self.curs.execute(query)
+        except (psycopg2.OperationalError, psycopg2.DatabaseError), e:
+            self.logbuf.insert(self.logbuf.get_end_iter(), 'Error:\n' + str(e))
+            return
+
+        self.execute_poll()
+
+
+    def execute_poll(self, source=None, cond=None):
+        try:
+            state = self.conn.poll()
+        except (psycopg2.OperationalError, psycopg2.DatabaseError), e:
+            self.logbuf.insert(self.logbuf.get_end_iter(), 'Error:\n' + str(e))
+            return
+
+        if state == psycopg2.extensions.POLL_OK:
+            self.execute_finish()
+        elif state == psycopg2.extensions.POLL_WRITE:
+            gobject.io_add_watch(self.conn.fileno(), gobject.IO_OUT, self.execute_poll)
+        elif state == psycopg2.extensions.POLL_READ:
+            gobject.io_add_watch(self.conn.fileno(), gobject.IO_IN, self.execute_poll)
+        else:
+            self.logbuf.insert(self.logbuf.get_end_iter(), "poll() returned %s" % state)
+            return
+
+
+    def execute_finish(self):
+        t2 = time.time()
+        t = (t2 - self.t1)*1000
+
+        self.throbber.clear()
+
+        self.logbuf.insert(self.logbuf.get_end_iter(),
+            'Query successful (%d ms, %d rows)\n' % (t, self.curs.rowcount))
+
+        # notices
+        for n in self.conn.notices:
+            self.logbuf.insert(self.logbuf.get_end_iter(), n)
+
+        if self.curs.rowcount >= 0:
+            rows = self.curs.fetchall()
+
+            names = []
+            for c in self.curs.description:
+                name = c[0]
+                typename = self.get_typename(c[1], c[3])
+                names += [(name, typename)]
+
+            self.dataview.load_data(names, rows)
+
+        self.tb_execute.set_sensitive(True)
+        if self.conn.get_transaction_status() == psycopg2.extensions.TRANSACTION_STATUS_INTRANS:
+            self.tb_commit.set_sensitive(True)
+            self.tb_rollback.set_sensitive(True)
+        else:
+            self.tb_connect.set_sensitive(True)
+            self.tb_begin.set_sensitive(True)
+
+
+    def simulate_click(self, tb):
+        if tb.get_property('sensitive'):
+            tb.get_child().activate()
+
+
+    def keypress(self, w, event):
+        keyname = gtk.gdk.keyval_name(event.keyval)
+        if keyname == 'F1':
+            self.simulate_click(self.tb_settings)
+            return True
+        if keyname == 'F2':
+            print self.cb_server.popup()
+            return True
+        if keyname == 'F3':
+            print self.cb_dbname.popup()
+            return True
+        if keyname == 'F4':
+            self.simulate_click(self.tb_connect)
+            return True
+        if keyname == 'F5':
+            self.simulate_click(self.tb_execute)
+            return True
+        if keyname == 'F6':
+            self.simulate_click(self.tb_begin)
+            return True
+        if keyname == 'F7':
+            self.simulate_click(self.tb_commit)
+            return True
+        if keyname == 'F8':
+            self.simulate_click(self.tb_rollback)
+            return True
+
+        return False
+
+
+    def toolbar_server_keypress(self, w, event):
+        keyname = gtk.gdk.keyval_name(event.keyval)
+        if keyname == 'Tab':
+            self.populate_db_list()
+            self.cb_dbname.grab_focus()
+            return True
+
+
+    def on_server_changed(self, w):
+        if self.cb_server.get_active() != -1:
+            self.populate_db_list()
+
+
+    def populate_db_list(self):
+        conninfo = self.get_conninfo(True)
+        try:
+            conn = self.db.get_conn(conninfo)
+        except DatabaseError, e:
+            self.logbuf.insert(self.logbuf.get_end_iter(), 'Error:\n%s\n' % e)
+            return
+
+        curs = conn.cursor()
+        curs.execute('SELECT * FROM pg_catalog.pg_database WHERE NOT datistemplate ORDER BY 1')
+        psycopg2.extras.wait_select(conn)
+        rows = curs.fetchall()
+
+        self.db.put_conn(conninfo, conn)
+
+        for i in range(self.cb_dbname.get_model().iter_n_children(None)):
+            self.cb_dbname.remove_text(0)
+
+        for row in rows:
+            self.cb_dbname.append_text(row[0])
+
+        self.cb_dbname.set_active(0)
+
+
+    def reload_server_list(self, sel=None):
+        # clean
+        for i in range(self.cb_server.get_model().iter_n_children(None)):
+            self.cb_server.remove_text(0)
+        self.cb_server.get_child().set_text('')
+
+        try:
+            # populate
+            for server in cfg.servers.server:
+                if str(server.name):
+                    title = '%s (%s)' % (str(server.name), str(server.host))
+                else:
+                    title = str(server.host)
+                self.cb_server.append_text(title)
+        except AttributeError:
+            pass
+
+        if not sel is None:
+            self.cb_server.set_active(sel)
+
+
+    def settings(self, w):
+        Settings(self)
+
+
+
+if __name__ == '__main__':
+    try:
+        cfg.load('pgconsole.xml.gz')
+    except IOError:
+        cfg.new('pgconsole.xml.gz')
+
+    try:
+        gtkrc = cfg.root.style.gtkrc.text
+    except AttributeError:
+        pass
+    else:
+        gtk.rc_parse_string(gtkrc)
+
+    app = PgConsoleApp()
+    app.main()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pgconsole/config.py	Tue Aug 16 23:53:54 2011 +0200
@@ -0,0 +1,73 @@
+from lxml import etree
+from lxml import objectify
+
+class Config:
+    def __init__(self):
+        self.tree = None
+        self.fname = None
+
+
+    def new(self, fname=None):
+        E = objectify.E
+        self.root = E.psqlconsole(
+            E.servers,
+            E.editor(
+                E.nodes(
+                    E.node('-- Type your query here', name='Untitled', type='text'),
+                    selected='Untitled'
+                )
+            ),
+            E.window(
+                E.size(
+                    E.width(800),
+                    E.height(600)
+                ),
+                E.dividers(
+                    E.verticaldivider(300),
+                    E.horizontaldivider(500),
+                    E.editordivider(-1)
+                )
+            )
+        )
+        self.tree = etree.ElementTree(self.root)
+        self.servers = self.root.servers
+        self.fname = fname
+
+
+    def load(self, fname=None):
+        if fname:
+            self.fname = fname
+        self.tree = objectify.parse(self.fname)
+        self.root = self.tree.getroot()
+        self.servers = self.root.servers
+
+
+    def save(self, fname=None):
+        if fname:
+            self.fname = fname
+        self.tree.write(
+            self.fname,
+            encoding='utf-8',
+            xml_declaration=True,
+            pretty_print=True,
+            compression=6)
+
+
+    def add_server(self, name, host, port, user, password):
+        e = etree.SubElement(self.servers, 'server')
+        e.name = name
+        e.host = host
+        e.port = port
+        e.user = user
+        e.password = password
+        return e
+
+
+cfg = Config()
+
+
+if __name__ == '__main__':
+    cfg.load('../psqlconsole.xml.gz')
+    print cfg.root.servers.server[0].host.text
+    cfg.save()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pgconsole/database.py	Tue Aug 16 23:53:54 2011 +0200
@@ -0,0 +1,101 @@
+import psycopg2
+import psycopg2.extensions
+import psycopg2.extras
+
+
+class DatabaseError(Exception):
+    def __init__(self, msg, query=None):
+        self.query = query
+        Exception.__init__(self, msg)
+
+
+class BadConnectionError(Exception):
+    pass
+
+
+class Row(dict):
+    def __getattr__(self, key):
+        return self[key]
+
+
+class Database:
+    def __init__(self):
+        # pool of database connections
+        # indexed by conninfo, items are lists of connections
+        self.pool = {}
+        # number of unused connections per conninfo to keep open
+        self.pool_keep_open = 1
+
+
+    def __del__(self):
+        for conninfo in self.pool.keys():
+            for conn in self.pool[conninfo]:
+                conn.close()
+
+
+    def connect(self, conninfo):
+        try:
+            conn = psycopg2.connect(conninfo, async=1)
+            psycopg2.extras.wait_select(conn)
+        except psycopg2.DatabaseError, e:
+            raise DatabaseError(str(e))
+        return conn
+
+
+    def get_conn(self, conninfo):
+        if not conninfo in self.pool:
+            self.pool[conninfo] = []
+            return self.connect(conninfo)
+        else:
+            conn = None
+            while len(self.pool[conninfo]) and conn is None:
+                conn = self.pool[conninfo].pop()
+                if conn.closed:
+                    conn = None
+            if conn is None:
+                return self.connect(conninfo)
+        return conn
+
+
+    def put_conn(self, conninfo, conn):
+        if len(self.pool[conninfo]) >= self.pool_keep_open:
+            conn.close()
+        else:
+            self.pool[conninfo].append(conn)
+
+
+    def execute(self, q, args=[]):
+        conn = self.get_conn()
+        try:
+            curs = conn.cursor()
+            curs.execute(q, args)
+            psycopg2.extras.wait_select(curs.connection)
+#            conn.commit()
+        except psycopg2.OperationalError, e:
+            # disconnected?
+#            conn.rollback()
+            conn.close()
+            raise BadConnectionError(str(e))
+        except psycopg2.DatabaseError, e:
+#            conn.rollback()
+            raise DatabaseError(str(e), curs.query)
+        return curs
+
+
+    def finish(self, curs):
+        self.put_conn(curs.connection)
+
+
+    def row(self, curs, row):
+        return Row(zip([x[0] for x in curs.description], row))
+
+
+    def fetchone(self, curs):
+        return self.row(curs, curs.fetchone())
+
+
+    def fetchall(self, curs):
+        rows = curs.fetchall()
+        return [self.row(curs, row) for row in rows]
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pgconsole/dataview.py	Tue Aug 16 23:53:54 2011 +0200
@@ -0,0 +1,139 @@
+import gtk, gobject, pango
+from cgi import escape
+
+
+class DataView(gtk.ScrolledWindow):
+    def __init__(self):
+        super(DataView, self).__init__()
+
+        sw = self
+        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+        sw.set_shadow_type(gtk.SHADOW_ETCHED_IN)
+        self.treeview = gtk.TreeView(gtk.ListStore(str))
+        self.treeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
+        self.treeview.set_property("rubber-banding", True)
+        sw.add(self.treeview)
+
+
+    def load_data(self, names, rows):
+        x = [str] * (len(names) + 1)
+        liststore = gtk.ListStore(*x)
+
+        for c in self.treeview.get_columns():
+            self.treeview.remove_column(c)
+
+        tvcolumn = gtk.TreeViewColumn()
+        cell = DataViewCellRenderer()
+        cell.set_property('head', True)
+        tvcolumn.pack_start(cell, True)
+        tvcolumn.set_property('resizable', True)
+        #tvcolumn.set_property('sizing', gtk.TREE_VIEW_COLUMN_FIXED)
+        tvcolumn.set_attributes(cell, text=0)
+        tvcolumn.set_min_width(2)
+        self.treeview.append_column(tvcolumn)
+
+        i = 0
+        for c in names:
+            i += 1
+            typename = c[1]
+            title = '<b>%s</b>' % escape(c[0])
+            if typename:
+                title += '\n<span size="small">%s</span>' % typename
+
+            tvcolumn = gtk.TreeViewColumn()
+            cell = DataViewCellRenderer()
+
+            lab = gtk.Label()
+            lab.set_use_underline(False)
+            lab.set_markup(title)
+            lab.show()
+            tvcolumn.set_widget(lab)
+            tvcolumn.set_property('resizable', True)
+            tvcolumn.pack_start(cell, True)
+            tvcolumn.set_attributes(cell, text=i)
+
+            self.treeview.append_column(tvcolumn)
+
+        self.treeview.set_model(liststore)
+
+        i = 0
+        for row in rows:
+            i += 1
+            liststore.append([i]+list(row))
+
+
+
+class DataViewCellRenderer(gtk.GenericCellRenderer):
+    __gtype_name__ = 'DataViewCellRenderer'
+    __gproperties__ = {
+        'text': (gobject.TYPE_STRING, None, None, '', gobject.PARAM_READWRITE),
+        'head': (gobject.TYPE_BOOLEAN, None, None, False, gobject.PARAM_READWRITE)}
+
+
+    def __init__(self):
+        gtk.GenericCellRenderer.__init__(self)
+        self._props = {'text' : '', 'head' : False}
+
+
+    def do_set_property(self, pspec, value):
+        if not pspec.name in self._props:
+            raise AttributeError, 'Unknown property: %s' % pspec.name
+        self._props[pspec.name] = value
+
+
+    def do_get_property(self, pspec):
+        return self._props[pspec.name]
+
+
+    def on_get_size(self, widget, cell_area):
+        if cell_area == None:
+            pangoctx = widget.get_pango_context()
+            layout = pango.Layout(pangoctx)
+            layout.set_width(-1)
+            layout.set_text(self.get_property('text') or 'NULL')
+            w,h = layout.get_pixel_size()
+            return (0, 0, w+5, 20)
+        x = cell_area.x
+        y = cell_area.x
+        w = cell_area.width
+        h = cell_area.height
+
+        return (x,y,w,h)
+
+
+    def on_render(self, window, widget, background_area, cell_area, expose_area, flags):
+        x = background_area.x
+        y = background_area.y
+        w = background_area.width
+        h = background_area.height
+
+        ctx = window.cairo_create()
+        ctx.set_line_width(0.4)
+        ctx.set_source_rgb(0, 0, 0)
+        ctx.move_to(x+w-1.5, y-0.5)
+        ctx.line_to(x+w-1.5, y+h-0.5)
+        ctx.line_to(x-0.5, y+h-0.5)
+        ctx.stroke()
+        ctx.set_source_rgb(1, 1, 1)
+        ctx.move_to(x+w-0.5, y-0.5)
+        ctx.line_to(x+w-0.5, y+h-0.5)
+        ctx.stroke()
+
+        pangoctx = widget.get_pango_context()
+        layout = pango.Layout(pangoctx)
+        text = self.get_property('text')
+        head = self.get_property('head')
+
+        if head:
+            layout.set_markup('<b>%s</b>' % text)
+        else:
+            if text is None:
+                layout.set_markup('<span foreground="gray">NULL</span>')
+            else:
+                layout.set_text(text)
+
+        widget.style.paint_layout(window, gtk.STATE_NORMAL, True,
+                                cell_area, widget, '',
+                                cell_area.x, cell_area.y,
+                                layout)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pgconsole/editor.py	Tue Aug 16 23:53:54 2011 +0200
@@ -0,0 +1,455 @@
+import gtk, pango
+import gtksourceview2 as gtksourceview
+from lxml import etree
+
+from panedext import HPanedExt
+from config import cfg
+
+
+class Editor(HPanedExt):
+    def __init__(self):
+        super(Editor, self).__init__()
+
+        self.view = gtksourceview.View()
+        self.view.connect('toggle-overwrite', self.on_toggle_overwrite)
+
+        vbox = gtk.VBox()
+        self.vbox1 = vbox
+        self.add1(vbox)
+
+        sw = gtk.ScrolledWindow()
+        sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+        sw.set_shadow_type(gtk.SHADOW_IN)
+        vbox.pack_start(sw)
+        tree = gtk.TreeView()
+        tree.set_headers_visible(False)
+        tree.get_selection().set_mode(gtk.SELECTION_BROWSE)
+        sw.add(tree)
+
+        model = gtk.ListStore(str, str, object, bool)  # title, filename, buffer, modified
+        tree.set_model(model)
+        cell = gtk.CellRendererPixbuf()
+        cell.set_property('stock-id', gtk.STOCK_SAVE)
+        column = gtk.TreeViewColumn("File", cell, visible=3)
+        tree.append_column(column)
+        column = gtk.TreeViewColumn("File", gtk.CellRendererText(), text=0)
+        tree.append_column(column)
+        tree.get_selection().connect('changed', self.item_change)
+        tree.set_property('can-focus', False)
+        self.filelist = tree
+
+
+        hbox = gtk.HBox()
+
+        img = gtk.Image()
+        img.set_from_stock(gtk.STOCK_NEW, gtk.ICON_SIZE_SMALL_TOOLBAR)
+        btn = gtk.Button()
+        btn.set_relief(gtk.RELIEF_NONE)
+        btn.set_focus_on_click(False)
+        btn.set_image(img)
+        btn.connect('clicked', self.item_new)
+        hbox.pack_start(btn, expand=False)
+
+        img = gtk.Image()
+        img.set_from_stock(gtk.STOCK_OPEN, gtk.ICON_SIZE_SMALL_TOOLBAR)
+        btn = gtk.Button()
+        btn.set_relief(gtk.RELIEF_NONE)
+        btn.set_focus_on_click(False)
+        btn.set_image(img)
+        btn.connect('clicked', self.item_open)
+        hbox.pack_start(btn, expand=False)
+
+        img = gtk.Image()
+        img.set_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_SMALL_TOOLBAR)
+        btn = gtk.Button()
+        btn.set_relief(gtk.RELIEF_NONE)
+        btn.set_focus_on_click(False)
+        btn.set_image(img)
+        btn.connect('clicked', self.item_close)
+        hbox.pack_start(btn, expand=False)
+        hbox.connect('size-request', self.leftbuttons_size_request)
+
+        vbox.pack_start(hbox, expand=False)
+
+        vbox = gtk.VBox()
+        vbox.set_property("width-request", 200)
+        self.add2(vbox)
+        self.child_set_property(vbox, 'shrink', False)
+
+
+        # scroll
+        sw = gtk.ScrolledWindow()
+        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+        sw.set_shadow_type(gtk.SHADOW_ETCHED_IN)
+        sw.add(self.view)
+        vbox.pack_start(sw)
+
+
+        self.view.set_show_line_numbers(True)
+        self.view.set_smart_home_end(True)
+        #self.view.set_insert_spaces_instead_of_tabs(True)
+        self.view.set_property("tab-width", 4)
+        self.view.set_auto_indent(True)
+
+        # font
+        font_desc = pango.FontDescription('monospace')
+        if font_desc:
+            self.view.modify_font(font_desc)
+
+        # status bar
+        hbox = gtk.HBox()
+
+        self.st_file, fr1 = self.construct_status('SQL snippet')
+        self.st_file.set_ellipsize(pango.ELLIPSIZE_START)
+        self.st_insovr, fr2 = self.construct_status('INS')
+        self.st_linecol, fr3 = self.construct_status('Line: 0 Col: 0')
+
+
+        img = gtk.Image()
+        img.set_from_stock(gtk.STOCK_SAVE, gtk.ICON_SIZE_SMALL_TOOLBAR)
+        #save = img
+        save = gtk.Button()
+        save.set_relief(gtk.RELIEF_NONE)
+        save.set_focus_on_click(False)
+        save.set_image(img)
+        save.connect('clicked', self.item_save)
+        hbox.pack_start(save, expand=False)
+
+        hbox.pack_start(fr1, expand=True)
+        hbox.pack_start(fr2, expand=False)
+        hbox.pack_start(fr3, expand=False)
+
+        #sep = gtk.HSeparator()
+        #vbox.pack_start(sep, expand=False, fill=False, padding=0)
+
+        #align = gtk.Alignment()
+        #align.set_property("bottom-padding", 3)
+        #align.set_property("xscale", 1.0)
+        #align.add(hbox)
+
+        #frame = gtk.Frame()
+        #frame.add(hbox)
+        #frame.set_shadow_type(gtk.SHADOW_ETCHED_IN)
+        vbox.pack_start(hbox, expand=False, padding=0)
+
+        self.load_nodes()
+        self.build_context_menu()
+
+
+    def build_context_menu(self):
+        menu = gtk.Menu()
+        item = gtk.ImageMenuItem(gtk.STOCK_SAVE, "Save")
+        item.connect("activate", self.item_save)
+        item.show()
+        menu.append(item)
+
+        item = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS, "Save as")
+        item.connect("activate", self.item_save_as)
+        item.show()
+        menu.append(item)
+
+        item = gtk.ImageMenuItem(gtk.STOCK_CLOSE, "Close")
+        item.connect("activate", self.item_close)
+        item.show()
+        menu.append(item)
+
+        item = gtk.SeparatorMenuItem()
+        item.show()
+        menu.append(item)
+
+        item = gtk.ImageMenuItem(gtk.STOCK_GO_UP, "Move up")
+        item.show()
+        menu.append(item)
+
+        item = gtk.ImageMenuItem(gtk.STOCK_GO_DOWN, "Move down")
+        item.show()
+        menu.append(item)
+
+        self.filelist_menu = menu
+
+        self.filelist.connect_object("button-press-event", self.on_filelist_button_press_event, menu)
+
+
+    def on_filelist_button_press_event(self, w, event):
+        if event.button == 3:
+            x = int(event.x)
+            y = int(event.y)
+            pathinfo = self.filelist.get_path_at_pos(x, y)
+            if pathinfo is not None:
+                path, col, cellx, celly = pathinfo
+                self.filelist.grab_focus()
+                self.filelist.set_cursor(path, col, 0)
+                self.filelist_menu.popup(None, None, None, event.button, event.time)
+            return True
+
+
+
+    def make_buffer(self):
+        buffer = gtksourceview.Buffer()
+        buffer.connect('mark-set', self.buffer_mark_set)
+        buffer.connect('changed', self.buffer_changed)
+
+        # style
+        mgr = gtksourceview.style_scheme_manager_get_default()
+        style_scheme = mgr.get_scheme('kate')
+        if style_scheme:
+            buffer.set_style_scheme(style_scheme)
+
+        # syntax
+        lngman = gtksourceview.language_manager_get_default()
+        langsql = lngman.get_language('sql')
+        buffer.set_language(langsql)
+        buffer.set_highlight_syntax(True)
+
+        return buffer
+
+
+    def load_nodes(self):
+        model = self.filelist.get_model()
+        sel = cfg.root.editor.nodes.get('selected')
+        for node in cfg.root.editor.nodes.node:
+            buffer = self.make_buffer()
+            name = node.get('name')
+            type = node.get('type')
+            filename = None
+            if type == 'text' and node.text:
+                buffer.set_text(node.text)
+            if type == 'file':
+                filename = node.text
+                content = open(filename).read()
+                buffer.set_text(content)
+            iter = model.append([name, filename, buffer, False])
+            if sel == name:
+                self.filelist.get_selection().select_iter(iter)
+
+
+    def set_text(self, text):
+        self.buffer.set_text(text)
+
+
+    def get_text(self):
+        start, end = self.buffer.get_bounds()
+        return self.buffer.get_text(start, end)
+
+
+    def get_selection(self):
+        bounds = self.buffer.get_selection_bounds()
+        if not bounds:
+            return None
+        return self.buffer.get_text(*bounds)
+
+
+    def construct_status(self, text):
+        st = gtk.Label(text)
+        st.set_property("single-line-mode", True)
+        st.set_property("xpad", 3)
+        st.set_alignment(0.0, 0.5)
+        fr = gtk.Frame()
+        fr.set_shadow_type(gtk.SHADOW_ETCHED_OUT)
+        fr.add(st)
+        return st, fr
+
+
+    def on_toggle_overwrite(self, w):
+        if not w.get_overwrite():
+            self.st_insovr.set_label('OVR')
+        else:
+            self.st_insovr.set_label('INS')
+
+
+    def buffer_mark_set(self, w, iter, textmark):
+        if textmark == w.get_insert():
+            line = iter.get_line()
+            col = iter.get_visible_line_offset()
+            self.st_linecol.set_label('Line: %d Col: %d' % (line + 1, col + 1))
+
+
+    def buffer_changed(self, w):
+        iter = w.get_iter_at_mark(w.get_insert())
+        line = iter.get_line()
+        col = iter.get_visible_line_offset()
+        self.st_linecol.set_label('Line: %d Col: %d' % (line + 1, col + 1))
+
+
+    def item_change(self, w):
+        model, sel = self.filelist.get_selection().get_selected_rows()
+        if not sel:
+            return
+        iter = model.get_iter(sel[0])
+        title, filename, self.buffer = model.get(iter, 0, 1, 2)
+        self.view.set_buffer(self.buffer)
+
+        if filename:
+            self.st_file.set_text(filename)
+        else:
+            self.st_file.set_text('SQL snippet')
+
+        # update config
+        cfg.root.editor.nodes.set('selected', title)
+
+
+    def probe_title(self, model, title):
+        iter = model.get_iter_first()
+        i = 1
+        while iter:
+            if model.get_value(iter, 0).split(' /')[0] == title:
+                new = title
+                if i > 1:
+                    new += ' /%d' % i
+                model.set_value(iter, 0, new)
+                i += 1
+            iter = model.iter_next(iter)
+        if i > 1:
+            title = '%s /%d' % (title, i)
+        return title
+
+
+    def item_new(self, w):
+        model = self.filelist.get_model()
+        title = 'Untitled'
+        title = self.probe_title(model, title)
+        buffer = self.make_buffer()
+        iter = model.append([title, None, buffer, False])
+        self.filelist.get_selection().select_iter(iter)
+
+        # update config
+        etree.SubElement(cfg.root.editor.nodes, 'node', type='text', name=title)
+        cfg.root.editor.nodes.set('selected', title)
+        cfg.save()
+
+
+    def item_open(self, w):
+        dialog = gtk.FileChooserDialog(title='Open file', action=gtk.FILE_CHOOSER_ACTION_OPEN,
+            buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK))
+        dialog.set_default_response(gtk.RESPONSE_OK)
+        dialog.set_select_multiple(True)
+
+        filter = gtk.FileFilter()
+        filter.set_name("SQL files")
+        filter.add_pattern("*.sql")
+        dialog.add_filter(filter)
+
+        filter = gtk.FileFilter()
+        filter.set_name("All files")
+        filter.add_pattern("*")
+        dialog.add_filter(filter)
+
+        response = dialog.run()
+        if response == gtk.RESPONSE_OK:
+            filenames = dialog.get_filenames()
+            for fname in filenames:
+                self.open_file(fname)
+
+        dialog.destroy()
+
+
+    def open_file(self, filename):
+        title = filename.rsplit('/', 1)[1]
+
+        model = self.filelist.get_model()
+        iter = model.get_iter_first()
+        i = 1
+        while iter:
+            if model.get_value(iter, 1) == filename:
+                # file already opened, select it
+                self.filelist.get_selection().select_iter(iter)
+                return
+            iter = model.iter_next(iter)
+
+        title = self.probe_title(model, title)
+
+        # add item
+        buffer = self.make_buffer()
+        buffer.set_text(open(filename).read())
+        iter = model.append([title, filename, buffer, False])
+        self.filelist.get_selection().select_iter(iter)
+
+        # update config
+        el = etree.SubElement(cfg.root.editor.nodes, 'node', type='file', name=title)
+        el._setText(filename)
+        cfg.root.editor.nodes.set('selected', title)
+        cfg.save()
+
+
+    def item_save(self, w):
+        model, sel = self.filelist.get_selection().get_selected_rows()
+        if not sel:
+            return
+        iter = model.get_iter(sel[0])
+
+        filename, buffer = model.get(iter, 1, 2)
+
+        data = buffer.get_text(*buffer.get_bounds())
+        open(filename, 'w').write(data)
+
+
+    def item_save_as(self, w):
+        model, sel = self.filelist.get_selection().get_selected_rows()
+        if not sel:
+            return
+        iter = model.get_iter(sel[0])
+
+        filename, buffer = model.get(iter, 1, 2)
+
+        path, file = filename.rsplit('/',1)
+
+        dialog = gtk.FileChooserDialog(title='Save as', action=gtk.FILE_CHOOSER_ACTION_SAVE,
+            buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_SAVE,gtk.RESPONSE_OK))
+        dialog.set_default_response(gtk.RESPONSE_OK)
+        dialog.set_current_folder(path)
+        dialog.set_current_name(file)
+
+        filter = gtk.FileFilter()
+        filter.set_name("SQL files")
+        filter.add_pattern("*.sql")
+        dialog.add_filter(filter)
+
+        filter = gtk.FileFilter()
+        filter.set_name("All files")
+        filter.add_pattern("*")
+        dialog.add_filter(filter)
+
+        response = dialog.run()
+        if response == gtk.RESPONSE_OK:
+            filename = dialog.get_filename()
+            data = buffer.get_text(*buffer.get_bounds())
+            open(filename, 'w').write(data)
+
+            title = filename.rsplit('/',1)[1]
+            title = self.probe_title(model, title)
+            model.set(iter, 0, title)
+            model.set(iter, 1, filename)
+            model.set(iter, 3, False) # modified
+
+        dialog.destroy()
+
+
+    def item_close(self, w):
+        model, sel = self.filelist.get_selection().get_selected_rows()
+        if not sel:
+            return
+        iter = model.get_iter(sel[0])
+        newiter = model.iter_next(iter)
+        if newiter is None and sel[0][0] > 0:
+            newiter = model.get_iter((sel[0][0]-1,))
+
+        title, buffer = model.get(iter, 0, 2)
+        #buffer.destroy()
+
+        model.remove(iter)
+
+        if newiter is None:
+            self.item_new(None)
+        else:
+            self.filelist.get_selection().select_iter(newiter)
+
+        # update config
+        el = cfg.root.editor.nodes.find('node[@name="%s"]' % title)
+        el.getparent().remove(el)
+        cfg.save()
+
+
+    def leftbuttons_size_request(self, w, request):
+        self.set_snap1(request[0])
+        return True
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pgconsole/panedext.py	Tue Aug 16 23:53:54 2011 +0200
@@ -0,0 +1,61 @@
+import gtk
+
+
+class PanedExt(gtk.Paned):
+    """Extended gtk.Paned (abstract)
+
+    set_snap1(int), set_snap2(int)
+        set minimum size of child widget
+        if the handle is moved to the edge, child widget is hidden
+    """
+    def __init__(self):
+        self.connect('notify::position', self.on_position_change)
+        self.min1 = 0
+        self.min2 = 0
+
+
+    def set_snap1(self, minpos):
+        self.min1 = minpos
+
+
+    def set_snap2(self, minpos):
+        self.min2 = minpos
+
+
+    def on_position_change(self, w, scrolltype):
+        if self.allocation[0] == -1:
+            return False
+
+        pos = self.get_position()
+        maxpos = self.get_property('max-position')
+
+        if self.min1:
+            if pos > 0 and pos < self.min1:
+                if pos < self.min1 / 2:
+                    self.set_position(0)
+                else:
+                    self.set_position(self.min1)
+                return True
+
+        if self.min2:
+           if pos > maxpos - self.min2 and pos < maxpos:
+                if pos > maxpos - self.min2 / 2:
+                    self.set_position(maxpos)
+                else:
+                    self.set_position(maxpos - self.min2)
+                return True
+
+        return False
+
+
+class HPanedExt(gtk.HPaned, PanedExt):
+    def __init__(self):
+        gtk.HPaned.__init__(self)
+        PanedExt.__init__(self)
+
+
+class VPanedExt(gtk.VPaned, PanedExt):
+    def __init__(self):
+        gtk.VPaned.__init__(self)
+        PanedExt.__init__(self)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pgconsole/settings.py	Tue Aug 16 23:53:54 2011 +0200
@@ -0,0 +1,198 @@
+import gtk
+
+from config import cfg
+
+
+class Settings(gtk.Window):
+    def __init__(self, parent):
+        super(Settings, self).__init__(gtk.WINDOW_TOPLEVEL)
+
+        self._parent = parent
+
+        self.set_title('Settings')
+        self.set_modal(True)
+        self.set_transient_for(parent.win)
+        self.set_position(gtk.WIN_POS_CENTER)
+        #self.set_border_width(10)
+        self.connect("key_press_event", self.on_keypress)
+        self.connect("destroy", self.on_destroy)
+
+        self.tabs = gtk.Notebook()
+        self.add(self.tabs)
+
+        ### Servers
+        vbox = gtk.VBox(spacing=10)
+        vbox.set_border_width(10)
+        self.tabs.append_page(vbox, gtk.Label('Servers'))
+
+        hbox = gtk.HBox(spacing=10)
+        vbox.pack_start(hbox)
+        sw = gtk.ScrolledWindow()
+        sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+        sw.set_shadow_type(gtk.SHADOW_IN)
+        hbox.pack_start(sw)
+        tree = gtk.TreeView()
+        tree.set_headers_visible(False)
+        sw.add(tree)
+        tree.get_selection().set_mode(gtk.SELECTION_BROWSE)
+
+        model = gtk.ListStore(str, object)
+        tree.set_model(model)
+        column = gtk.TreeViewColumn("Server", gtk.CellRendererText(), text=0)
+        tree.append_column(column)
+        tree.get_selection().connect('changed', self.item_change)
+        tree.set_size_request(100, -1)
+
+        try:
+            for server in cfg.servers.server:
+                if str(server.name):
+                    title = '%s (%s)' % (str(server.name), str(server.host))
+                else:
+                    title = str(server.host)
+                model.append([title, server])
+        except AttributeError:
+            pass
+
+        self.itemtree = tree
+
+        vbox = gtk.VBox(spacing=10)
+        hbox.pack_start(vbox)
+
+        table = gtk.Table(5, 2)
+        table.set_row_spacings(4)
+        table.set_col_spacings(10)
+        vbox.pack_start(table)
+
+        label = gtk.Label("Name:")
+        label.set_alignment(0.0, 0.5)
+        table.attach(label, 0, 1, 0, 1)
+        entry = gtk.Entry()
+        table.attach(entry, 1, 2, 0, 1)
+        self.entry_name = entry
+
+        label = gtk.Label("Host:")
+        label.set_alignment(0.0, 0.5)
+        table.attach(label, 0, 1, 1, 2)
+        entry = gtk.Entry()
+        entry.set_text('127.0.0.1')
+        table.attach(entry, 1, 2, 1, 2)
+        self.entry_host = entry
+
+        label = gtk.Label("Port:")
+        label.set_alignment(0.0, 0.5)
+        table.attach(label, 0, 1, 2, 3)
+        entry = gtk.Entry()
+        entry.set_text('5432')
+        table.attach(entry, 1, 2, 2, 3)
+        self.entry_port = entry
+
+        label = gtk.Label("User:")
+        label.set_alignment(0.0, 0.5)
+        table.attach(label, 0, 1, 3, 4)
+        entry = gtk.Entry()
+        entry.set_text('postgres')
+        table.attach(entry, 1, 2, 3, 4)
+        self.entry_user = entry
+
+        label = gtk.Label("Password:")
+        label.set_alignment(0.0, 0.5)
+        table.attach(label, 0, 1, 4, 5)
+        entry = gtk.Entry()
+        table.attach(entry, 1, 2, 4, 5)
+        self.entry_password = entry
+
+        hbox = gtk.HBox()
+        vbox.pack_start(hbox)
+
+        btn = gtk.Button('Add')
+        btn.connect('clicked', self.item_add)
+        hbox.pack_start(btn)
+        btn = gtk.Button('Save')
+        btn.connect('clicked', self.item_save)
+        hbox.pack_start(btn)
+        btn = gtk.Button('Remove')
+        btn.connect('clicked', self.item_remove)
+        hbox.pack_start(btn)
+
+        ### Editor
+        vbox = gtk.VBox(spacing=10)
+        vbox.set_border_width(10)
+        self.tabs.append_page(vbox, gtk.Label('Editor'))
+
+        self.show_all()
+        self.itemtree.grab_focus()
+
+
+
+    def item_change(self, w):
+        model, sel = self.itemtree.get_selection().get_selected_rows()
+        if not sel:
+            return
+        el, = model.get(model.get_iter(sel[0]), 1)
+
+        self.entry_name.set_text(str(el.name))
+        self.entry_host.set_text(str(el.host))
+        self.entry_port.set_text(str(el.port))
+        self.entry_user.set_text(str(el.user))
+        self.entry_password.set_text(str(el.password))
+
+
+    def item_add(self, w):
+        el = cfg.add_server(
+            self.entry_name.get_text(),
+            self.entry_host.get_text(),
+            self.entry_port.get_text(),
+            self.entry_user.get_text(),
+            self.entry_password.get_text())
+        if str(el.name):
+            title = '%s (%s)' % (str(el.name), str(el.host))
+        else:
+            title = str(el.host)
+        iter = self.itemtree.get_model().append([title, el])
+        self.itemtree.get_selection().select_iter(iter)
+        cfg.save()
+
+
+    def item_save(self, w):
+        model, sel = self.itemtree.get_selection().get_selected_rows()
+        el = cfg.servers.server[sel[0][0]]
+        el.name = self.entry_name.get_text()
+        el.host = self.entry_host.get_text()
+        el.port = self.entry_port.get_text()
+        el.user = self.entry_user.get_text()
+        el.password = self.entry_password.get_text()
+        cfg.save()
+
+        if str(el.name):
+            title = '%s (%s)' % (str(el.name), str(el.host))
+        else:
+            title = str(el.host)
+        model.set(model.get_iter(sel[0]), 0, title)
+
+
+    def item_remove(self, w):
+        model, sel = self.itemtree.get_selection().get_selected_rows()
+        model.remove(model.get_iter(sel[0]))
+        if model.get_iter_first():
+            self.itemtree.get_selection().select_iter(model.get_iter_first())
+        del cfg.servers.server[sel[0][0]]
+        cfg.save()
+
+
+    def on_keypress(self, w, event):
+        keyname = gtk.gdk.keyval_name(event.keyval)
+
+        if keyname == 'Escape':
+            self.destroy()
+            return True
+        return False
+
+
+    def on_destroy(self, w):
+        model, sel = self.itemtree.get_selection().get_selected_rows()
+        if sel:
+            sel = sel[0][0]
+        else:
+            sel = None
+        self._parent.reload_server_list(sel)
+
--- a/pgtoolkit/config.py	Tue Aug 16 16:03:46 2011 +0200
+++ b/pgtoolkit/config.py	Tue Aug 16 23:53:54 2011 +0200
@@ -5,7 +5,7 @@
     def __init__(self):
         self.args = {}       # config file arguments
         self.registered_args = {}
-        self.log = logging.getLogger('config') 
+        self.log = logging.getLogger('config')
 
     def add_argument(self, name, type=str, default=None):
         self.registered_args[name] = {'type':type, 'default':default}
--- a/tests/testprettysize.py	Tue Aug 16 16:03:46 2011 +0200
+++ b/tests/testprettysize.py	Tue Aug 16 23:53:54 2011 +0200
@@ -1,7 +1,7 @@
 #!/usr/bin/env python3
 
 import unittest
-import prettysize
+from pgtoolkit import prettysize
 
 class TestHumansize(unittest.TestCase):
     def test_humansize(self):