gui.py 24 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 gettext import (bindtextdomain, textdomain, bind_textdomain_codeset,
                     gettext as _)
22

23

24 25 26 27
bindtextdomain('poezio')
textdomain('poezio')
bind_textdomain_codeset('poezio', 'utf-8')

28
import locale
29 30 31 32
locale.setlocale(locale.LC_ALL, '')
import sys

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

import common
40

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

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

62
        self.muc = muc
63

64
        self.commands = {
65 66 67 68
            'help': (self.command_help, _('OLOL, this is SOOO recursive')),
            'join': (self.command_join, _('Usage: /join [room_name][/nick]\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). Examples:\n/join room@server.tld\n/join room@server.tld/John\n/join /me_again\n/join')),
            '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.')),
69
            'next': (self.rotate_rooms_right, _('Usage: /next\nNext: Go to the next room.')),
70
            'n': (self.rotate_rooms_right, _('Usage: /n\nN: Go to the next room.')),
71
            'prev': (self.rotate_rooms_left, _('Usage: /prev\nPrev: Go to the previous room.')),
72
            'p': (self.rotate_rooms_left, _('Usage: /p\nP: Go to the previous room.')),
73 74
            '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.')),
75
            'part': (self.command_part, _('Usage: /part [message]\nPart: disconnect from a room. You can specify an optional message.')),
76
            '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')),
77 78 79
            '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]"')),
80 81
            '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]"')), 
           '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)')),
82
            '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>.')),
83
            'kick': (self.command_kick, _('Usage: /kick <nick> [reason]\nKick: Kick the user with the specified nickname. You also can give an optional reason.')),
84
            'topic': (self.command_topic, _('Usage: /topic <subject>\nTopic: Change the subject of the room')),
85
            'nick': (self.command_nick, _('Usage: /nick <nickname>\nNick: Change your nickname in the current room'))
86 87
            }

88 89 90 91 92 93 94
        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,
95
            "KEY_DC": self.window.input.key_dc,
96 97 98 99
            "KEY_F(5)": self.rotate_rooms_left,
            "KEY_F(6)": self.rotate_rooms_right,
            "kLFT5": self.rotate_rooms_left,
            "kRIT5": self.rotate_rooms_right,
100
            "\t": self.auto_completion,
101 102 103
            "KEY_BACKSPACE": self.window.input.key_backspace
            }

104 105 106 107 108
        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)
109
        self.handler.connect('error', self.information)
110

111 112
    def main_loop(self, stdscr):
        while 1:
113
            stdscr.leaveok(1)
114
            self.window.input.win.move(0, self.window.input.pos)
115
            curses.doupdate()
116 117 118 119
            try:
                key = stdscr.getkey()
            except:
                self.window.resize(stdscr)
120
                self.window.refresh(self.rooms)
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 137 138 139 140
                if ord(key) == 27 and ord(stdscr.getkey()) == 91:
                    last = ord(stdscr.getkey()) # FIXME: ugly ugly workaroung.
                    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
    def room_message(self, stanza, date=None):
223 224 225 226 227 228
        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
229
        if len(sys.argv) > 1:
louiz@4325f9fc-e183-4c21-96ce-0ab188b42d13's avatar
?  
230
            self.information(str(stanza).encode('utf-8'))
231 232 233
        if stanza.getType() != 'groupchat':
            return  # ignore all messages not comming from a MUC
        nick_from = stanza.getFrom().getResource()
234 235
        room_from = stanza.getFrom().getStripped()
        room = self.get_room_by_name(room_from)
236
	if not room:
237
	    self.information(_("message received for a non-existing room: %s") % (room_from))
238
            return
239
        body = stanza.getBody()
240 241
        subject = stanza.getSubject()
        if subject:
242
            if nick_from:
243
                self.add_info(room, _("%(nick)s changed the subject to: %(subject)s") % {'nick':nick_from, 'subject':subject}, date)
244
            else:
245
                self.add_info(room, _("The subject is: %(subject)s") % {'subject':subject}, date)
246
            room.topic = subject.encode('utf-8').replace('\n', '|')
247 248
            if room == self.current_room():
                self.window.topic_win.refresh(room.topic)
249
        else:
250 251
            if body.startswith('/me '):
                self.add_info(room, nick_from + ' ' + body[4:], date)
252
            else:
253
                self.add_message(room, nick_from, body, date, delayed)
254
        self.window.input.refresh()
255
        curses.doupdate()
256 257

    def room_presence(self, stanza):
258 259
        if len(sys.argv) > 1:
            self.information(str(stanza))
260 261
        from_nick = stanza.getFrom().getResource()
        from_room = stanza.getFrom().getStripped()
262
	room = self.get_room_by_name(from_room)
263
	if not room:
264
            return
265
        if stanza.getType() == 'error':
266
            msg = _("Error: %s") % stanza.getError()
267
        else:
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
            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))
                    if not config.get('hide_enter_join', "false") == "true":
                        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)
