gui.py 25.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
#!/usr/bin/python
# -*- coding:utf-8 -*-
#
# Copyright 2010 Le Coz Florent <louizatakk@fedoraproject.org>
#
# This file is part of Poezio.
#
# Poezio is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# Poezio is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Poezio.  If not, see <http://www.gnu.org/licenses/>.

20 21
from common import debug

22 23
from gettext import (bindtextdomain, textdomain, bind_textdomain_codeset,
                     gettext as _)
24

25

26 27 28 29
bindtextdomain('poezio')
textdomain('poezio')
bind_textdomain_codeset('poezio', 'utf-8')

30
import locale
31 32 33 34
locale.setlocale(locale.LC_ALL, '')
import sys

import curses
35
import xmpp
36
from datetime import datetime
37 38 39 40 41
from time import (altzone, daylight, gmtime, localtime, mktime, strftime,
                  time as time_time, timezone, tzname)
from calendar import timegm

import common
42

43
from handler import Handler
44
from logging import logger
45
from random import randrange
46
from config import config
47
from window import Window
48 49
from user import User
from room import Room
50

51 52 53 54
class Gui(object):
    """
    Graphical user interface using ncurses
    """
55
    def __init__(self, stdscr=None, muc=None):
56
        self.room_number = 0
57 58
        self.init_curses(stdscr)
        self.stdscr = stdscr
59
        self.rooms = [Room('Info', '', self.next_room_number())]         # current_room is self.rooms[0]
60
        self.window = Window(stdscr)
61 62 63
        self.window.new_room(self.current_room())
        self.window.refresh(self.rooms)

64
        self.muc = muc
65

66
        self.commands = {
67
            'help': (self.command_help, _('OLOL, this is SOOO recursive')),
68
            'join': (self.command_join, _('Usage: /join [room_name][/nick] [password]\nJoin: Join the specified room. You can specify a nickname after a slash (/). If no nickname is specified, you will use the default_nick in the configuration file. You can omit the room name: you will then join the room you\'re looking at (useful if you were kicked). You can also provide a password to join the room.\nExamples:\n/join room@server.tld\n/join room@server.tld/John\n/join /me_again\n/join\n/join room@server.tld/my_nick password\n/join / pass')),
69 70
            'quit': (self.command_quit, _('Usage: /quit\nQuit: Just disconnect from the server and exit poezio.')),
            'exit': (self.command_quit, _('Usage: /exit\nExit: Just disconnect from the server and exit poezio.')),
71
            'next': (self.rotate_rooms_right, _('Usage: /next\nNext: Go to the next room.')),
72
            'n': (self.rotate_rooms_right, _('Usage: /n\nN: Go to the next room.')),
73
            'prev': (self.rotate_rooms_left, _('Usage: /prev\nPrev: Go to the previous room.')),
74
            'p': (self.rotate_rooms_left, _('Usage: /p\nP: Go to the previous room.')),
75 76
            'win': (self.command_win, _('Usage: /win <number>\nWin: Go to the specified room.')),
            'w': (self.command_win, _('Usage: /w <number>\nW: Go to the specified room.')),
77
            'part': (self.command_part, _('Usage: /part [message]\nPart: disconnect from a room. You can specify an optional message.')),
78
            'show': (self.command_show, _(u'Usage: /show <availability> [status]\nShow: Change your availability and (optionaly) your status. The <availability> argument is one of "avail, available, ok, here, chat, away, afk, dnd, busy, xa" and the optional [message] argument will be your status message')),
79 80 81
            'away': (self.command_away, _('Usage: /away [message]\nAway: Sets your availability to away and (optional) sets your status message. This is equivalent to "/show away [message]"')),
            'busy': (self.command_busy, _('Usage: /busy [message]\nBusy: Sets your availability to busy and (optional) sets your status message. This is equivalent to "/show busy [message]"')),
            'avail': (self.command_avail, _('Usage: /avail [message]\nAvail: Sets your availability to available and (optional) sets your status message. This is equivalent to "/show available [message]"')),
82
            'available': (self.command_avail, _('Usage: /available [message]\nAvailable: Sets your availability to available and (optional) sets your status message. This is equivalent to "/show available [message]"')),
83
           'bookmark': (self.command_bookmark, _('Usage: /bookmark [roomname][/nick]\nBookmark: Bookmark the specified room (you will then auto-join it on each poezio start). This commands uses the same syntaxe as /join. Type /help join for syntaxe examples. Note that when typing "/bookmark" on its own, the room will be bookmarked with the nickname you\'re currently using in this room (instead of default_nick)')),
84
            'set': (self.command_set, _('Usage: /set <option> [value]\nSet: Sets the value to the option in your configuration file. You can, for example, change your default nickname by doing `/set default_nick toto` or your resource with `/set resource blabla`. You can also set an empty value (nothing) by providing no [value] after <option>.')),
85
            'kick': (self.command_kick, _('Usage: /kick <nick> [reason]\nKick: Kick the user with the specified nickname. You also can give an optional reason.')),
86
            'topic': (self.command_topic, _('Usage: /topic <subject>\nTopic: Change the subject of the room')),
87
            'nick': (self.command_nick, _('Usage: /nick <nickname>\nNick: Change your nickname in the current room'))
88 89
            }

