view frontends/sortilege/sortilege.py @ 0:c4bc297b82f0

sat: - first public release, initial commit
author goffi@necton2
date Sat, 29 Aug 2009 13:34:59 +0200
parents
children bb72c29f3432
line wrap: on
line source

#!/usr/bin/python
# -*- coding: utf-8 -*-

"""
sortilege: a SAT frontend
Copyright (C) 2009  Jérôme Poisson (goffi@goffi.org)

This program 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, either version 3 of the License, or
(at your option) any later version.

This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
"""


import curses
import pdb
from window import Window
from editbox import EditBox
from statusbar import StatusBar
from chat import Chat
from tools.jid  import JID
import logging
from logging import debug, info, error
import locale
import sys, os
import gobject
import time
from curses import ascii
import locale
from signal import signal, SIGWINCH 
import fcntl
import struct
import termios
from boxsizer import BoxSizer
from quick_frontend.quick_chat_list import QuickChatList
from quick_frontend.quick_contact_list import QuickContactList
from quick_frontend.quick_app import QuickApp

### logging configuration FIXME: put this elsewhere ###
logging.basicConfig(level=logging.CRITICAL,  #TODO: configure it top put messages in a log file
                    format='%(message)s')
###

const_APP_NAME      = "Sortilège"
const_CONTACT_WIDTH = 30

def ttysize():
    """This function return term size.
    Comes from Donn Cave from python list mailing list"""
    buf = 'abcdefgh'
    buf = fcntl.ioctl(0, termios.TIOCGWINSZ, buf)
    row, col, rpx, cpx = struct.unpack('hhhh', buf)
    return row, col

def C(k):
    """return the value of Ctrl+key"""
    return ord(ascii.ctrl(k))

class ChatList(QuickChatList):
    """This class manage the list of chat windows"""
    
    def __init__(self, host):
        QuickChatList.__init__(self, host)
        self.sizer=host.sizer

    def createChat(self, name):
        chat = Chat(name, self.host)
        self.sizer.appendColum(0,chat)
        self.sizer.update()
        return chat 
        

class ContactList(Window, QuickContactList):
    
    def __init__(self):
        QuickContactList.__init__(self)
        self.__index=0  #indicate which contact is selected (ie: where we are)
        Window.__init__(self, stdscr, stdscr.getmaxyx()[0]-2,const_CONTACT_WIDTH,0,0, True, "Contact List", code=code)

    def resize(self, height, width, y, x):
        Window.resize(self, height, width, y, x)
        self.update()

    def resizeAdapt(self):
        """Adapt window size to stdscr size.
        Must be called when stdscr is resized."""
        self.resize(stdscr.getmaxyx()[0]-2,const_CONTACT_WIDTH,0,0)
        self.update()
        
    def registerEnterCB(self, CB):
        self.__enterCB=CB

    def replace(self, jid, name="", show="", status="", group=""):
        """add a contact to the list"""
        self.jid_ids[jid] = name or jid
        self.update()

    def indexUp(self):
        """increment select contact index"""
        if self.__index < len(self.jid_ids)-1:  #we dont want to select a missing contact
            self.__index = self.__index + 1
        self.update()
    
    def indexDown(self):
        """decrement select contact index"""
        if self.__index > 0:
            self.__index = self.__index - 1
        self.update()

    def remove(self, jid):
        """remove a contact from the list"""
        del self.jid_ids[jid]
        if self.__index >= len(self.jid_ids) and self.__index > 0:  #if select index is out of border, we put it on the last contact
            self.__index = len(self.jid_ids)-1
        self.update()

    def update(self):
        """redraw all the window"""
        if self.isHidden():
            return
        Window.update(self)
        viewList=[]
        for jid in self.jid_ids:
            viewList.append(self.jid_ids[jid])
        viewList.sort()
        begin=0 if self.__index<self.rHeight else self.__index-self.rHeight+1 
        idx=0
        for item in viewList[begin:self.rHeight+begin]:
            attr = curses.A_REVERSE if ( self.isActive() and (idx+begin) == self.__index ) else 0
            centered = item.center(self.rWidth) ## it's nicer in the center :)
            self.addYXStr(idx, 0, centered, attr)
            idx = idx + 1

        self.noutrefresh()

    def handleKey(self, k):
        if k == curses.KEY_UP:
            self.indexDown()
        elif k == curses.KEY_DOWN:
            self.indexUp()
        elif k == ascii.NL:
            if not self.jid_ids:
                return
            try:
                self.__enterCB(self.jid_ids.keys()[self.__index])
            except NameError:
                pass # TODO: thrown an error here
            
