gui.py 23.8 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
bindtextdomain('poezio')
textdomain('poezio')
bind_textdomain_codeset('poezio', 'utf-8')

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

import curses
32 33
from datetime import datetime

34
from handler import Handler
35
from logging import logger
36
from random import randrange
37
from config import config
38
from window import Window
39 40
from user import User
from room import Room
41

42 43 44 45
class Gui(object):
    """
    Graphical user interface using ncurses
    """
46
    def __init__(self, stdscr=None, muc=None):
47
        self.room_number = 0
48 49
        self.init_curses(stdscr)
        self.stdscr = stdscr
50
        self.rooms = [Room('Info', '', self.next_room_number())]         # current_room is self.rooms[0]
51
        self.window = Window(stdscr)
52 53 54
        self.window.new_room(self.current_room())
        self.window.refresh(self.rooms)

55
        self.muc = muc
56

57
        self.commands = {
58 59 60 61
            '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.')),
62
            'next': (self.rotate_rooms_right, _('Usage: /next\nNext: Go to the next room.')),
63
            'n': (self.rotate_rooms_right, _('Usage: /n\nN: Go to the next room.')),
64
            'prev': (self.rotate_rooms_left, _('Usage: /prev\nPrev: Go to the previous room.')),
65
            'p': (self.rotate_rooms_left, _('Usage: /p\nP: Go to the previous room.')),
66 67
            '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.')),
68
            'part': (self.command_part, _('Usage: /part [message]\nPart: disconnect from a room. You can specify an optional message.')),
69
            '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')),
70 71 72
            '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]"')),
73 74
            '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)')),
75
            '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>.')),
76
            'kick': (self.command_kick, _('Usage: /kick <nick> [reason]\nKick: Kick the user with the specified nickname. You also can give an optional reason.')),
77
            'nick': (self.command_nick, _('Usage: /nick <nickname>\nNick: Change your nickname in the current room'))
78 79
            }

80 81 82 83 84 85 86
        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,
87
            "KEY_DC": self.window.input.key_dc,
88 89 90 91
            "KEY_F(5)": self.rotate_rooms_left,
            "KEY_F(6)": self.rotate_rooms_right,
            "kLFT5": self.rotate_rooms_left,
            "kRIT5": self.rotate_rooms_right,
92
            "\t": self.auto_completion,
93 94 95
            "KEY_BACKSPACE": self.window.input.key_backspace
            }

96 97 98 99 100
        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)
101
        self.handler.connect('room-delayed-message', self.room_delayed_message)
102

103 104
    def main_loop(self, stdscr):
        while 1:
105
            stdscr.leaveok(1)
106
            self.window.input.win.move(0, self.window.input.pos)
107
            curses.doupdate()
108 109 110 111
            try:
                key = stdscr.getkey()
            except:
                self.window.resize(stdscr)
112
                self.window.refresh(self.rooms)
113
                continue
114
            if str(key) in self.key_func.keys():
115
                self.key_func[key]()
116 117
            elif str(key) == 'KEY_RESIZE':
                self.window.resize(stdscr)
118
                self.window.refresh(self.rooms)
119 120
            elif len(key) >= 4:
                continue
121 122
            elif ord(key) == 10:
                self.execute()
123
            elif ord(key) == 8 or ord(key) == 127:
124
                self.window.input.key_backspace()
125 126
            elif ord(key) < 32:
                continue
127
            else:
128 129 130 131 132
                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
133
                elif ord(key) > 190 and ord(key) < 225:
134 135 136 137 138 139
                    key = key+stdscr.getkey()
                elif ord(key) == 226:
                    key = key+stdscr.getkey()
                    key = key+stdscr.getkey()
                self.window.do_command(key)

140 141 142 143 144
    def next_room_number(self):
        nb = self.room_number
        self.room_number += 1
        return nb

145
    def current_room(self):
146
        return self.rooms[0]
147 148 149 150 151 152 153

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

154 155 156
    def init_curses(self, stdscr):
        curses.start_color()
        curses.noecho()