90 91 92 93 94 95 96
        self.key_func = {
            "KEY_LEFT": self.window.input.key_left,
            "KEY_RIGHT": self.window.input.key_right,
            "KEY_UP": self.window.input.key_up,
            "KEY_END": self.window.input.key_end,
            "KEY_HOME": self.window.input.key_home,
            "KEY_DOWN": self.window.input.key_down,
97
            "KEY_DC": self.window.input.key_dc,
98 99 100 101
            "KEY_F(5)": self.rotate_rooms_left,
            "KEY_F(6)": self.rotate_rooms_right,
            "kLFT5": self.rotate_rooms_left,
            "kRIT5": self.rotate_rooms_right,
102
            "\t": self.auto_completion,
103 104 105
            "KEY_BACKSPACE": self.window.input.key_backspace
            }

106 107 108 109 110
        self.handler = Handler()
        self.handler.connect('on-connected', self.on_connected)
        self.handler.connect('join-room', self.join_room)
        self.handler.connect('room-presence', self.room_presence)
        self.handler.connect('room-message', self.room_message)
111
        self.handler.connect('error-message', self.room_error)
112
        self.handler.connect('error', self.information)
113

114 115
    def main_loop(self, stdscr):
        while 1:
116
            stdscr.leaveok(1)
117
            curses.doupdate()
118 119 120
            try:
                key = stdscr.getkey()
            except:
121
                continue
122
            if str(key) in self.key_func.keys():
123
                self.key_func[key]()
124 125
            elif str(key) == 'KEY_RESIZE':
                self.window.resize(stdscr)
126
                self.window.refresh(self.rooms)
127 128
            elif len(key) >= 4:
                continue
129 130
            elif ord(key) == 10:
                self.execute()
131
            elif ord(key) == 8 or ord(key) == 127:
132
                self.window.input.key_backspace()
133 134
            elif ord(key) < 32:
                continue
135
            else:
136
                if ord(key) == 27 and ord(stdscr.getkey()) == 91:
137
                    last = ord(stdscr.getkey()) # FIXME: ugly ugly workaround.
138 139 140
                    if last == 51:
                        self.window.input.key_dc()
                    continue
141
                elif ord(key) > 190 and ord(key) < 225:
142 143 144 145 146 147
                    key = key+stdscr.getkey()
                elif ord(key) == 226:
                    key = key+stdscr.getkey()
                    key = key+stdscr.getkey()
                self.window.do_command(key)

148 149 150 151 152
    def next_room_number(self):
        nb = self.room_number
        self.room_number += 1
        return nb

153
    def current_room(self):
154
        return self.rooms[0]
155 156 157 158 159 160 161

    def get_room_by_name(self, name):
	for room in self.rooms:
	    if room.name == name:
		return room
	return None

162 163 164
    def init_curses(self, stdscr):
        curses.start_color()
        curses.noecho()
165 166
        # curses.cbreak()
        # curses.raw()
167
        curses.use_default_colors()
168
        stdscr.keypad(True)
169
        curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE)
