diff 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 diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/frontends/sortilege/sortilege.py	Sat Aug 29 13:34:59 2009 +0200
@@ -0,0 +1,379 @@
+#!/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()