157 158
        # curses.cbreak()
        # curses.raw()
159
        curses.use_default_colors()
160
        stdscr.keypad(True)
161
        curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE)
162 163 164 165 166 167 168 169
        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)
170
        curses.init_pair(10, curses.COLOR_WHITE, curses.COLOR_CYAN) # current room
171
        curses.init_pair(11, curses.COLOR_WHITE, curses.COLOR_BLUE) # normal room
172
        curses.init_pair(12, curses.COLOR_WHITE, curses.COLOR_MAGENTA) # new message room
173
        curses.init_pair(13, curses.COLOR_WHITE, curses.COLOR_RED) # highlight room
174 175
        curses.init_pair(14, curses.COLOR_WHITE, curses.COLOR_YELLOW)
        curses.init_pair(15, curses.COLOR_WHITE, curses.COLOR_GREEN)
176

177 178
    def reset_curses(self):
	curses.echo()
179
        curses.nocbreak()
180
        curses.endwin()
181

182
    def on_connected(self, jid):
183 184
        self.information(_("Welcome on Poezio \o/!"))
        self.information(_("Your JID is %s") % jid)
185 186

    def join_room(self, room, nick):
187 188 189 190 191 192 193 194 195 196 197 198 199
        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)
200

201 202 203
    def auto_completion(self):
        self.window.input.auto_completion(self.current_room().users)

204 205
    def rotate_rooms_right(self, args=None):
        self.current_room().set_color_state(11)
206
        self.rooms.append(self.rooms.pop(0))
207
        self.window.refresh(self.rooms)
208

209 210
    def rotate_rooms_left(self, args=None):
        self.current_room().set_color_state(11)
211
        self.rooms.insert(0, self.rooms.pop())
212
        self.window.refresh(self.rooms)
213

214 215 216 217
    def room_delayed_message(self, stanza):
        self.room_message(stanza)

    def room_message(self, stanza, date=None):
218
        if len(sys.argv) > 1:
louiz@4325f9fc-e183-4c21-96ce-0ab188b42d13's avatar
?  
219
            self.information(str(stanza).encode('utf-8'))
220 221
        if stanza.getType() != 'groupchat':
            return  # ignore all messages not comming from a MUC
222 223
        if not date:
            date = datetime.now()
224
        nick_from = stanza.getFrom().getResource()
225 226
        room_from = stanza.getFrom().getStripped()
        room = self.get_room_by_name(room_from)
227
	if not room:
228
	    self.information(_("message received for a non-existing room: %s") % (room_from))
229
            return
230
        body = stanza.getBody()
231 232
        subject = stanza.getSubject()
        if subject:
233
            if nick_from:
234
                self.add_info(room, _("%(nick)s changed the subject to: %(subject)s") % {'nick':nick_from, 'subject':subject}, date)
235
            else:
236
                self.add_info(room, _("The subject is: %(subject)s") % {'subject':subject}, date)
237
            room.topic = subject.encode('utf-8').replace('\n', '|')
238 239
            if room == self.current_room():
                self.window.topic_win.refresh(room.topic)
240
        else:
241 242
            if body.startswith('/me '):
                self.add_info(room, nick_from + ' ' + body[4:], date)
243
            else:
244
                self.add_message(room, nick_from, body, date)
245
        self.window.input.refresh()
246
        curses.doupdate()
247 248

    def room_presence(self, stanza):
249 250
        if len(sys.argv) > 1:
            self.information(str(stanza))
251 252
        from_nick = stanza.getFrom().getResource()
        from_room = stanza.getFrom().getStripped()
253
	room = self.get_room_by_name(from_room)
254
	if not room:
255
	    self.information(_("presence received for a non-existing room: %s") % (from_room))
256
        if stanza.getType() == 'error':
257
            msg = _("Error: %s") % stanza.getError()
258
        else:
259 260 261 262 263 264 265 266 267 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 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
            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)
                    reason = stanza.getReason().encode('utf-8') or ''
                    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})