170 171 172 173 174 175 176 177
        curses.init_pair(2, curses.COLOR_BLUE, -1)
        curses.init_pair(3, curses.COLOR_RED, -1) # Admin
        curses.init_pair(4, curses.COLOR_BLUE, -1) # Participant
        curses.init_pair(5, curses.COLOR_WHITE, -1) # Visitor
        curses.init_pair(6, curses.COLOR_CYAN, -1)
        curses.init_pair(7, curses.COLOR_GREEN, -1)
        curses.init_pair(8, curses.COLOR_MAGENTA, -1)
        curses.init_pair(9, curses.COLOR_YELLOW, -1)
178
        curses.init_pair(10, curses.COLOR_WHITE, curses.COLOR_CYAN) # current room
179
        curses.init_pair(11, curses.COLOR_WHITE, curses.COLOR_BLUE) # normal room
180
        curses.init_pair(12, curses.COLOR_WHITE, curses.COLOR_MAGENTA) # new message room
181
        curses.init_pair(13, curses.COLOR_WHITE, curses.COLOR_RED) # highlight room
182 183
        curses.init_pair(14, curses.COLOR_WHITE, curses.COLOR_YELLOW)
        curses.init_pair(15, curses.COLOR_WHITE, curses.COLOR_GREEN)
184

185 186
    def reset_curses(self):
	curses.echo()
187
        curses.nocbreak()
188
        curses.endwin()
189

190
    def on_connected(self, jid):
191 192
        self.information(_("Welcome on Poezio \o/!"))
        self.information(_("Your JID is %s") % jid)
193 194

    def join_room(self, room, nick):
195 196 197 198 199 200 201 202 203 204 205 206 207
        r = Room(room, nick, self.next_room_number())
        self.current_room().set_color_state(11)
        if self.current_room().nb == 0:
            self.rooms.append(r)
        else:
            for ro in self.rooms:
                if ro.nb == 0:
                    self.rooms.insert(self.rooms.index(ro), r)
                    break
        while self.current_room().nb != r.nb:
            self.rooms.insert(0, self.rooms.pop())
        self.window.new_room(r)
        self.window.refresh(self.rooms)
208

209 210 211
    def auto_completion(self):
        self.window.input.auto_completion(self.current_room().users)

212 213
    def rotate_rooms_right(self, args=None):
        self.current_room().set_color_state(11)
214
        self.rooms.append(self.rooms.pop(0))
215
        self.window.refresh(self.rooms)
216

217 218
    def rotate_rooms_left(self, args=None):
        self.current_room().set_color_state(11)
219
        self.rooms.insert(0, self.rooms.pop())
220
        self.window.refresh(self.rooms)
221

222 223 224 225 226 227 228 229 230
    def room_error(self, room, error, msg):
        r = self.get_room_by_name(room)
        code = error.getAttr('code')
        typ = error.getAttr('type')
        body = error.getTag('text').getData()
        self.add_info(r, _('Error: %(code)s-%(msg)s: %(body)s' % {'msg':msg, 'code':code, 'body':body}))
        if code == '401':
            self.add_info(r, _('To provide a password in order to join the room, type "/join / password" (replace "password" by the real password)'))

231
    def room_message(self, stanza, date=None):
232 233 234 235 236 237
        delay_tag = stanza.getTag('delay', namespace='urn:xmpp:delay')
        if delay_tag and not date:
            delayed = True
            date = common.datetime_tuple(delay_tag.getAttr('stamp'))
        else:
            delayed = False
238 239 240
        if stanza.getType() != 'groupchat':
            return  # ignore all messages not comming from a MUC
        nick_from = stanza.getFrom().getResource()
241 242
        room_from = stanza.getFrom().getStripped()
        room = self.get_room_by_name(room_from)
243
	if not room:
244
	    self.information(_("message received for a non-existing room: %s") % (room_from))
245
            return
246
        body = stanza.getBody()
247 248
        subject = stanza.getSubject()
        if subject:
249
            if nick_from:
250
                self.add_info(room, _("%(nick)s changed the subject to: %(subject)s") % {'nick':nick_from, 'subject':subject}, date)
251
            else:
252
                self.add_info(room, _("The subject is: %(subject)s") % {'subject':subject}, date)
253
            room.topic = subject.encode('utf-8').replace('\n', '|')
254 255
            if room == self.current_room():
                self.window.topic_win.refresh(room.topic)
256
        elif body:
257 258
            if body.startswith('/me '):
                self.add_info(room, nick_from + ' ' + body[4:], date)
259
            else:
260
                self.add_message(room, nick_from, body, date, delayed)
261
        self.window.input.refresh()
262
        curses.doupdate()
263 264

    def room_presence(self, stanza):
265 266
        if len(sys.argv) > 1:
            self.information(str(stanza))
267 268
        from_nick = stanza.getFrom().getResource()
        from_room = stanza.getFrom().getStripped()
269
	room = self.get_room_by_name(from_room)
270
	if not room:
271
            return
272
        else:
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
            msg = None
            affiliation = stanza.getAffiliation()
            show = stanza.getShow()
            status = stanza.getStatus()
            role = stanza.getRole()
            if not room.joined:     # user in the room BEFORE us.
                room.users.append(User(from_nick, affiliation, show, status, role))
                if from_nick.encode('utf-8') == room.own_nick:
                    room.joined = True
                    self.add_info(room, _("Your nickname is %s") % (from_nick))
                else:
                    self.add_info(room, _("%s is in the room") % (from_nick.encode('utf-8')))
            else:
                change_nick = stanza.getStatusCode() == '303'
                kick = stanza.getStatusCode() == '307'
                user = room.get_user_by_name(from_nick)
                # New user
                if not user:
                    room.users.append(User(from_nick, affiliation, show, status, role))
292 293
                    hide_exit_join = config.get('hide_exit_join', -1)
                    if hide_exit_join != 0:
294 295 296 297 298 299 300 301 302 303
                        self.add_info(room, _('%(nick)s joined the room %(roomname)s') % {'nick':from_nick, 'roomname': room.name})
                # nick change
                elif change_nick:
                    if user.nick == room.own_nick:
                        room.own_nick = stanza.getNick().encode('utf-8')
                    user.change_nick(stanza.getNick())
                    self.add_info(room, _('%(old_nick)s is now known as %(new_nick)s') % {'old_nick':from_nick, 'new_nick':stanza.getNick()})
                # kick
                elif kick:
                    room.users.remove(user)
304 305 306 307
                    try:
                        reason = stanza.getReason().encode('utf-8')
                    except:
                        reason = ''
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
                    try:
                        by = stanza.getActor().encode('utf-8')
                    except:
                        by = None
                    if from_nick == room.own_nick: # we are kicked
                        room.disconnect()
                        if by:
                            self.add_info(room, _('You have been kicked by %(by)s. Reason: %(reason)s') % {'by':by, 'reason':reason})
                        else:
                            self.add_info(room, _('You have been kicked. Reason: %s') % (reason))
                    else:
                        if by:
                            self.add_info(room, _('%(nick)s has been kicked by %(by)s. Reason: %(reason)s') % {'nick':from_nick, 'by':by, 'reason':reason})
                        else:
                            self.add_info(room, _('%(nick)s has been kicked. Reason: %(reason)s') % {'nick':from_nick, 'reason':reason})
                # user quit
                elif status == 'offline' or role == 'none':
                    room.users.remove(user)
326 327 328 329 330
                    hide_exit_join = config.get('hide_exit_join', -1)\
                        if config.get('hide_exit_join', -1) >= -1\
                        else -1
                    if hide_exit_join == -1 or \
                            user.has_talked_since(hide_exit_join):
331 332 333 334
                        self.add_info(room, _('%s has left the room') % (from_nick))
                # status change
                else:
                    user.update(affiliation, show, status, role)
335 336 337 338 339 340
                    hide_status_change = config.get('hide_status_change', -1)\
                        if config.get('hide_status_change', -1) >= -1\
                        else -1
                    if hide_status_change == -1 or \
                            user.has_talked_since(hide_status_change) or\
                            user.nick == room.own_nick:
