#!/usr/bin/python # -*- coding: utf-8 -*- """ sortilege: a SAT frontend Copyright (C) 2009, 2010 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 . """ 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 from quick_frontend.quick_contact_management import QuickContactManagement ### 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, host, CM): QuickContactList.__init__(self, CM) self.host = host self.jid_list = [] 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 clear_contacts(self): """clear all the contact list""" del self.jid_list[:] self.__index = 0 self.update() #FIXME: window is not updated correctly (contacts are still here until C-L) def replace(self, jid): """add a contact to the list""" name = self.CM.getAttr(jid,'name') self.jid_list.append(jid.short) self.update() def indexUp(self): """increment select contact index""" if self.__index < len(self.jid_list)-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 disconnect(self, jid): """for now, we just remove the contact""" self.remove(jid) def remove(self, jid): """remove a contact from the list""" self.jid_list.remove(jid.short) if self.__index >= len(self.jid_list) and self.__index > 0: #if select index is out of border, we put it on the last contact self.__index = len(self.jid_list)-1 self.update() def update(self): """redraw all the window""" if self.isHidden(): return Window.update(self) self.jid_list.sort() begin=0 if self.__index ", 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 self.plug_profile() 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 ("hiding %s", self.currentChat) self.chat_wins[self.currentChat].hide() self.currentChat=chat debug ("showing %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 choosed: %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.profiles[self.profile]['whoami']) self.bridge.sendMessage(self.currentChat, text, profile_key=self.profile) def showDialog(self, message, title, type="info"): if type==question: raise NotImplementedError pass def presenceUpdate(self, jabber_id, show, priority, statuses, profile): QuickApp.presenceUpdate(self, jabber_id, show, priority, statuses, profile) self.editBar.replace_cur() curses.doupdate() def askConfirmation(self, type, id, data): #FIXME info ("FIXME: askConfirmation not implemented") def actionResult(self, type, id, data): #FIXME info ("FIXME: actionResult not implemented") def newMessage(self, from_jid, msg, type, to_jid, profile): QuickApp.newMessage(self, from_jid, msg, type, to_jid, profile) 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()