gui.py 24.6 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 122 123
                debug("main_loop exception")
                # self.window.resize(stdscr)
                # self.window.refresh(self.rooms)
124
                continue
125
            if str(key) in self.key_func.keys():
126
                self.key_func[key]()
127 128
            elif str(key) == 'KEY_RESIZE':
                self.window.resize(stdscr)
129
                self.window.refresh(self.rooms)
130 131
            elif len(key) >= 4:
                continue
132 133
            elif ord(key) == 10:
                self.execute()
134
            elif ord(key) == 8 or ord(key) == 127:
135
                self.window.input.key_backspace()
136 137
            elif ord(key) < 32:
                continue
138
            else:
139
                if ord(key) == 27 and ord(stdscr.getkey()) == 91:
140
                    last = ord(stdscr.getkey()) # FIXME: ugly ugly workaround.
141 142 143
                    if last == 51:
                        self.window.input.key_dc()
                    continue
144
                elif ord(key) > 190 and ord(key) < 225:
145 146 147 148 149 150
                    key = key+stdscr.getkey()
                elif ord(key) == 226:
                    key = key+stdscr.getkey()
                    key = key+stdscr.getkey()
                self.window.do_command(key)

151 152 153 154 155
    def next_room_number(self):
        nb = self.room_number
        self.room_number += 1
        return nb

156
    def current_room(self):
157
        return self.rooms[0]
158 159 160 161 162 163 164

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

165 166 167
    def init_curses(self, stdscr):
        curses.start_color()
        curses.noecho()
168 169
        # curses.cbreak()
        # curses.raw()
170
        curses.use_default_colors()
171
        stdscr.keypad(True)
172
        curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE)
173 174 175 176 177 178 179 180
        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)
181
        curses.init_pair(10, curses.COLOR_WHITE, curses.COLOR_CYAN) # current room
182
        curses.init_pair(11, curses.COLOR_WHITE, curses.COLOR_BLUE) # normal room
183
        curses.init_pair(12, curses.COLOR_WHITE, curses.COLOR_MAGENTA) # new message room
184
        curses.init_pair(13, curses.COLOR_WHITE, curses.COLOR_RED) # highlight room
185 186
        curses.init_pair(14, curses.COLOR_WHITE, curses.COLOR_YELLOW)
        curses.init_pair(15, curses.COLOR_WHITE, curses.COLOR_GREEN)
187

188 189
    def reset_curses(self):
	curses.echo()
190
        curses.nocbreak()
191
        curses.endwin()
192

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

    def join_room(self, room, nick):
198 199 200 201 202 203 204 205 206 207 208 209 210
        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)
211

212 213 214
    def auto_completion(self):
        self.window.input.auto_completion(self.current_room().users)

215 216
    def rotate_rooms_right(self, args=None):
        self.current_room().set_color_state(11)
217
        self.rooms.append(self.rooms.pop(0))
218
        self.window.refresh(self.rooms)
219

220 221
    def rotate_rooms_left(self, args=None):
        self.current_room().set_color_state(11)
222
        self.rooms.insert(0, self.rooms.pop())
223
        self.window.refresh(self.rooms)
224

225 226 227 228 229 230 231 232 233
    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)'))

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

    def room_presence(self, stanza):
268 269
        if len(sys.argv) > 1:
            self.information(str(stanza))
270 271
        from_nick = stanza.getFrom().getResource()
        from_room = stanza.getFrom().getStripped()
272
	room = self.get_room_by_name(from_room)
273
	if not room:
274
            return
275
        else:
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
            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)
306 307 308 309
                    try:
                        reason = stanza.getReason().encode('utf-8')
                    except:
                        reason = ''
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
                    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})
335 336
            if room == self.current_room():
                self.window.user_win.refresh(room.users)
337 338
        self.window.input.refresh()
        curses.doupdate()
339 340 341 342 343 344 345

    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)
        """
346 347
        if not date:
            date = datetime.now()
348
        msg = room.add_info(info, date)
349
        self.window.text_win.add_line(room, (date, msg))
350
        if room.name == self.current_room().name:
351
            self.window.text_win.refresh(room.name)
louiz@4325f9fc-e183-4c21-96ce-0ab188b42d13's avatar
?  
352
            self.window.input.refresh()
353 354
            curses.doupdate()

355
    def add_message(self, room, nick_from, body, date=None, delayed=False):
356 357
        if not date:
            date = datetime.now()
358 359 360 361
        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)
362
        elif not delayed:
363
            self.window.info_win.refresh(self.rooms, self.current_room())
364 365

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

385 386 387
    def command_help(self, args):
        room = self.current_room()
        if len(args) == 0:
388
            msg = _('Available commands are:')
389 390
            for command in self.commands.keys():
                msg += "%s " % command
391
            msg += _("\nType /help <command_name> to know what each command does")
392 393 394 395
        if len(args) == 1:
            if args[0] in self.commands.keys():
                msg = self.commands[args[0]][1]
            else:
396
                msg = _('Unknown command: %s') % args[0]
397
        self.add_info(room, msg)
398

399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419
    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)

420 421 422 423 424 425 426 427 428 429 430 431 432 433
    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)

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

469 470 471 472 473 474 475 476 477 478 479 480 481 482
    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]
483 484
            if roomname == '':
                roomname = self.current_room().name
485 486 487 488
        if nick:
            res = roomname+'/'+nick
        else:
            res = roomname
489 490 491 492 493 494 495 496 497
        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)
498
        config.set_and_save('rooms', bookmarked+':'+res)
499

500
    def command_set(self, args):
501
        if len(args) != 2 and len(args) != 1:
502 503 504
            self.command_help(['set'])
            return
        option = args[0]
505 506 507 508
        if len(args) == 2:
            value = args[1]
        else:
            value = ''
509
        config.set_and_save(option, value)
510 511
        msg = "%s=%s" % (option, value)
        room = self.current_room()
512
        self.add_info(room, msg)
513

514
    def command_show(self, args):
515 516 517 518 519 520 521 522 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
        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)
551

552 553 554 555 556 557 558 559 560
    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
561 562
        if room.joined:
            self.muc.quit_room(room.name, room.own_nick, msg)
563
        self.rooms.remove(self.current_room())
564
        self.window.refresh(self.rooms)
565

566 567 568 569 570 571 572
    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)

573 574 575 576 577 578 579 580 581
    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)

582 583
    def information(self, msg):
        room = self.get_room_by_name("Info")
584
        self.add_info(room, msg)
585

586
    def command_quit(self, args):
587
	self.reset_curses()
588
        sys.exit()