diff --git a/gertty/alembic/versions/4388de50824a_add_topic_table.py b/gertty/alembic/versions/4388de50824a_add_topic_table.py
new file mode 100644
index 0000000..6bdfe79
--- /dev/null
+++ b/gertty/alembic/versions/4388de50824a_add_topic_table.py
@@ -0,0 +1,35 @@
+"""add topic table
+
+Revision ID: 4388de50824a
+Revises: 254ac5fc3941
+Create Date: 2015-10-31 19:06:38.538948
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '4388de50824a'
+down_revision = '254ac5fc3941'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+    op.create_table('topic',
+    sa.Column('key', sa.Integer(), nullable=False),
+    sa.Column('name', sa.String(length=255), index=True, nullable=False),
+    sa.Column('sequence', sa.Integer(), index=True, unique=True, nullable=False),
+    sa.PrimaryKeyConstraint('key')
+    )
+
+    op.create_table('project_topic',
+    sa.Column('key', sa.Integer(), nullable=False),
+    sa.Column('project_key', sa.Integer(), sa.ForeignKey('project.key'), index=True),
+    sa.Column('topic_key', sa.Integer(), sa.ForeignKey('topic.key'), index=True),
+    sa.Column('sequence', sa.Integer(), nullable=False),
+    sa.PrimaryKeyConstraint('key'),
+    sa.UniqueConstraint('topic_key', 'sequence', name='topic_key_sequence_const'),
+    )
+
+def downgrade():
+    pass
diff --git a/gertty/db.py b/gertty/db.py
index 7ca4818..b6eefe6 100644
--- a/gertty/db.py
+++ b/gertty/db.py
@@ -21,7 +21,7 @@ import threading
 import alembic
 import alembic.config
 import sqlalchemy
-from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, Boolean, DateTime, Text
+from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, Boolean, DateTime, Text, UniqueConstraint
 from sqlalchemy.schema import ForeignKey
 from sqlalchemy.orm import mapper, sessionmaker, relationship, scoped_session
 from sqlalchemy.orm.session import Session
@@ -43,6 +43,20 @@ branch_table = Table(
     Column('project_key', Integer, ForeignKey("project.key"), index=True),
     Column('name', String(255), index=True, nullable=False),
     )
+topic_table = Table(
+    'topic', metadata,
+    Column('key', Integer, primary_key=True),
+    Column('name', String(255), index=True, nullable=False),
+    Column('sequence', Integer, index=True, unique=True, nullable=False),
+    )
+project_topic_table = Table(
+    'project_topic', metadata,
+    Column('key', Integer, primary_key=True),
+    Column('project_key', Integer, ForeignKey("project.key"), index=True),
+    Column('topic_key', Integer, ForeignKey("topic.key"), index=True),
+    Column('sequence', Integer, nullable=False),
+    UniqueConstraint('topic_key', 'sequence', name='topic_key_sequence_const'),
+    )
 change_table = Table(
     'change', metadata,
     Column('key', Integer, primary_key=True),
@@ -200,6 +214,35 @@ class Branch(object):
         self.project_key = project.key
         self.name = name
 
+class ProjectTopic(object):
+    def __init__(self, project, topic, sequence):
+        self.project_key = project.key
+        self.topic_key = topic.key
+        self.sequence = sequence
+
+class Topic(object):
+    def __init__(self, name, sequence):
+        self.name = name
+        self.sequence = sequence
+
+    def addProject(self, project):
+        session = Session.object_session(self)
+        seq = max([x.sequence for x in self.project_topics] + [0])
+        pt = ProjectTopic(project, self, seq+1)
+        self.project_topics.append(pt)
+        self.projects.append(project)
+        session.add(pt)
+        session.flush()
+
+    def removeProject(self, project):
+        session = Session.object_session(self)
+        for pt in self.project_topics:
+            if pt.project_key == project.key:
+                self.project_topics.remove(pt)
+                session.delete(pt)
+        self.projects.remove(project)
+        session.flush()
+
 class Change(object):
     def __init__(self, project, id, owner, number, branch, change_id,
                  subject, created, updated, status, topic=None,
@@ -506,28 +549,40 @@ class File(object):
 
 mapper(Account, account_table)
 mapper(Project, project_table, properties=dict(
-        branches=relationship(Branch, backref='project',
-                              order_by=branch_table.c.name,
-                              cascade='all, delete-orphan'),
-        changes=relationship(Change, backref='project',
-                             order_by=change_table.c.number,
-                             cascade='all, delete-orphan'),
-        unreviewed_changes=relationship(Change,
-                                        primaryjoin=and_(project_table.c.key==change_table.c.project_key,
-                                                         change_table.c.hidden==False,
-                                                         change_table.c.status!='MERGED',
-                                                         change_table.c.status!='ABANDONED',
-                                                         change_table.c.reviewed==False),
-                                        order_by=change_table.c.number,
-                                        ),
-        open_changes=relationship(Change,
-                                  primaryjoin=and_(project_table.c.key==change_table.c.project_key,
-                                                   change_table.c.status!='MERGED',
-                                                   change_table.c.status!='ABANDONED'),
-                                  order_by=change_table.c.number,
-                                  ),
-        ))
+    branches=relationship(Branch, backref='project',
+                          order_by=branch_table.c.name,
+                          cascade='all, delete-orphan'),
+    changes=relationship(Change, backref='project',
+                         order_by=change_table.c.number,
+                         cascade='all, delete-orphan'),
+    topics=relationship(Topic,
+                        secondary=project_topic_table,
+                        order_by=topic_table.c.name,
+                        viewonly=True),
+    unreviewed_changes=relationship(Change,
+                                    primaryjoin=and_(project_table.c.key==change_table.c.project_key,
+                                                     change_table.c.hidden==False,
+                                                     change_table.c.status!='MERGED',
+                                                     change_table.c.status!='ABANDONED',
+                                                     change_table.c.reviewed==False),
+                                    order_by=change_table.c.number,
+                                ),
+    open_changes=relationship(Change,
+                              primaryjoin=and_(project_table.c.key==change_table.c.project_key,
+                                               change_table.c.status!='MERGED',
+                                               change_table.c.status!='ABANDONED'),
+                              order_by=change_table.c.number,
+                          ),
+))
 mapper(Branch, branch_table)
+mapper(Topic, topic_table, properties=dict(
+    projects=relationship(Project,
+                          secondary=project_topic_table,
+                          order_by=project_table.c.name,
+                          viewonly=True),
+    project_topics=relationship(ProjectTopic),
+))
+mapper(ProjectTopic, project_topic_table)
 mapper(Change, change_table, properties=dict(
         owner=relationship(Account),
         revisions=relationship(Revision, backref='change',
@@ -668,20 +723,26 @@ class DatabaseSession(object):
     def vacuum(self):
         self.session().execute("VACUUM")
 
-    def getProjects(self, subscribed=False, unreviewed=False):
+    def getProjects(self, subscribed=False, unreviewed=False, topicless=False):
         """Retrieve projects.
 
         :param subscribed: If True limit to only subscribed projects.
         :param unreviewed: If True limit to only projects with unreviewed
             changes.