class SortilegeApp(QuickApp):
    
    def __init__(self):
        #debug(const_APP_NAME+" init...")

        ## unicode support ##
        locale.setlocale(locale.LC_ALL, '')
        global code
        code = locale.getpreferredencoding()
        self.code=code

        ## main loop setup ##
        self.loop=gobject.MainLoop()
        gobject.io_add_watch(0, gobject.IO_IN, self.loopCB)

        ## misc init stuff ##
        self.listWins=[]
        self.chatParams={'timestamp':True,
                         'color':True,
                         'short_nick':False}

    def start(self):
        curses.wrapper(self.start_curses)

    def start_curses(self, win):
        global stdscr
        stdscr = win
        self.stdscr = stdscr
        curses.raw() #we handle everything ourself
        curses.curs_set(False)
        stdscr.nodelay(True)

        ## colours ##
        self.color(True)

        ## windows ##
        self.contactList = ContactList()
        self.editBar = EditBox(stdscr, "> ", self.code)
        self.editBar.activate(False)
        self.statusBar = StatusBar(stdscr, self.code)
        self.statusBar.hide(True)
        self.addWin(self.contactList)
        self.addWin(self.editBar)
        self.addWin(self.statusBar)
        self.sizer=BoxSizer(stdscr)
        self.sizer.appendRow(self.contactList)
        self.sizer.appendRow(self.statusBar)
        self.sizer.appendRow(self.editBar)
        self.currentChat=None

        self.contactList.registerEnterCB(self.onContactChoosed)
        self.editBar.registerEnterCB(self.onTextEntered)

        self.chat_wins=ChatList(self)

        QuickApp.__init__(self)  #XXX: yes it's an unusual place for the constructor of a parent class, but the init order is important

        signal (SIGWINCH, self.onResize) #we manage SIGWINCH ourselves, because the loop is not called otherwise

        #last but not least, we adapt windows' sizes
        self.sizer.update()
        self.editBar.replace_cur()
        curses.doupdate()

        self.loop.run()

    def addWin(self, win):
        self.listWins.append(win)

    def color(self, activate=True):
        if activate:
            debug ("Activation des couleurs")
            curses.init_pair(1, curses.COLOR_BLUE, curses.COLOR_BLACK)
            curses.init_pair(2, curses.COLOR_RED, curses.COLOR_BLACK)
        else:
            debug ("Desactivation des couleurs")
            curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK)
            curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLACK)
            

    def showChat(self, chat):
        debug ("show chat")
        if self.currentChat:
            debug ("hide de %s", self.currentChat)
            self.chat_wins[self.currentChat].hide()
        self.currentChat=chat
        debug ("show de %s", self.currentChat)
        self.chat_wins[self.currentChat].show()
        self.chat_wins[self.currentChat].update()
        

    ### EVENTS ###

    def onContactChoosed(self, jid_txt):
        """Called when a contact is selected in contact list."""
        jid=JID(jid_txt)
        debug ("contact choose: %s", jid)
        self.showChat(jid.short)
        self.statusBar.remove_item(jid.short)
        if len(self.statusBar)==0:
            self.statusBar.hide()
            self.sizer.update()


    def onTextEntered(self, text):
        jid=JID(self.whoami)
        self.bridge.sendMessage(self.currentChat, text)

    def showDialog(self, message, title, type="info"):
        if type==question:
            raise NotImplementedError
        pass


    def presenceUpdate(self, jabber_id, type, show, status, priority):
        QuickApp.presenceUpdate(self, jabber_id, type, show, status, priority)
        self.editBar.replace_cur()
        curses.doupdate()

    def newMessage(self, from_jid, msg, type, to_jid):
        QuickApp.newMessage(self, from_jid, msg, type, to_jid)
        sender=JID(from_jid)
        addr=JID(to_jid)
        win = addr if sender.short == self.whoami.short else sender  #FIXME: duplicate code with QuickApp
        if (self.currentChat==None):
            self.currentChat=win.short
            self.showChat(win.short)
            
        # we show the window in the status bar
        if not self.currentChat == win.short:
            self.statusBar.add_item(win.short)
            self.statusBar.show()
        self.sizer.update()
        self.statusBar.update()

        self.editBar.replace_cur()
        curses.doupdate()

    def onResize(self, sig, stack):
        """Called on SIGWINCH.
        resize the screen and launch the loop"""
        height, width = ttysize()
        curses.resizeterm(height, width)
        gobject.idle_add(self.callOnceLoop)
    
    def callOnceLoop(self):
        """Call the loop and return false (for not being called again by gobject mainloop).
        Usefull for calling loop when there is no input in stdin"""
        self.loopCB()
        return False

    def __key_handling(self, k):
        """Handle key and transmit to active window."""

        ### General keys, handled _everytime_ ###
        if k == C('x'):
            if os.getenv('TERM')=='screen':
                os.system('screen -X remove')
            else:
                self.loop.quit()

        ## windows navigation
        elif k == C('l') and not self.contactList.isHidden():
            """We go to the contact list"""
            self.contactList.activate(not self.contactList.isActive())
            if self.currentChat:
                self.editBar.activate(not self.contactList.isActive())

        elif k == curses.KEY_F2:
            self.contactList.hide(not self.contactList.isHidden())
            if self.contactList.isHidden():
                self.contactList.activate(False) #TODO: auto deactivation when hiding ?
                if self.currentChat:
                    self.editBar.activate(True)
            self.sizer.update()

        ## Chat Params ##    
        elif k == C('c'):
            self.chatParams["color"] = not self.chatParams["color"]
            self.color(self.chatParams["color"])
        elif k == C('t'):
            self.chatParams["timestamp"] = not self.chatParams["timestamp"]
            self.chat_wins[self.currentChat].update()
        elif k == C('s'):
            self.chatParams["short_nick"] = not self.chatParams["short_nick"]
            self.chat_wins[self.currentChat].update()

        ## misc ##
        elif k == curses.KEY_RESIZE:
            stdscr.erase()
            height, width = stdscr.getmaxyx()
            if height<5 and width<35:
                stdscr.addstr("Pleeeeasse, I can't even breathe !")
            else:
                for win in self.listWins:
                    win.resizeAdapt()
                for win in self.chat_wins.keys():
                    self.chat_wins[win].resizeAdapt()
                self.sizer.update() # FIXME: everything need to be managed by the sizer

        ## we now throw the key to win handlers ##
        else: 
            for win in self.listWins:
                if win.isActive():
                    win.handleKey(k)
            if self.currentChat:
                self.chat_wins[self.currentChat].handleKey(k)

    def loopCB(self, source="", cb_condition=""):
        """This callback is called by the main loop"""
        #pressed = self.contactList.window.getch()
        pressed = stdscr.getch()
        if pressed != curses.ERR:
            self.__key_handling(pressed)
            self.editBar.replace_cur()
            curses.doupdate()
            

        return True


sat = SortilegeApp()
sat.start()