Mercurial > libervia-backend
changeset 112:f551e44adb25
Primitivus first draft
- Sortilège is recoded using urwid, and renamed in Primitivus as it is no more based on curses
- soritlege code moved to sortilege_old, and deprecated
- Primitivus first draft, begining of ProfileManager widget
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 30 Jun 2010 14:24:24 +0800 |
parents | 6c927140ba82 |
children | e5ca22113280 |
files | frontends/primitivus/primitivus frontends/primitivus/profile_manager.py frontends/sortilege/__init__.py frontends/sortilege/boxsizer.py frontends/sortilege/chat.py frontends/sortilege/editbox.py frontends/sortilege/sortilege frontends/sortilege/statusbar.py frontends/sortilege/window.py frontends/sortilege_old/__init__.py frontends/sortilege_old/boxsizer.py frontends/sortilege_old/chat.py frontends/sortilege_old/editbox.py frontends/sortilege_old/sortilege frontends/sortilege_old/statusbar.py frontends/sortilege_old/window.py |
diffstat | 14 files changed, 1148 insertions(+), 1036 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/primitivus/primitivus Wed Jun 30 14:24:24 2010 +0800 @@ -0,0 +1,78 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +Primitivus: 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 <http://www.gnu.org/licenses/>. +""" + + +from quick_frontend.quick_app import QuickApp +from quick_frontend.quick_chat_list import QuickChatList +from quick_frontend.quick_contact_list import QuickContactList +from quick_frontend.quick_contact_management import QuickContactManagement +import urwid +from profile_manager import ProfileManager +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 +#from curses import ascii +#import locale +#from signal import signal, SIGWINCH +#import fcntl +#import struct +#import termios +#from boxsizer import BoxSizer + + +### logging configuration FIXME: put this elsewhere ### +logging.basicConfig(level=logging.CRITICAL, #TODO: configure it to put messages in a log file + format='%(message)s') +### + +const_APP_NAME = "Primitivus" +const_PALETTE = [('title', 'black', 'light gray', 'standout,underline'),] + +class PrimitivusApp(QuickApp): + + def __init__(self): + QuickApp.__init__(self) #XXX: yes it's an unusual place for the constructor of a parent class, but the init order is important + + ## main loop setup ## + self.main_widget = self.__buildMainWidget() + self.loop = urwid.MainLoop(self.main_widget, const_PALETTE, event_loop=urwid.GLibEventLoop(), unhandled_input=self.key_handler) + + def start(self): + self.loop.run() + + def key_handler(self, input): + if input in ('q', 'Q') or input == 'ctrl x': + raise urwid.ExitMainLoop() + + def __buildMainWidget(self): + main_widget = ProfileManager(self) + return main_widget + + +sat = PrimitivusApp() +sat.start()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/primitivus/profile_manager.py Wed Jun 30 14:24:24 2010 +0800 @@ -0,0 +1,34 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +Primitivus: 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 <http://www.gnu.org/licenses/>. +""" + +import urwid + + +class ProfileManager(urwid.WidgetWrap): + + def __init__(self, host): + self.host = host + profiles = self.host.bridge.getProfilesList() + widgets = [urwid.Text(str(profiles))] + content = urwid.SimpleListWalker(widgets) + display_widget = urwid.Frame(urwid.ListBox(content),urwid.AttrMap(urwid.Text("Profile Manager",align='center'),'title')) + urwid.WidgetWrap.__init__(self, display_widget) +
--- a/frontends/sortilege/boxsizer.py Tue Jun 29 16:07:51 2010 +0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,77 +0,0 @@ -#!/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 <http://www.gnu.org/licenses/>. -""" - - -from window import Window -import os,pdb - -class BoxSizer: - """This class manage the position of the window like boxes.""" - - - - def __init__(self, parent): - self.__parent=parent - self.boxes=[] - - - - def appendRow(self, win): - self.boxes.append([win]) - - def appendColum(self, index, win): - if len(self.boxes)<=index: - #TODO: throw an error here - return - self.boxes[index].append(win) - - def update(self): - """Resize boxes""" - oriY=0 - visible_row=[] - for row in self.boxes: - current_row=[] - oriX=0 - for win in row: - x=win.getOriX() - y=win.getOriY() - w=win.getOriWidth() - h=win.getOriHeight() - if win.isHidden(): - if len(current_row)>1 and win is row[-1]: - #if the last win is hidden, we expand previous visible one - current_row[-1][2] = current_row[-1][2] + (win.getX() - oriX)+win.getWidth() - else: - current_row.append([win, h+y-oriY, w+x-oriX, oriY, oriX]) - oriX=oriX+w - - if oriX!=0: - oriY=oriY+h - visible_row.append(current_row) - elif visible_row: - #if all the row is empty, we take the space - for box in visible_row[-1]: - box[1]=box[1]+h - oriY=oriY+h #this only happen if it's not the first visible row - - for row in visible_row: - for win in row: - win[0].resize(win[1], win[2], win[3], win[4])
--- a/frontends/sortilege/chat.py Tue Jun 29 16:07:51 2010 +0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,153 +0,0 @@ -#!/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 <http://www.gnu.org/licenses/>. -""" - - - -import os.path -import pdb -from logging import debug, info, error -from window import Window -import os -from time import time, localtime, strftime -import curses -import curses.ascii as ascii -from tools.jid import JID -from quick_frontend.quick_chat import QuickChat - - -def C(k): - """return the value of Ctrl+key""" - return ord(ascii.ctrl(k)) - -class Chat(Window, QuickChat): - - def __init__(self, to_id, host): - QuickChat.__init__(self, to_id, host) - self.__parent=host.stdscr - self.to_id=JID(to_id) - self.content=[] - self.__scollIdx=0 - Window.__init__(self, self.__parent, self.__parent.getmaxyx()[0]-2, self.__parent.getmaxyx()[1]-30, 0,30, code=host.code) - - #history - self.historyPrint(50, True, profile=self.host.profile) - - self.hide() - - def resize(self, height, width, y, x): - Window.resize(self, height, width, y, x) - self.update() - - def resizeAdapt(self): - """Adapt window size to self.__parent size. - Must be called when self.__parent is resized.""" - self.resize(self.__parent.getmaxyx()[0]-2, self.__parent.getmaxyx()[1]-30, 0,30) - self.update() - - def __getHeader(self, line): - """Return the header of a line (eg: "[12:34] <toto> ").""" - header='' - if self.host.chatParams["timestamp"]: - header = header + '[%s] ' % strftime("%H:%M", localtime(float(line[0]))) - if self.host.chatParams['short_nick']: - header = header + ('> ' if line[1]==self.host.profiles[self.host.profile]['whoami'] else '** ') - else: - header = header + '<%s> ' % line[1] - return header - - def update(self): - if self.isHidden(): - return - Window.update(self) - content=[] #what is really printed - irange=range(len(self.content)) - irange.reverse() #we print the text upward - for idx in irange: - header=self.__getHeader(self.content[idx]) - msg=self.content[idx][2] - part=0 #part of the text - if JID(self.content[idx][1]).short==self.host.profiles[self.host.profile]['whoami'].short: - att_header=curses.color_pair(1) - else: - att_header=curses.color_pair(2) - - while (msg): - if part==0: - hd=header - att=att_header - max=self.rWidth-len(header) - else: - hd="" - att=0 - max=self.rWidth - - LF = msg.find('\n') #we look for Line Feed - if LF != -1 and LF < max: - max = LF - next = max + 1 #we skip the LF - else: - next = max - - content.insert(part,[att,hd, msg[:max]]) - msg=msg[next:] #we erase treated part - part=part+1 - - if len(content)>=self.rHeight+self.__scollIdx: - #all the screen is filled, we can continue - break - - if self.__scollIdx>0 and len(content)<self.rHeight+self.__scollIdx: - self.__scollIdx=abs(len(content)-self.rHeight) #all the log fit on the screen, we must stop here - - idx=0 - for line in content[-self.rHeight-self.__scollIdx : -self.__scollIdx or None]: - self.addYXStr(idx, 0, line[1], line[0]) - self.addYXStr(idx, len(line[1]), line[2]) - idx=idx+1 - - self.noutrefresh() - - def scrollIdxUp(self): - """increment scroll index""" - self.__scollIdx = self.__scollIdx + 1 - self.update() - - def scrollIdxDown(self): - """decrement scroll index""" - if self.__scollIdx > 0: - self.__scollIdx = self.__scollIdx - 1 - self.update() - - def printMessage(self, jid, msg, profile, timestamp=""): - if timestamp=="": - current_time=time() - timestamp=str(current_time) - if self.last_history and current_time - float(self.last_history) < 5: #FIXME: Q&D fix to avoid double print on new chat window - return - self.content.append([timestamp,jid.short,msg]) - self.update() - - def handleKey(self, k): - if k == C('p'): - self.scrollIdxUp() - elif k == C('n'): - self.scrollIdxDown() -
--- a/frontends/sortilege/editbox.py Tue Jun 29 16:07:51 2010 +0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,176 +0,0 @@ -#!/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 <http://www.gnu.org/licenses/>. -""" - - -import curses -from curses import ascii -from window import Window - -def C(k): - """return the value of Ctrl+key""" - return ord(ascii.ctrl(k)) - -def A(k): - """return the value of Alt+key""" - return ord(ascii.alt(k)) - -class EditBox(Window): - """This class manage the edition of text""" - - def __init__(self, parent, header, code="utf-8"): - self.__header=header - self.__text = unicode() - self.__curs_pos=0 - self.__buffer=str() - self.__replace_mode=False - self.__parent=parent - self.__code=code - - Window.__init__(self, self.__parent, 1, self.__parent.getmaxyx()[1], self.__parent.getmaxyx()[0]-1,0, code=code) - self.update() - - def registerEnterCB(self, CB): - self.__enterCB=CB - - def resizeAdapt(self): - """Adapt window size to self.__parent size. - Must be called when self.__parent is resized.""" - self.resize(1, self.__parent.getmaxyx()[1], self.__parent.getmaxyx()[0]-1,0) - self.update() - - def __getTextToPrint(self): - """return the text printed on the edit line""" - width = self.rWidth - len(self.__header) -1 - if self.__curs_pos<width: - begin = 0 - end = width - else: - begin = self.__curs_pos-width - end = self.__curs_pos - return self.__header+self.__text[begin:end] - - def update(self): - Window.update(self) - text = self.__getTextToPrint() - self.addYXStr(0, 0, text, limit=self.rWidth) - - self.noutrefresh() - - def __dec_cur(self): - """move cursor on the left""" - if self.__curs_pos>0: - self.__curs_pos = self.__curs_pos - 1 - - def __inc_cur(self): - """move cursor on the right""" - if self.__curs_pos<len(self.__text): - self.__curs_pos = self.__curs_pos + 1 - - def move_cur(self, x): - pos = x+len(self.__header) - if pos>=self.rWidth: - pos=self.rWidth-1 - self.move(0, pos) - - def clear_text(self): - """Clear the text of the edit box""" - self.__text="" - self.__curs_pos=0 - - def replace_cur(self): - """must be called earch time the cursor is moved""" - self.move_cur(self.__curs_pos) - self.noutrefresh() - - def activate(self, state=True): - cursor_mode = 1 if state else 0 - curses.curs_set(cursor_mode) - Window.activate(self,state) - - def handleKey(self, k): - if ascii.isgraph(k) or ascii.isblank(k): - pacman = 0 if not self.__replace_mode else 1 - self.__text = self.__text[:self.__curs_pos] + chr(k) + self.__text[self.__curs_pos + pacman:] - self.__curs_pos = self.__curs_pos + 1 - - elif k==ascii.NL: - try: - self.__enterCB(self.__text) - except NameError: - pass # TODO: thrown an error here - self.clear_text() - - elif k==curses.KEY_BACKSPACE: - self.__text = self.__text[:self.__curs_pos-1]+self.__text[self.__curs_pos:] - self.__dec_cur() - - elif k==curses.KEY_DC: - self.__text = self.__text[:self.__curs_pos]+self.__text[self.__curs_pos+1:] - - elif k==curses.KEY_IC: - self.__replace_mode = not self.__replace_mode - - elif k==curses.KEY_LEFT: - self.__dec_cur() - - elif k==curses.KEY_RIGHT: - self.__inc_cur() - - elif k==curses.KEY_HOME or k==C('a'): - self.__curs_pos=0 - - elif k==curses.KEY_END or k==C('e'): - self.__curs_pos=len(self.__text) - - elif k==C('k'): - self.__text = self.__text[:self.__curs_pos] - - elif k==C('w'): - before = self.__text[:self.__curs_pos] - pos = before.rstrip().rfind(" ")+1 - self.__text = before[:pos] + self.__text[self.__curs_pos:] - self.__curs_pos = pos - - elif k>255: - self.__buffer="" - - else: ## FIXME: dangerous code, must be checked ! (specialy buffer overflow) ## - #We now manage unicode - self.__buffer = self.__buffer+chr(k) - decoded=unicode() - if len(self.__buffer)>4: - self.__buffer="" - return - try: - decoded = self.__buffer.decode(self.__code) - except UnicodeDecodeError, e: - if e.reason!="unexpected end of data": - self.__buffer="" - return - if len(self.__buffer)==1: ## FIXME: awful ! only for test ! - self.__buffer="" - return - self.__text = self.__text + decoded - self.__curs_pos = self.__curs_pos + 1 - self.__buffer="" - - self.update() -
--- a/frontends/sortilege/sortilege Tue Jun 29 16:07:51 2010 +0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,400 +0,0 @@ -#!/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 <http://www.gnu.org/licenses/>. -""" - - -from quick_frontend.quick_app import QuickApp -from quick_frontend.quick_chat_list import QuickChatList -from quick_frontend.quick_contact_list import QuickContactList -from quick_frontend.quick_contact_management import QuickContactManagement -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 - - -### 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, groups=None): - """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.rHeight else self.__index-self.rHeight+1 - idx=0 - for item in self.jid_list[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_list: - return - try: - self.__enterCB(self.jid_list[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.CM = QuickContactManagement() - 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, self.CM) - 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 - 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 (_("Activating colors")) - curses.init_pair(1, curses.COLOR_BLUE, curses.COLOR_BLACK) - curses.init_pair(2, curses.COLOR_RED, curses.COLOR_BLACK) - else: - debug (_("Deactivating colors")) - 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()
--- a/frontends/sortilege/statusbar.py Tue Jun 29 16:07:51 2010 +0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,71 +0,0 @@ -#!/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 <http://www.gnu.org/licenses/>. -""" - - -import curses -from window import Window -import os - -class StatusBar(Window): - """This class manage the edition of text""" - - def __init__(self, parent, code="utf-8"): - self.__parent=parent - self.__code=code - self.__items=set() - - Window.__init__(self, self.__parent, 1, self.__parent.getmaxyx()[1], self.__parent.getmaxyx()[0]-2,0, code=code) - - def __len__(self): - return len(self.__items) - - def resizeAdapt(self): - """Adapt window size to self.__parent size. - Must be called when self.__parent is resized.""" - self.resize(1, self.__parent.getmaxyx()[1], self.__parent.getmaxyx()[0]-2,0) - self.update() - - def update(self): - if self.isHidden(): - return - Window.update(self) - x=0 - for item in self.__items: - pitem="[%s] " % item - self.addYXStr(0, x, pitem, curses.A_REVERSE) - x = x + len(pitem) - if x>=self.rWidth: - break - self.addYXStr(0, x, (self.rWidth-x)*" ", curses.A_REVERSE) - self.noutrefresh() - - def clear_text(self): - """Clear the text of the edit box""" - del(self.__items[:]) - - def add_item(self, item): - self.__items.add(item) - self.update() - - def remove_item(self, item): - if item in self.__items: - self.__items.remove(item) - self.update()
--- a/frontends/sortilege/window.py Tue Jun 29 16:07:51 2010 +0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,159 +0,0 @@ -#!/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 <http://www.gnu.org/licenses/>. -""" - - -import curses -import os -import pdb - - -class Window(): - def __init__(self, parent, height, width, y, x, border=False, title="", code="utf-8"): - self.__border=border - self.__title=title - self.__active=False - self.__parent=parent - self.__code=code - self.__hide=False - - self.resize(height, width, y, x) - self.oriCoords=self.__coords #FIXME: tres moche, a faire en mieux - - def hide(self, hide=True): - self.__hide=hide - - def show(self): - self.__hide=False - - def isHidden(self): - return self.__hide - - def getY(self): - return self.__coords[2] - - def getX(self): - return self.__coords[3] - - def getHeight(self): - return self.__coords[0] - - def getWidth(self): - return self.__coords[1] - - - #FIXME: tres moche, a faire en plus joli - def getOriY(self): - return self.oriCoords[2] - - def getOriX(self): - return self.oriCoords[3] - - def getOriHeight(self): - return self.oriCoords[0] - - def getOriWidth(self): - return self.oriCoords[1] - - def defInsideCoord(self): - """define the inside coordinates (win coordinates minus decorations)""" - height,width,y,x=self.__coords - self.oriX = x if not self.__border else x+1 - self.oriY = y if not self.__border else y+1 - self.endX = x+width if not self.__border else x+width-2 - self.endY = y+height if not self.__border else y+height-2 - self.rWidth = width if not self.__border else width-2 - self.rHeight = height if not self.__border else height-2 - - def resize(self, height, width, y, x): - self.__coords=[height, width, y, x] - - # we check that coordinates are under limits - self.__coordAdjust(self.__coords) - height,width,y,x=self.__coords - - self.window = self.__parent.subwin(height, width, y, x) - self.defInsideCoord() - - def __coordAdjust(self, coords): - """Check that coordinates are under limits, adjust them else otherwise""" - height,width,y,x=coords - parentY, parentX = self.__parent.getbegyx() - parentHeight, parentWidth = self.__parent.getmaxyx() - - if y < parentY: - y = parentY - if x < parentX: - x = parentX - if height > parentHeight - y: - height = parentHeight - y - if width > parentWidth - x: - width = parentWidth - x - coords[0], coords[1], coords[2], coords[3] = [height, width, y, x] - - - def activate(self,state=True): - """Declare this window as current active one""" - self.__active=state - self.update() - - def isActive(self): - return self.__active - - def addYXStr(self, y, x, text, attr = 0, limit=0): - if self.__border: - x=x+1 - y=y+1 - n = self.rWidth-x if not limit else limit - encoded = text.encode(self.__code) - adjust = len(encoded) - len(text) # hack because addnstr doesn't manage unicode - try: - self.window.addnstr(y, x, encoded, n + adjust, attr) - except: - #We have to catch error to write on last line last col FIXME: is there a better way ? - pass - - def move(self, y, x): - self.window.move(y,x) - - def noutrefresh(self): - self.window.noutrefresh() - - def update(self): - """redraw all the window""" - if self.__hide: - return - self.clear() - - def border(self): - """redraw the border and title""" - y,x = self.window.getbegyx() - width = self.window.getmaxyx()[1] - if self.__border: - self.window.border() - if self.__title: - if len(self.__title)>width: - self.__title="" - else: - self.window.addstr(y,x+(width-len(self.__title))/2, self.__title) - - def clear(self): - self.window.clear() - self.border()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/sortilege_old/boxsizer.py Wed Jun 30 14:24:24 2010 +0800 @@ -0,0 +1,77 @@ +#!/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 <http://www.gnu.org/licenses/>. +""" + + +from window import Window +import os,pdb + +class BoxSizer: + """This class manage the position of the window like boxes.""" + + + + def __init__(self, parent): + self.__parent=parent + self.boxes=[] + + + + def appendRow(self, win): + self.boxes.append([win]) + + def appendColum(self, index, win): + if len(self.boxes)<=index: + #TODO: throw an error here + return + self.boxes[index].append(win) + + def update(self): + """Resize boxes""" + oriY=0 + visible_row=[] + for row in self.boxes: + current_row=[] + oriX=0 + for win in row: + x=win.getOriX() + y=win.getOriY() + w=win.getOriWidth() + h=win.getOriHeight() + if win.isHidden(): + if len(current_row)>1 and win is row[-1]: + #if the last win is hidden, we expand previous visible one + current_row[-1][2] = current_row[-1][2] + (win.getX() - oriX)+win.getWidth() + else: + current_row.append([win, h+y-oriY, w+x-oriX, oriY, oriX]) + oriX=oriX+w + + if oriX!=0: + oriY=oriY+h + visible_row.append(current_row) + elif visible_row: + #if all the row is empty, we take the space + for box in visible_row[-1]: + box[1]=box[1]+h + oriY=oriY+h #this only happen if it's not the first visible row + + for row in visible_row: + for win in row: + win[0].resize(win[1], win[2], win[3], win[4])
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/sortilege_old/chat.py Wed Jun 30 14:24:24 2010 +0800 @@ -0,0 +1,153 @@ +#!/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 <http://www.gnu.org/licenses/>. +""" + + + +import os.path +import pdb +from logging import debug, info, error +from window import Window +import os +from time import time, localtime, strftime +import curses +import curses.ascii as ascii +from tools.jid import JID +from quick_frontend.quick_chat import QuickChat + + +def C(k): + """return the value of Ctrl+key""" + return ord(ascii.ctrl(k)) + +class Chat(Window, QuickChat): + + def __init__(self, to_id, host): + QuickChat.__init__(self, to_id, host) + self.__parent=host.stdscr + self.to_id=JID(to_id) + self.content=[] + self.__scollIdx=0 + Window.__init__(self, self.__parent, self.__parent.getmaxyx()[0]-2, self.__parent.getmaxyx()[1]-30, 0,30, code=host.code) + + #history + self.historyPrint(50, True, profile=self.host.profile) + + self.hide() + + def resize(self, height, width, y, x): + Window.resize(self, height, width, y, x) + self.update() + + def resizeAdapt(self): + """Adapt window size to self.__parent size. + Must be called when self.__parent is resized.""" + self.resize(self.__parent.getmaxyx()[0]-2, self.__parent.getmaxyx()[1]-30, 0,30) + self.update() + + def __getHeader(self, line): + """Return the header of a line (eg: "[12:34] <toto> ").""" + header='' + if self.host.chatParams["timestamp"]: + header = header + '[%s] ' % strftime("%H:%M", localtime(float(line[0]))) + if self.host.chatParams['short_nick']: + header = header + ('> ' if line[1]==self.host.profiles[self.host.profile]['whoami'] else '** ') + else: + header = header + '<%s> ' % line[1] + return header + + def update(self): + if self.isHidden(): + return + Window.update(self) + content=[] #what is really printed + irange=range(len(self.content)) + irange.reverse() #we print the text upward + for idx in irange: + header=self.__getHeader(self.content[idx]) + msg=self.content[idx][2] + part=0 #part of the text + if JID(self.content[idx][1]).short==self.host.profiles[self.host.profile]['whoami'].short: + att_header=curses.color_pair(1) + else: + att_header=curses.color_pair(2) + + while (msg): + if part==0: + hd=header + att=att_header + max=self.rWidth-len(header) + else: + hd="" + att=0 + max=self.rWidth + + LF = msg.find('\n') #we look for Line Feed + if LF != -1 and LF < max: + max = LF + next = max + 1 #we skip the LF + else: + next = max + + content.insert(part,[att,hd, msg[:max]]) + msg=msg[next:] #we erase treated part + part=part+1 + + if len(content)>=self.rHeight+self.__scollIdx: + #all the screen is filled, we can continue + break + + if self.__scollIdx>0 and len(content)<self.rHeight+self.__scollIdx: + self.__scollIdx=abs(len(content)-self.rHeight) #all the log fit on the screen, we must stop here + + idx=0 + for line in content[-self.rHeight-self.__scollIdx : -self.__scollIdx or None]: + self.addYXStr(idx, 0, line[1], line[0]) + self.addYXStr(idx, len(line[1]), line[2]) + idx=idx+1 + + self.noutrefresh() + + def scrollIdxUp(self): + """increment scroll index""" + self.__scollIdx = self.__scollIdx + 1 + self.update() + + def scrollIdxDown(self): + """decrement scroll index""" + if self.__scollIdx > 0: + self.__scollIdx = self.__scollIdx - 1 + self.update() + + def printMessage(self, jid, msg, profile, timestamp=""): + if timestamp=="": + current_time=time() + timestamp=str(current_time) + if self.last_history and current_time - float(self.last_history) < 5: #FIXME: Q&D fix to avoid double print on new chat window + return + self.content.append([timestamp,jid.short,msg]) + self.update() + + def handleKey(self, k): + if k == C('p'): + self.scrollIdxUp() + elif k == C('n'): + self.scrollIdxDown() +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/sortilege_old/editbox.py Wed Jun 30 14:24:24 2010 +0800 @@ -0,0 +1,176 @@ +#!/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 <http://www.gnu.org/licenses/>. +""" + + +import curses +from curses import ascii +from window import Window + +def C(k): + """return the value of Ctrl+key""" + return ord(ascii.ctrl(k)) + +def A(k): + """return the value of Alt+key""" + return ord(ascii.alt(k)) + +class EditBox(Window): + """This class manage the edition of text""" + + def __init__(self, parent, header, code="utf-8"): + self.__header=header + self.__text = unicode() + self.__curs_pos=0 + self.__buffer=str() + self.__replace_mode=False + self.__parent=parent + self.__code=code + + Window.__init__(self, self.__parent, 1, self.__parent.getmaxyx()[1], self.__parent.getmaxyx()[0]-1,0, code=code) + self.update() + + def registerEnterCB(self, CB): + self.__enterCB=CB + + def resizeAdapt(self): + """Adapt window size to self.__parent size. + Must be called when self.__parent is resized.""" + self.resize(1, self.__parent.getmaxyx()[1], self.__parent.getmaxyx()[0]-1,0) + self.update() + + def __getTextToPrint(self): + """return the text printed on the edit line""" + width = self.rWidth - len(self.__header) -1 + if self.__curs_pos<width: + begin = 0 + end = width + else: + begin = self.__curs_pos-width + end = self.__curs_pos + return self.__header+self.__text[begin:end] + + def update(self): + Window.update(self) + text = self.__getTextToPrint() + self.addYXStr(0, 0, text, limit=self.rWidth) + + self.noutrefresh() + + def __dec_cur(self): + """move cursor on the left""" + if self.__curs_pos>0: + self.__curs_pos = self.__curs_pos - 1 + + def __inc_cur(self): + """move cursor on the right""" + if self.__curs_pos<len(self.__text): + self.__curs_pos = self.__curs_pos + 1 + + def move_cur(self, x): + pos = x+len(self.__header) + if pos>=self.rWidth: + pos=self.rWidth-1 + self.move(0, pos) + + def clear_text(self): + """Clear the text of the edit box""" + self.__text="" + self.__curs_pos=0 + + def replace_cur(self): + """must be called earch time the cursor is moved""" + self.move_cur(self.__curs_pos) + self.noutrefresh() + + def activate(self, state=True): + cursor_mode = 1 if state else 0 + curses.curs_set(cursor_mode) + Window.activate(self,state) + + def handleKey(self, k): + if ascii.isgraph(k) or ascii.isblank(k): + pacman = 0 if not self.__replace_mode else 1 + self.__text = self.__text[:self.__curs_pos] + chr(k) + self.__text[self.__curs_pos + pacman:] + self.__curs_pos = self.__curs_pos + 1 + + elif k==ascii.NL: + try: + self.__enterCB(self.__text) + except NameError: + pass # TODO: thrown an error here + self.clear_text() + + elif k==curses.KEY_BACKSPACE: + self.__text = self.__text[:self.__curs_pos-1]+self.__text[self.__curs_pos:] + self.__dec_cur() + + elif k==curses.KEY_DC: + self.__text = self.__text[:self.__curs_pos]+self.__text[self.__curs_pos+1:] + + elif k==curses.KEY_IC: + self.__replace_mode = not self.__replace_mode + + elif k==curses.KEY_LEFT: + self.__dec_cur() + + elif k==curses.KEY_RIGHT: + self.__inc_cur() + + elif k==curses.KEY_HOME or k==C('a'): + self.__curs_pos=0 + + elif k==curses.KEY_END or k==C('e'): + self.__curs_pos=len(self.__text) + + elif k==C('k'): + self.__text = self.__text[:self.__curs_pos] + + elif k==C('w'): + before = self.__text[:self.__curs_pos] + pos = before.rstrip().rfind(" ")+1 + self.__text = before[:pos] + self.__text[self.__curs_pos:] + self.__curs_pos = pos + + elif k>255: + self.__buffer="" + + else: ## FIXME: dangerous code, must be checked ! (specialy buffer overflow) ## + #We now manage unicode + self.__buffer = self.__buffer+chr(k) + decoded=unicode() + if len(self.__buffer)>4: + self.__buffer="" + return + try: + decoded = self.__buffer.decode(self.__code) + except UnicodeDecodeError, e: + if e.reason!="unexpected end of data": + self.__buffer="" + return + if len(self.__buffer)==1: ## FIXME: awful ! only for test ! + self.__buffer="" + return + self.__text = self.__text + decoded + self.__curs_pos = self.__curs_pos + 1 + self.__buffer="" + + self.update() +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/sortilege_old/sortilege Wed Jun 30 14:24:24 2010 +0800 @@ -0,0 +1,400 @@ +#!/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 <http://www.gnu.org/licenses/>. +""" + + +from quick_frontend.quick_app import QuickApp +from quick_frontend.quick_chat_list import QuickChatList +from quick_frontend.quick_contact_list import QuickContactList +from quick_frontend.quick_contact_management import QuickContactManagement +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 + + +### 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, groups=None): + """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.rHeight else self.__index-self.rHeight+1 + idx=0 + for item in self.jid_list[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_list: + return + try: + self.__enterCB(self.jid_list[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.CM = QuickContactManagement() + 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, self.CM) + 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 + 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 (_("Activating colors")) + curses.init_pair(1, curses.COLOR_BLUE, curses.COLOR_BLACK) + curses.init_pair(2, curses.COLOR_RED, curses.COLOR_BLACK) + else: + debug (_("Deactivating colors")) + 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()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/sortilege_old/statusbar.py Wed Jun 30 14:24:24 2010 +0800 @@ -0,0 +1,71 @@ +#!/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 <http://www.gnu.org/licenses/>. +""" + + +import curses +from window import Window +import os + +class StatusBar(Window): + """This class manage the edition of text""" + + def __init__(self, parent, code="utf-8"): + self.__parent=parent + self.__code=code + self.__items=set() + + Window.__init__(self, self.__parent, 1, self.__parent.getmaxyx()[1], self.__parent.getmaxyx()[0]-2,0, code=code) + + def __len__(self): + return len(self.__items) + + def resizeAdapt(self): + """Adapt window size to self.__parent size. + Must be called when self.__parent is resized.""" + self.resize(1, self.__parent.getmaxyx()[1], self.__parent.getmaxyx()[0]-2,0) + self.update() + + def update(self): + if self.isHidden(): + return + Window.update(self) + x=0 + for item in self.__items: + pitem="[%s] " % item + self.addYXStr(0, x, pitem, curses.A_REVERSE) + x = x + len(pitem) + if x>=self.rWidth: + break + self.addYXStr(0, x, (self.rWidth-x)*" ", curses.A_REVERSE) + self.noutrefresh() + + def clear_text(self): + """Clear the text of the edit box""" + del(self.__items[:]) + + def add_item(self, item): + self.__items.add(item) + self.update() + + def remove_item(self, item): + if item in self.__items: + self.__items.remove(item) + self.update()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/sortilege_old/window.py Wed Jun 30 14:24:24 2010 +0800 @@ -0,0 +1,159 @@ +#!/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 <http://www.gnu.org/licenses/>. +""" + + +import curses +import os +import pdb + + +class Window(): + def __init__(self, parent, height, width, y, x, border=False, title="", code="utf-8"): + self.__border=border + self.__title=title + self.__active=False + self.__parent=parent + self.__code=code + self.__hide=False + + self.resize(height, width, y, x) + self.oriCoords=self.__coords #FIXME: tres moche, a faire en mieux + + def hide(self, hide=True): + self.__hide=hide + + def show(self): + self.__hide=False + + def isHidden(self): + return self.__hide + + def getY(self): + return self.__coords[2] + + def getX(self): + return self.__coords[3] + + def getHeight(self): + return self.__coords[0] + + def getWidth(self): + return self.__coords[1] + + + #FIXME: tres moche, a faire en plus joli + def getOriY(self): + return self.oriCoords[2] + + def getOriX(self): + return self.oriCoords[3] + + def getOriHeight(self): + return self.oriCoords[0] + + def getOriWidth(self): + return self.oriCoords[1] + + def defInsideCoord(self): + """define the inside coordinates (win coordinates minus decorations)""" + height,width,y,x=self.__coords + self.oriX = x if not self.__border else x+1 + self.oriY = y if not self.__border else y+1 + self.endX = x+width if not self.__border else x+width-2 + self.endY = y+height if not self.__border else y+height-2 + self.rWidth = width if not self.__border else width-2 + self.rHeight = height if not self.__border else height-2 + + def resize(self, height, width, y, x): + self.__coords=[height, width, y, x] + + # we check that coordinates are under limits + self.__coordAdjust(self.__coords) + height,width,y,x=self.__coords + + self.window = self.__parent.subwin(height, width, y, x) + self.defInsideCoord() + + def __coordAdjust(self, coords): + """Check that coordinates are under limits, adjust them else otherwise""" + height,width,y,x=coords + parentY, parentX = self.__parent.getbegyx() + parentHeight, parentWidth = self.__parent.getmaxyx() + + if y < parentY: + y = parentY + if x < parentX: + x = parentX + if height > parentHeight - y: + height = parentHeight - y + if width > parentWidth - x: + width = parentWidth - x + coords[0], coords[1], coords[2], coords[3] = [height, width, y, x] + + + def activate(self,state=True): + """Declare this window as current active one""" + self.__active=state + self.update() + + def isActive(self): + return self.__active + + def addYXStr(self, y, x, text, attr = 0, limit=0): + if self.__border: + x=x+1 + y=y+1 + n = self.rWidth-x if not limit else limit + encoded = text.encode(self.__code) + adjust = len(encoded) - len(text) # hack because addnstr doesn't manage unicode + try: + self.window.addnstr(y, x, encoded, n + adjust, attr) + except: + #We have to catch error to write on last line last col FIXME: is there a better way ? + pass + + def move(self, y, x): + self.window.move(y,x) + + def noutrefresh(self): + self.window.noutrefresh() + + def update(self): + """redraw all the window""" + if self.__hide: + return + self.clear() + + def border(self): + """redraw the border and title""" + y,x = self.window.getbegyx() + width = self.window.getmaxyx()[1] + if self.__border: + self.window.border() + if self.__title: + if len(self.__title)>width: + self.__title="" + else: + self.window.addstr(y,x+(width-len(self.__title))/2, self.__title) + + def clear(self): + self.window.clear() + self.border()