+        :param topicless: If True limit to only projects without topics.
         """
         query = self.session().query(Project)
         if subscribed:
             query = query.filter_by(subscribed=subscribed)
             if unreviewed:
                 query = query.filter(exists().where(Project.unreviewed_changes))
+        if topicless:
+            query = query.filter_by(topics=None)
         return query.order_by(Project.name).all()
 
+    def getTopics(self):
+        return self.session().query(Topic).order_by(Topic.sequence).all()
+
     def getProject(self, key):
         try:
             return self.session().query(Project).filter_by(key=key).one()
@@ -694,6 +755,18 @@ class DatabaseSession(object):
         except sqlalchemy.orm.exc.NoResultFound:
             return None
 
+    def getTopic(self, key):
+        try:
+            return self.session().query(Topic).filter_by(key=key).one()
+        except sqlalchemy.orm.exc.NoResultFound:
+            return None
+
+    def getTopicByName(self, name):
+        try:
+            return self.session().query(Topic).filter_by(name=name).one()
+        except sqlalchemy.orm.exc.NoResultFound:
+            return None
+
     def getSyncQueryByName(self, name):
         try:
             return self.session().query(SyncQuery).filter_by(name=name).one()
@@ -872,3 +945,9 @@ class DatabaseSession(object):
         self.session().add(o)
         self.session().flush()
         return o
+
+    def createTopic(self, *args, **kw):
+        o = Topic(*args, **kw)
+        self.session().add(o)
+        self.session().flush()
+        return o
diff --git a/gertty/keymap.py b/gertty/keymap.py
index fd3d3ce..ff79c59 100644
--- a/gertty/keymap.py
+++ b/gertty/keymap.py
@@ -70,6 +70,12 @@ SORT_BY_REVERSE = 'reverse the sort'
 TOGGLE_LIST_REVIEWED = 'toggle list reviewed'
 TOGGLE_LIST_SUBSCRIBED = 'toggle list subscribed'
 TOGGLE_SUBSCRIBED = 'toggle subscribed'
+NEW_PROJECT_TOPIC = 'new project topic'
+DELETE_PROJECT_TOPIC = 'delete project topic'
+MOVE_PROJECT_TOPIC = 'move to project topic'
+COPY_PROJECT_TOPIC = 'copy to project topic'
+REMOVE_PROJECT_TOPIC = 'remove from project topic'
+RENAME_PROJECT_TOPIC = 'rename project topic'
 # Diff screens:
 SELECT_PATCHSETS = 'select patchsets'
 NEXT_SELECTABLE = 'next selectable'
@@ -129,6 +135,12 @@ DEFAULT_KEYMAP = {
     TOGGLE_LIST_REVIEWED: 'l',
     TOGGLE_LIST_SUBSCRIBED: 'L',
     TOGGLE_SUBSCRIBED: 's',
+    NEW_PROJECT_TOPIC: [['T', 'n']],
+    DELETE_PROJECT_TOPIC: [['T', 'delete']],
+    MOVE_PROJECT_TOPIC: [['T', 'm']],
+    COPY_PROJECT_TOPIC: [['T', 'c']],
+    REMOVE_PROJECT_TOPIC: [['T', 'D']],
+    RENAME_PROJECT_TOPIC: [['T', 'r']],
 
     SELECT_PATCHSETS: 'p',
     NEXT_SELECTABLE: 'tab',
diff --git a/gertty/mywid.py b/gertty/mywid.py
index 9151f86..82b304a 100644
--- a/gertty/mywid.py
+++ b/gertty/mywid.py
@@ -148,6 +148,33 @@ class ButtonDialog(urwid.WidgetWrap):
         listbox = urwid.ListBox(rows)
         super(ButtonDialog, self).__init__(urwid.LineBox(listbox, title))
 
+class LineEditDialog(ButtonDialog):
+    signals = ['save', 'cancel']
+    def __init__(self, app, title, message, entry_prompt=None,
+                 entry_text='', ring=None):
+        self.app = app
+        save_button = FixedButton('Save')
+        cancel_button = FixedButton('Cancel')
+        urwid.connect_signal(save_button, 'click',
+                             lambda button:self._emit('save'))
+        urwid.connect_signal(cancel_button, 'click',
+                             lambda button:self._emit('cancel'))
+        super(LineEditDialog, self).__init__(title, message, entry_prompt,
+                                             entry_text,
+                                             buttons=[save_button,
+                                                      cancel_button],
+                                             ring=ring)
+
+    def keypress(self, size, key):
+        if not self.app.input_buffer:
+            key = super(LineEditDialog, self).keypress(size, key)
+        keys = self.app.input_buffer + [key]
+        commands = self.app.config.keymap.getCommands(keys)
+        if keymap.ACTIVATE in commands:
+            self._emit('save')
+            return None
+        return key
+
 class TextEditDialog(urwid.WidgetWrap):
     signals = ['save', 'cancel']
     def __init__(self, title, prompt, button, text, ring=None):
diff --git a/gertty/view/project_list.py b/gertty/view/project_list.py
index da67d32..574ebff 100644
--- a/gertty/view/project_list.py
+++ b/gertty/view/project_list.py
@@ -17,6 +17,7 @@ import logging
 import urwid
 
 from gertty import keymap
+from gertty import mywid
 from gertty import sync
 from gertty.view import change_list as view_change_list
 from gertty.view import mouse_scroll_decorator
@@ -31,10 +32,14 @@ class ProjectRow(urwid.Button):
     def selectable(self):
         return True
 
-    def __init__(self, project, callback=None):
+    def __init__(self, project, topic, callback=None):
         super(ProjectRow, self).__init__('', on_press=callback,
                                          user_data=(project.key, project.name))
         self.project_key = project.key
+        if topic:
+            self.topic_key = topic.key
+        else:
+            self.topic_key = None
         name = urwid.Text(project.name)
         name.set_wrap_mode('clip')
         self.unreviewed_changes = urwid.Text(u'', align=urwid.RIGHT)
@@ -60,6 +65,47 @@ class ProjectRow(urwid.Button):
         self.unreviewed_changes.set_text('%i ' % len(project.unreviewed_changes))
         self.open_changes.set_text('%i ' % len(project.open_changes))
 
+class TopicRow(urwid.Button):
+    project_focus_map = {None: 'focused',
+                         'subscribed-project': 'focused-subscribed-project',
+                         }
+
+    def selectable(self):
+        return True
+
+    def _setName(self, name):
+        self.name.set_text('[[ '+name+' ]]')
+
+    def __init__(self, topic, callback=None):
+        super(TopicRow, self).__init__('', on_press=callback,
+                                       user_data=(topic.key, topic.name))
+        self.topic_key = topic.key
+        self.name = urwid.Text('')
+        self._setName(topic.name)
+        self.name.set_wrap_mode('clip')
+        self.unreviewed_changes = urwid.Text(u'', align=urwid.RIGHT)
+        self.open_changes = urwid.Text(u'', align=urwid.RIGHT)
+        col = urwid.Columns([
+                self.name,
+                ('fixed', 11, self.unreviewed_changes),
+                ('fixed', 5, self.open_changes),
+                ])
+        self.row_style = urwid.AttrMap(col, '')
+        self._w = urwid.AttrMap(self.row_style, None, focus_map=self.project_focus_map)
+        self.row_style.set_attr_map({None: 'subscribed-project'})
+        self.update(topic)
+
+    def update(self, topic, unreviewed_changes=None, open_changes=None):
+        self._setName(topic.name)
+        if unreviewed_changes is None:
+            self.unreviewed_changes.set_text('')
+        else:
+            self.unreviewed_changes.set_text('%i ' % unreviewed_changes)
+        if open_changes is None:
+            self.open_changes.set_text('')
+        else:
+            self.open_changes.set_text('%i ' % open_changes)
+
 class ProjectListHeader(urwid.WidgetWrap):
     def __init__(self):
         cols = [urwid.Text(u'Project'),
@@ -79,8 +125,20 @@ class ProjectListView(urwid.WidgetWrap):
             (key(keymap.TOGGLE_SUBSCRIBED),
              "Toggle the subscription flag for the currently selected project"),
             (key(keymap.REFRESH),
-             "Sync subscribed projects")
-            ]
+             "Sync subscribed projects"),
+            (key(keymap.NEW_PROJECT_TOPIC),
+             "Create project topic"),
+            (key(keymap.DELETE_PROJECT_TOPIC),
+             "Delete selected project topic"),
+            (key(keymap.MOVE_PROJECT_TOPIC),
+             "Move selected project to topic"),
+            (key(keymap.COPY_PROJECT_TOPIC),
+             "Copy selected project to topic"),
+            (key(keymap.REMOVE_PROJECT_TOPIC),
+             "Remove selected project from topic"),
+            (key(keymap.RENAME_PROJECT_TOPIC),
+             "Rename selected project topic"),
+        ]
 
     def __init__(self, app):
         super(ProjectListView, self).__init__(urwid.Pile([]))
@@ -89,6 +147,8 @@ class ProjectListView(urwid.WidgetWrap):
         self.unreviewed = True
         self.subscribed = True
         self.project_rows = {}
+        self.topic_rows = {}
+        self.open_topics = set()
         self.listbox = urwid.ListBox(urwid.SimpleFocusListWalker([]))
         self.header = ProjectListHeader()
         self.refresh()
@@ -110,6 +170,56 @@ class ProjectListView(urwid.WidgetWrap):
         self.log.debug("Refreshing project list due to event %s" % (event,))
         return True
 
+    def _deleteRow(self, row):
+        if row in self.listbox.body:
+            self.listbox.body.remove(row)
+        if isinstance(row, ProjectRow):
+            del self.project_rows[(row.topic_key, row.project_key)]
+        else:
+            del self.topic_rows[row.topic_key]
+
+    def _projectRow(self, i, project, topic):
+        # Ensure that the row at i is the given project.  If the row
+        # already exists somewhere in the list, delete all rows
+        # between i and the row and then update the row.  If the row
+        # does not exist, insert the row at position i.
+        topic_key = topic and topic.key or None
+        key = (topic_key, project.key)
+        row = self.project_rows.get(key)
+        while row:  # This is "if row: while True:".
+            if i >= len(self.listbox.body):
+                break
+            current_row = self.listbox.body[i]
+            if (isinstance(current_row, ProjectRow) and
+                current_row.project_key == project.key):
+                break
+            self._deleteRow(current_row)
+        if not row:
+            row = ProjectRow(project, topic, self.onSelect)
+            self.listbox.body.insert(i, row)
+            self.project_rows[key] = row
+        else:
+            row.update(project)
+        return i+1
+
+    def _topicRow(self, i, topic):
+        row = self.topic_rows.get(topic.key)
+        while row:  # This is "if row: while True:".
+            if i >= len(self.listbox.body):
+                break
+            current_row = self.listbox.body[i]
+            if (isinstance(current_row, TopicRow) and
+                current_row.topic_key == topic.key):
+                break
+            self._deleteRow(current_row)
+        if not row:
+            row = TopicRow(topic, self.onSelectTopic)
+            self.listbox.body.insert(i, row)
+            self.topic_rows[topic.key] = row
+        else:
+            row.update(topic)
+        return i + 1
+
     def refresh(self):
         if self.subscribed:
             self.title = u'Subscribed projects'
@@ -118,24 +228,33 @@ class ProjectListView(urwid.WidgetWrap):
         else:
             self.title = u'All projects'
         self.app.status.update(title=self.title)
-        unseen_keys = set(self.project_rows.keys())
         with self.app.db.getSession() as session:
             i = 0
-            for project in session.getProjects(
+            for project in session.getProjects(topicless=True,
                     subscribed=self.subscribed, unreviewed=self.unreviewed):
-                row = self.project_rows.get(project.key)
-                if not row:
-                    row = ProjectRow(project, self.onSelect)
-                    self.listbox.body.insert(i, row)
-                    self.project_rows[project.key] = row
-                else:
-                    row.update(project)
-                    unseen_keys.remove(project.key)
-                i += 1
-        for key in unseen_keys:
-            row = self.project_rows[key]
-            self.listbox.body.remove(row)
-            del self.project_rows[key]
+                #self.log.debug("project: %s" % project.name)
+                i = self._projectRow(i, project, None)
+            for topic in session.getTopics():
+                #self.log.debug("topic: %s" % topic.name)
+                i = self._topicRow(i, topic)
+                topic_unreviewed = 0
+                topic_open = 0
+                for project in topic.projects:
+                    #self.log.debug("  project: %s" % project.name)
+                    topic_unreviewed += len(project.unreviewed_changes)
+                    topic_open += len(project.open_changes)
+                    if self.subscribed:
+                        if not project.subscribed:
+                            continue
+                        if self.unreviewed and not project.unreviewed_changes:
+                            continue
+                    if topic.key in self.open_topics:
+                        i = self._projectRow(i, project, topic)
+                topic_row = self.topic_rows.get(topic.key)
+                topic_row.update(topic, topic_unreviewed, topic_open)
+        while i < len(self.listbox.body):
+            current_row = self.listbox.body[i]
+            self._deleteRow(current_row)
 
     def toggleSubscribed(self, project_key):
         with self.app.db.getSession() as session:
@@ -151,6 +270,126 @@ class ProjectListView(urwid.WidgetWrap):
                 "_project_key:%s %s" % (project_key, self.app.config.project_change_list_query),
                 project_name, project_key=project_key, unreviewed=True))
 
+    def onSelectTopic(self, button, data):
+        topic_key = data[0]
+        self.open_topics ^= set([topic_key])
+        self.refresh()
+
+    def createTopic(self):
+        dialog = mywid.LineEditDialog(self.app, 'Topic', 'Create a new topic.',
+                                      'Topic: ', '', self.app.ring)
+        urwid.connect_signal(dialog, 'save',
+            lambda button: self.closeCreateTopic(dialog, True))
+        urwid.connect_signal(dialog, 'cancel',
+            lambda button: self.closeCreateTopic(dialog, False))
+        self.app.popup(dialog)
+
+    def closeCreateTopic(self, dialog, save):
+        if save:
+            last_topic_key = None
+            for row in self.listbox.body:
+                if isinstance(row, TopicRow):
+                    last_topic_key = row.topic_key
+            with self.app.db.getSession() as session:
+                if last_topic_key:
+                    last_topic = session.getTopic(last_topic_key)
+                    seq = last_topic.sequence + 1
+                else:
+                    seq = 0
+                t = session.createTopic(dialog.entry.edit_text, seq)
+        self.app.backScreen()
+
+    def deleteTopic(self):
+        pos = self.listbox.focus_position
+        row = self.listbox.body[pos]
+        if not isinstance(row, TopicRow):
+            return
+        with self.app.db.getSession() as session:
+            topic = session.getTopic(row.topic_key)
+            session.delete(topic)
+        self.refresh()
+
+    def renameTopic(self):
+        pos = self.listbox.focus_position
+        row = self.listbox.body[pos]
+        if not isinstance(row, TopicRow):
+            return
+        with self.app.db.getSession() as session:
+            topic = session.getTopic(row.topic_key)
+            name = topic.name
+            key = topic.key
+        dialog = mywid.LineEditDialog(self.app, 'Topic', 'Rename a new topic.',
+                                      'Topic: ', name, self.app.ring)
+        urwid.connect_signal(dialog, 'save',
+            lambda button: self.closeRenameTopic(dialog, True, key))
+        urwid.connect_signal(dialog, 'cancel',
+            lambda button: self.closeRenameTopic(dialog, False, key))
+        self.app.popup(dialog)
+
+    def closeRenameTopic(self, dialog, save, key):
+        if save:
+            with self.app.db.getSession() as session:
+                topic = session.getTopic(key)
+                topic.name = dialog.entry.edit_text
+        self.app.backScreen()
+
+    def copyMoveToTopic(self, move):
+        if move:
+            verb = 'Move'
+        else:
+            verb = 'Copy'
+        pos = self.listbox.focus_position
+        row = self.listbox.body[pos]
+        if not isinstance(row, ProjectRow):
+            return
+        dialog = mywid.LineEditDialog(self.app, 'Topic', '%s to topic.' % verb,
+                                      'Topic: ', '', self.app.ring)
+        urwid.connect_signal(dialog, 'save',
+            lambda button: self.closeCopyMoveToTopic(dialog, True, row, move))
+        urwid.connect_signal(dialog, 'cancel',
+            lambda button: self.closeCopyMoveToTopic(dialog, False, row, move))
+        self.app.popup(dialog)
+
+    def closeCopyMoveToTopic(self, dialog, save, row, move):
+        error = None
+        if save:
+            with self.app.db.getSession() as session:
+                project = session.getProject(row.project_key)
+                topic_name = dialog.entry.edit_text
+                new_topic = session.getTopicByName(topic_name)
+                if not new_topic:
+                    error = "Unable to find topic %s" % topic_name
+                else:
+                    if move and row.topic_key:
+                        old_topic = session.getTopic(row.topic_key)
+                        self.log.debug("Remove %s from %s" % (project, old_topic))
+                        old_topic.removeProject(project)
+                    self.log.debug("Add %s to %s" % (project, new_topic))
+                    new_topic.addProject(project)
+        self.app.backScreen()
+        if error:
+            self.app.error(error)
+
+    def moveToTopic(self):
+        self.copyMoveToTopic(True)
+
+    def copyToTopic(self):
+        self.copyMoveToTopic(False)
+
+    def removeFromTopic(self):
+        pos = self.listbox.focus_position
+        row = self.listbox.body[pos]
+        if not isinstance(row, ProjectRow):
+            return
+        if not row.topic_key:
+            return
+        with self.app.db.getSession() as session:
+            project = session.getProject(row.project_key)
+            topic = session.getTopic(row.topic_key)
+            self.log.debug("Remove %s from %s" % (project, topic))
+            topic.removeProject(project)
+        self.refresh()
+
     def keypress(self, size, key):
         if not self.app.input_buffer:
             key = super(ProjectListView, self).keypress(size, key)
@@ -182,6 +421,24 @@ class ProjectListView(urwid.WidgetWrap):
             if subscribed:
                 self.app.sync.submitTask(sync.SyncProjectTask(project_key))
             return None
+        if keymap.NEW_PROJECT_TOPIC in commands:
+            self.createTopic()
+            return None
+        if keymap.DELETE_PROJECT_TOPIC in commands:
+            self.deleteTopic()
+            return None
+        if keymap.COPY_PROJECT_TOPIC in commands:
+            self.copyToTopic()
+            return None
+        if keymap.MOVE_PROJECT_TOPIC in commands:
+            self.moveToTopic()
+            return None
+        if keymap.REMOVE_PROJECT_TOPIC in commands:
+            self.removeFromTopic()
+            return None
+        if keymap.RENAME_PROJECT_TOPIC in commands:
+            self.renameTopic()
+            return None
         if keymap.REFRESH in commands:
             self.app.sync.submitTask(
                 sync.SyncSubscribedProjectsTask(sync.HIGH_PRIORITY))