Mercurial > libervia-backend
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()