298 299 300 301
                    try:
                        reason = stanza.getReason().encode('utf-8')
                    except:
                        reason = ''
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326
                    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)
                    if not config.get('hide_enter_join', "false") == "true":
                        self.add_info(room, _('%s has left the room') % (from_nick))
                # status change
                else:
                    user.update(affiliation, show, status, role)
                    if not config.get('hide_status_change', "false") == "true":
                        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})
327 328
            if room == self.current_room():
                self.window.user_win.refresh(room.users)
329 330
        self.window.input.refresh()
        curses.doupdate()
331 332 333 334 335 336 337

    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)
        """
338 339
        if not date:
            date = datetime.now()
340
        msg = room.add_info(info, date)
341
        self.window.text_win.add_line(room, (date, msg))
342
        if room.name == self.current_room().name:
343
            self.window.text_win.refresh(room.name)
louiz@4325f9fc-e183-4c21-96ce-0ab188b42d13's avatar
?  
344
            self.window.input.refresh()
345 346
            curses.doupdate()

347
    def add_message(self, room, nick_from, body, date=None, delayed=False):
348 349
        if not date:
            date = datetime.now()
350 351 352 353
        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)
354
        elif not delayed:
355
            self.window.info_win.refresh(self.rooms, self.current_room())
356 357

    def execute(self):
358 359
        line = self.window.input.get_text()
        self.window.input.clear_text()
360
        self.window.input.refresh()
361 362
        if line == "":
            return
363
        if line.startswith('/'):
364 365 366
            command = line.strip()[:].split()[0][1:]
            args = line.strip()[:].split()[1:]
            if command in self.commands.keys():
367
                func = self.commands[command][0]
368
                func(args)
369
                return
370
            else:
371
                self.add_info(self.current_room(), _("Error: unknown command (%s)") % (command))
372
        elif self.current_room().name != 'Info':
373
            self.muc.send_message(self.current_room().name, line)
374
        self.window.input.refresh()
375
        curses.doupdate()
376

377 378 379
    def command_help(self, args):
        room = self.current_room()
        if len(args) == 0:
380
            msg = _('Available commands are:')
381 382
            for command in self.commands.keys():
                msg += "%s " % command
383
            msg += _("\nType /help <command_name> to know what each command does")
384 385 386 387
        if len(args) == 1:
            if args[0] in self.commands.keys():
                msg = self.commands[args[0]][1]
            else:
388
                msg = _('Unknown command: %s') % args[0]
389
        self.add_info(room, msg)
390

391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411
    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)

412 413 414 415 416 417 418 419 420 421 422 423 424 425
    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)

426
    def command_join(self, args):
427 428 429 430 431 432
        if len(args) == 0:
            r = self.current_room()
            if r.name == 'Info':
                return
            room = r.name
            nick = r.own_nick
433
        else:
434 435 436 437 438
            info = args[0].split('/')
            if len(info) == 1:
                nick = config.get('default_nick', 'Poezio')
            else:
                nick = info[1]
439
            if info[0] == '':   # happens with /join /nickname, which is OK
440 441 442 443 444 445 446
                r = self.current_room()
                if r.name == 'Info':
                    return
                room = r.name
            else:
                room = info[0]
            r = self.get_room_by_name(room)
447
        if r and r.joined:                   # if we are already in the room
448
            self.information(_("already in room [%s]") % room)
449
            return
450
        self.muc.join_room(room, nick)
451
        if not r:   # if the room window exists, we don't recreate it.
452
            self.join_room(room, nick)
453 454
        else:
            r.users = []
455

456 457 458 459 460 461 462 463 464 465 466 467 468 469
    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]
470 471
            if roomname == '':
                roomname = self.current_room().name
472 473 474 475
        if nick:
            res = roomname+'/'+nick
        else:
            res = roomname
476 477 478 479 480 481 482 483 484
        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)
485
        config.set_and_save('rooms', bookmarked+':'+res)
486

487
    def command_set(self, args):
488
        if len(args) != 2 and len(args) != 1:
489 490 491
            self.command_help(['set'])
            return
        option = args[0]
492 493 494 495
        if len(args) == 2:
            value = args[1]
        else:
            value = ''
496
        config.set_and_save(option, value)
497 498
        msg = "%s=%s" % (option, value)
        room = self.current_room()
499
        self.add_info(room, msg)
500

501
    def command_show(self, args):
502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537
        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)
538

539 540 541 542 543 544 545 546 547
    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
548 549
        if room.joined:
            self.muc.quit_room(room.name, room.own_nick, msg)
550
        self.rooms.remove(self.current_room())
551
        self.window.refresh(self.rooms)
552

553 554 555 556 557 558 559
    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)

560 561 562 563 564 565 566 567 568
    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)

569 570
    def information(self, msg):
        room = self.get_room_by_name("Info")
571
        self.add_info(room, msg)
572

573
    def command_quit(self, args):
574
	self.reset_curses()
575
        sys.exit()