315 316
            if room == self.current_room():
                self.window.user_win.refresh(room.users)
317 318
        self.window.input.refresh()
        curses.doupdate()
319 320 321 322 323 324 325

    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)
        """
326 327
        if not date:
            date = datetime.now()
328
        msg = room.add_info(info, date)
329
        self.window.text_win.add_line(room, (date, msg))
330
        if room.name == self.current_room().name:
331
            self.window.text_win.refresh(room.name)
louiz@4325f9fc-e183-4c21-96ce-0ab188b42d13's avatar
?  
332
            self.window.input.refresh()
333 334
            curses.doupdate()

335 336 337
    def add_message(self, room, nick_from, body, date=None):
        if not date:
            date = datetime.now()
338 339 340 341
        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)
342 343
        else:
            self.window.info_win.refresh(self.rooms, self.current_room())
344 345

    def execute(self):
346 347
        line = self.window.input.get_text()
        self.window.input.clear_text()
348
        self.window.input.refresh()
349 350
        if line == "":
            return
351
        if line.startswith('/'):
352 353 354
            command = line.strip()[:].split()[0][1:]
            args = line.strip()[:].split()[1:]
            if command in self.commands.keys():
355
                func = self.commands[command][0]
356
                func(args)
357
                return
358
            else:
359
                self.add_info(self.current_room(), _("Error: unknown command (%s)") % (command))
360
        elif self.current_room().name != 'Info':
361
            self.muc.send_message(self.current_room().name, line)
362
        self.window.input.refresh()
363
        curses.doupdate()
364

365 366 367
    def command_help(self, args):
        room = self.current_room()
        if len(args) == 0:
368
            msg = _('Available commands are:')
369 370
            for command in self.commands.keys():
                msg += "%s " % command
371
            msg += _("\nType /help <command_name> to know what each command does")
372 373 374 375
        if len(args) == 1:
            if args[0] in self.commands.keys():
                msg = self.commands[args[0]][1]
            else:
376
                msg = _('Unknown command: %s') % args[0]
377
        room.add_info(msg)
378
        self.window.text_win.add_line(room, (datetime.now(), msg))
379 380 381
        self.window.text_win.refresh(room.name)
        self.window.input.refresh()

382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403
    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)


404 405 406 407 408 409 410 411 412 413 414 415 416 417
    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)

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

446 447 448 449 450 451 452 453 454 455 456 457 458 459
    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]
460 461
            if roomname == '':
                roomname = self.current_room().name
462 463 464 465
        if nick:
            res = roomname+'/'+nick
        else:
            res = roomname
466 467 468 469 470 471 472 473 474
        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)
475
        config.set_and_save('rooms', bookmarked+':'+res)
476

477
    def command_set(self, args):
478
        if len(args) != 2 and len(args) != 1:
479 480 481
            self.command_help(['set'])
            return
        option = args[0]
482 483 484 485
        if len(args) == 2:
            value = args[1]
        else:
            value = ''
486
        config.set_and_save(option, value)
487 488 489
        msg = "%s=%s" % (option, value)
        room = self.current_room()
        room.add_info(msg)
490
        self.window.text_win.add_line(room, (datetime.now(), msg))
491 492 493
        self.window.text_win.refresh(room.name)
        self.window.input.refresh()

494
    def command_show(self, args):
495 496 497 498 499 500 501 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
        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)
531

532 533 534 535 536 537 538 539 540
    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
541 542
        if room.joined:
            self.muc.quit_room(room.name, room.own_nick, msg)
543
        self.rooms.remove(self.current_room())
544
        self.window.refresh(self.rooms)
545

546 547 548 549 550 551 552 553 554
    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)

555 556 557
    def information(self, msg):
        room = self.get_room_by_name("Info")
        info = room.add_info(msg)
558
        if self.current_room() == room:
559
            self.window.text_win.add_line(room, (datetime.now(), info))
560 561
            self.window.text_win.refresh(room.name)
            curses.doupdate()
562

563
    def command_quit(self, args):
564
	self.reset_curses()
565
        sys.exit()