341
                        self.add_info(room, _('%(nick)s changed his/her status : %(a)s, %(b)s, %(c)s, %(d)s') % {'nick':from_nick, 'a':affiliation, 'b':role, 'c':show, 'd':status})
342 343
            if room == self.current_room():
                self.window.user_win.refresh(room.users)
344 345
        self.window.input.refresh()
        curses.doupdate()
346 347 348 349 350 351 352

    def add_info(self, room, info, date=None):
        """
        add a new information in the specified room
        (displays it immediately AND saves it for redisplay
        in futur refresh)
        """
353 354
        if not date:
            date = datetime.now()
355
        msg = room.add_info(info, date)
356
        self.window.text_win.add_line(room, (date, msg))
357
        if room.name == self.current_room().name:
358
            self.window.text_win.refresh(room.name)
louiz@4325f9fc-e183-4c21-96ce-0ab188b42d13's avatar
?  
359
            self.window.input.refresh()
360 361
            curses.doupdate()

362
    def add_message(self, room, nick_from, body, date=None, delayed=False):
363 364
        if not date:
            date = datetime.now()
365 366 367 368
        color = room.add_message(nick_from, body, date)
        self.window.text_win.add_line(room, (date, nick_from.encode('utf-8'), body.encode('utf-8'), color))
        if room == self.current_room():
            self.window.text_win.refresh(room.name)
369
        elif not delayed:
370
            self.window.info_win.refresh(self.rooms, self.current_room())
371 372

    def execute(self):
373 374
        line = self.window.input.get_text()
        self.window.input.clear_text()
375
        self.window.input.refresh()
376 377
        if line == "":
            return
378
        if line.startswith('/'):
379 380 381
            command = line.strip()[:].split()[0][1:]
            args = line.strip()[:].split()[1:]
            if command in self.commands.keys():
382
                func = self.commands[command][0]
383
                func(args)
384
                return
385
            else:
386
                self.add_info(self.current_room(), _("Error: unknown command (%s)") % (command))
387
        elif self.current_room().name != 'Info':
388
            self.muc.send_message(self.current_room().name, line)
389
        self.window.input.refresh()
390
        curses.doupdate()
391

392 393 394
    def command_help(self, args):
        room = self.current_room()
        if len(args) == 0:
395
            msg = _('Available commands are:')
396 397
            for command in self.commands.keys():
                msg += "%s " % command
398
            msg += _("\nType /help <command_name> to know what each command does")
399 400 401 402
        if len(args) == 1:
            if args[0] in self.commands.keys():
                msg = self.commands[args[0]][1]
            else:
403
                msg = _('Unknown command: %s') % args[0]
404
        self.add_info(room, msg)
405

406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426
    def command_win(self, args):
        if len(args) != 1:
            self.command_help(['win'])
            return
        try:
            nb = int(args[0])
        except ValueError:
            self.command_help(['win'])
            return
        if self.current_room().nb == nb:
            return
        self.current_room().set_color_state(11)
        start = self.current_room()
        self.rooms.append(self.rooms.pop(0))
        while self.current_room().nb != nb:
            self.rooms.append(self.rooms.pop(0))
            if self.current_room() == start:
                self.window.refresh(self.rooms)
                return
        self.window.refresh(self.rooms)

427 428 429 430 431 432 433 434 435 436 437 438 439 440
    def command_kick(self, args):
        if len(args) < 1:
            self.command_help(['kick'])
            return
        nick = args[0]
        if len(args) >= 2:
            reason = ' '.join(args[1:])
        else:
            reason = ''
        if self.current_room().name == 'Info' or not self.current_room().joined:
            return
        roomname = self.current_room().name
        self.muc.eject_user(roomname, 'kick', nick, reason)

441
    def command_join(self, args):
442
        password = None
443 444 445 446 447 448
        if len(args) == 0:
            r = self.current_room()
            if r.name == 'Info':
                return
            room = r.name
            nick = r.own_nick
449
        else:
450 451 452 453 454
            info = args[0].split('/')
            if len(info) == 1:
                nick = config.get('default_nick', 'Poezio')
            else:
                nick = info[1]
455
            if info[0] == '':   # happens with /join /nickname, which is OK
456 457 458 459
                r = self.current_room()
                if r.name == 'Info':
                    return
                room = r.name
460 461
                if nick == '':
                    nick = r.own_nick
462 463 464
            else:
                room = info[0]
            r = self.get_room_by_name(room)
465 466
        if len(args) == 2:       # a password is provided
            password = args[1]
467
        if r and r.joined:                   # if we are already in the room
468
            self.information(_("already in room [%s]") % room)
469
            return
470
        self.muc.join_room(room, nick, password)
471
        if not r:   # if the room window exists, we don't recreate it.
472
            self.join_room(room, nick)
473
        else:
474
            r.own_nick = nick
475
            r.users = []
476

477 478 479 480 481 482 483 484 485 486 487 488 489 490
    def command_bookmark(self, args):
        nick = None
        if len(args) == 0:
            room = self.current_room()
            if room.name == 'Info':
                return
            roomname = room.name
            if room.joined:
                nick = room.own_nick
        else:
            info = args[0].split('/')
            if len(info) == 2:
                nick = info[1]
            roomname = info[0]
491 492
            if roomname == '':
                roomname = self.current_room().name
493 494 495 496
        if nick:
            res = roomname+'/'+nick
        else:
            res = roomname
497 498 499 500 501 502 503 504 505
        bookmarked = config.get('rooms', '')
        # check if the room is already bookmarked.
        # if yes, replace it (i.e., update the associated nick)
        bookmarked = bookmarked.split(':')
        for room in bookmarked:
            if room.split('/')[0] == roomname:
                bookmarked.remove(room)
                break
        bookmarked = ':'.join(bookmarked)
506
        config.set_and_save('rooms', bookmarked+':'+res)
507

508
    def command_set(self, args):
509
        if len(args) != 2 and len(args) != 1:
510 511 512
            self.command_help(['set'])
            return
        option = args[0]
513 514 515 516
        if len(args) == 2:
            value = args[1]
        else:
            value = ''
517
        config.set_and_save(option, value)
518 519
        msg = "%s=%s" % (option, value)
        room = self.current_room()
520
        self.add_info(room, msg)
521

522
    def command_show(self, args):
523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558
        possible_show = {'avail':'None',
                         'available':'None',
                         'ok':'None',
                         'here':'None',
                         'chat':'chat',
                         'away':'away',
                         'afk':'away',
                         'dnd':'dnd',
                         'busy':'dnd',
                         'xa':'xa'
                         }
        if len(args) < 1:
            return
        if not args[0] in possible_show.keys():
            self.command_help(['show'])
            return
        show = possible_show[args[0]]
        if len(args) > 1:
            msg = ' '.join(args[1:])
        else:
            msg = None
        for room in self.rooms:
            if room.joined:
                self.muc.change_show(room.name, room.own_nick, show, msg)

    def command_away(self, args):
        args.insert(0, 'away')
        self.command_show(args)

    def command_busy(self, args):
        args.insert(0, 'busy')
        self.command_show(args)

    def command_avail(self, args):
        args.insert(0, 'available')
        self.command_show(args)
559

560 561 562 563 564 565 566 567 568
    def command_part(self, args):
        reason = None
        room = self.current_room()
        if room.name == 'Info':
            return
        if len(args):
            msg = ' '.join(args)
        else:
            msg = None
569 570
        if room.joined:
            self.muc.quit_room(room.name, room.own_nick, msg)
571
        self.rooms.remove(self.current_room())
572
        self.window.refresh(self.rooms)
573

574 575 576 577 578 579 580
    def command_topic(self, args):
        subject = ' '.join(args)
        room = self.current_room()
        if not room.joined or room.name == "Info":
            return
        self.muc.change_subject(room.name, subject)

581 582 583 584 585 586 587 588 589
    def command_nick(self, args):
        if len(args) != 1:
            return
        nick = args[0]
        room = self.current_room()
        if not room.joined or room.name == "Info":
            return
        self.muc.change_nick(room.name, nick)

590 591
    def information(self, msg):
        room = self.get_room_by_name("Info")
592
        self.add_info(room, msg)
593

594
    def command_quit(self, args):
595
	self.reset_curses()
596
        sys.exit()