comparison frontends/sortilege/sortilege @ 32:c4badbf3dd97

wix.py renamed in wix and sortilege.py renamed in sortilege
author Goffi <goffi@goffi.org>
date Tue, 08 Dec 2009 08:13:12 +0100
parents frontends/sortilege/sortilege.py@bb72c29f3432
children 8c67ea98ab91
comparison
equal deleted inserted replaced
31:7b34ffa2ff45 32:c4badbf3dd97
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 """
5 sortilege: a SAT frontend
6 Copyright (C) 2009 Jérôme Poisson (goffi@goffi.org)
7
8 This program is free software: you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation, either version 3 of the License, or
11 (at your option) any later version.
12
13 This program is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
17
18 You should have received a copy of the GNU General Public License
19 along with this program. If not, see <http://www.gnu.org/licenses/>.
20 """
21
22
23 import curses
24 import pdb
25 from window import Window
26 from editbox import EditBox
27 from statusbar import StatusBar
28 from chat import Chat
29 from tools.jid import JID
30 import logging
31 from logging import debug, info, error
32 import locale
33 import sys, os
34 import gobject
35 import time
36 from curses import ascii
37 import locale
38 from signal import signal, SIGWINCH
39 import fcntl
40 import struct
41 import termios
42 from boxsizer import BoxSizer
43 from quick_frontend.quick_chat_list import QuickChatList
44 from quick_frontend.quick_contact_list import QuickContactList
45 from quick_frontend.quick_app import QuickApp
46
47 ### logging configuration FIXME: put this elsewhere ###
48 logging.basicConfig(level=logging.CRITICAL, #TODO: configure it top put messages in a log file
49 format='%(message)s')
50 ###
51
52 const_APP_NAME = "Sortilège"
53 const_CONTACT_WIDTH = 30
54
55 def ttysize():
56 """This function return term size.
57 Comes from Donn Cave from python list mailing list"""
58 buf = 'abcdefgh'
59 buf = fcntl.ioctl(0, termios.TIOCGWINSZ, buf)
60 row, col, rpx, cpx = struct.unpack('hhhh', buf)
61 return row, col
62
63 def C(k):
64 """return the value of Ctrl+key"""
65 return ord(ascii.ctrl(k))
66
67 class ChatList(QuickChatList):
68 """This class manage the list of chat windows"""
69
70 def __init__(self, host):
71 QuickChatList.__init__(self, host)
72 self.sizer=host.sizer
73
74 def createChat(self, name):
75 chat = Chat(name, self.host)
76 self.sizer.appendColum(0,chat)
77 self.sizer.update()
78 return chat
79
80
81 class ContactList(Window, QuickContactList):
82
83 def __init__(self):
84 QuickContactList.__init__(self)
85 self.__index=0 #indicate which contact is selected (ie: where we are)
86 Window.__init__(self, stdscr, stdscr.getmaxyx()[0]-2,const_CONTACT_WIDTH,0,0, True, "Contact List", code=code)
87
88 def resize(self, height, width, y, x):
89 Window.resize(self, height, width, y, x)
90 self.update()
91
92 def resizeAdapt(self):
93 """Adapt window size to stdscr size.
94 Must be called when stdscr is resized."""
95 self.resize(stdscr.getmaxyx()[0]-2,const_CONTACT_WIDTH,0,0)
96 self.update()
97
98 def registerEnterCB(self, CB):
99 self.__enterCB=CB
100
101 def replace(self, jid, name="", show="", status="", group=""):
102 """add a contact to the list"""
103 self.jid_ids[jid] = name or jid
104 self.update()
105
106 def indexUp(self):
107 """increment select contact index"""
108 if self.__index < len(self.jid_ids)-1: #we dont want to select a missing contact
109 self.__index = self.__index + 1
110 self.update()
111
112 def indexDown(self):
113 """decrement select contact index"""
114 if self.__index > 0:
115 self.__index = self.__index - 1
116 self.update()
117
118 def remove(self, jid):
119 """remove a contact from the list"""
120 del self.jid_ids[jid]
121 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
122 self.__index = len(self.jid_ids)-1
123 self.update()
124
125 def update(self):
126 """redraw all the window"""
127 if self.isHidden():
128 return
129 Window.update(self)
130 viewList=[]
131 for jid in self.jid_ids:
132 viewList.append(self.jid_ids[jid])
133 viewList.sort()
134 begin=0 if self.__index<self.rHeight else self.__index-self.rHeight+1
135 idx=0
136 for item in viewList[begin:self.rHeight+begin]:
137 attr = curses.A_REVERSE if ( self.isActive() and (idx+begin) == self.__index ) else 0
138 centered = item.center(self.rWidth) ## it's nicer in the center :)
139 self.addYXStr(idx, 0, centered, attr)
140 idx = idx + 1
141
142 self.noutrefresh()
143
144 def handleKey(self, k):
145 if k == curses.KEY_UP:
146 self.indexDown()
147 elif k == curses.KEY_DOWN:
148 self.indexUp()
149 elif k == ascii.NL:
150 if not self.jid_ids:
151 return
152 try:
153 self.__enterCB(self.jid_ids.keys()[self.__index])
154 except NameError:
155 pass # TODO: thrown an error here
156
157 class SortilegeApp(QuickApp):
158
159 def __init__(self):
160 #debug(const_APP_NAME+" init...")
161
162 ## unicode support ##
163 locale.setlocale(locale.LC_ALL, '')
164 global code
165 code = locale.getpreferredencoding()
166 self.code=code
167
168 ## main loop setup ##
169 self.loop=gobject.MainLoop()
170 gobject.io_add_watch(0, gobject.IO_IN, self.loopCB)
171
172 ## misc init stuff ##
173 self.listWins=[]
174 self.chatParams={'timestamp':True,
175 'color':True,
176 'short_nick':False}
177
178 def start(self):
179 curses.wrapper(self.start_curses)
180
181 def start_curses(self, win):
182 global stdscr
183 stdscr = win
184 self.stdscr = stdscr
185 curses.raw() #we handle everything ourself
186 curses.curs_set(False)
187 stdscr.nodelay(True)
188
189 ## colours ##
190 self.color(True)
191
192 ## windows ##
193 self.contactList = ContactList()
194 self.editBar = EditBox(stdscr, "> ", self.code)
195 self.editBar.activate(False)
196 self.statusBar = StatusBar(stdscr, self.code)
197 self.statusBar.hide(True)
198 self.addWin(self.contactList)
199 self.addWin(self.editBar)
200 self.addWin(self.statusBar)
201 self.sizer=BoxSizer(stdscr)
202 self.sizer.appendRow(self.contactList)
203 self.sizer.appendRow(self.statusBar)
204 self.sizer.appendRow(self.editBar)
205 self.currentChat=None
206
207 self.contactList.registerEnterCB(self.onContactChoosed)
208 self.editBar.registerEnterCB(self.onTextEntered)
209
210 self.chat_wins=ChatList(self)
211
212 QuickApp.__init__(self) #XXX: yes it's an unusual place for the constructor of a parent class, but the init order is important
213
214 signal (SIGWINCH, self.onResize) #we manage SIGWINCH ourselves, because the loop is not called otherwise
215
216 #last but not least, we adapt windows' sizes
217 self.sizer.update()
218 self.editBar.replace_cur()
219 curses.doupdate()
220
221 self.loop.run()
222
223 def addWin(self, win):
224 self.listWins.append(win)
225
226 def color(self, activate=True):
227 if activate:
228 debug ("Activation des couleurs")
229 curses.init_pair(1, curses.COLOR_BLUE, curses.COLOR_BLACK)
230 curses.init_pair(2, curses.COLOR_RED, curses.COLOR_BLACK)
231 else:
232 debug ("Desactivation des couleurs")
233 curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK)
234 curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLACK)
235
236
237 def showChat(self, chat):
238 debug ("show chat")
239 if self.currentChat:
240 debug ("hide de %s", self.currentChat)
241 self.chat_wins[self.currentChat].hide()
242 self.currentChat=chat
243 debug ("show de %s", self.currentChat)
244 self.chat_wins[self.currentChat].show()
245 self.chat_wins[self.currentChat].update()
246
247
248 ### EVENTS ###
249
250 def onContactChoosed(self, jid_txt):
251 """Called when a contact is selected in contact list."""
252 jid=JID(jid_txt)
253 debug ("contact choose: %s", jid)
254 self.showChat(jid.short)
255 self.statusBar.remove_item(jid.short)
256 if len(self.statusBar)==0:
257 self.statusBar.hide()
258 self.sizer.update()
259
260
261 def onTextEntered(self, text):
262 jid=JID(self.whoami)
263 self.bridge.sendMessage(self.currentChat, text)
264
265 def showDialog(self, message, title, type="info"):
266 if type==question:
267 raise NotImplementedError
268 pass
269
270
271 def presenceUpdate(self, jabber_id, type, show, status, priority):
272 QuickApp.presenceUpdate(self, jabber_id, type, show, status, priority)
273 self.editBar.replace_cur()
274 curses.doupdate()
275
276 def askConfirmation(self, type, id, data):
277 #FIXME
278 info ("FIXME: askConfirmation not implemented")
279
280 def actionResult(self, type, id, data):
281 #FIXME
282 info ("FIXME: actionResult not implemented")
283
284 def newMessage(self, from_jid, msg, type, to_jid):
285 QuickApp.newMessage(self, from_jid, msg, type, to_jid)
286 sender=JID(from_jid)
287 addr=JID(to_jid)
288 win = addr if sender.short == self.whoami.short else sender #FIXME: duplicate code with QuickApp
289 if (self.currentChat==None):
290 self.currentChat=win.short
291 self.showChat(win.short)
292
293 # we show the window in the status bar
294 if not self.currentChat == win.short:
295 self.statusBar.add_item(win.short)
296 self.statusBar.show()
297 self.sizer.update()
298 self.statusBar.update()
299
300 self.editBar.replace_cur()
301 curses.doupdate()
302
303 def onResize(self, sig, stack):
304 """Called on SIGWINCH.
305 resize the screen and launch the loop"""
306 height, width = ttysize()
307 curses.resizeterm(height, width)
308 gobject.idle_add(self.callOnceLoop)
309
310 def callOnceLoop(self):
311 """Call the loop and return false (for not being called again by gobject mainloop).
312 Usefull for calling loop when there is no input in stdin"""
313 self.loopCB()
314 return False
315
316 def __key_handling(self, k):
317 """Handle key and transmit to active window."""
318
319 ### General keys, handled _everytime_ ###
320 if k == C('x'):
321 if os.getenv('TERM')=='screen':
322 os.system('screen -X remove')
323 else:
324 self.loop.quit()
325
326 ## windows navigation
327 elif k == C('l') and not self.contactList.isHidden():
328 """We go to the contact list"""
329 self.contactList.activate(not self.contactList.isActive())
330 if self.currentChat:
331 self.editBar.activate(not self.contactList.isActive())
332
333 elif k == curses.KEY_F2:
334 self.contactList.hide(not self.contactList.isHidden())
335 if self.contactList.isHidden():
336 self.contactList.activate(False) #TODO: auto deactivation when hiding ?
337 if self.currentChat:
338 self.editBar.activate(True)
339 self.sizer.update()
340
341 ## Chat Params ##
342 elif k == C('c'):
343 self.chatParams["color"] = not self.chatParams["color"]
344 self.color(self.chatParams["color"])
345 elif k == C('t'):
346 self.chatParams["timestamp"] = not self.chatParams["timestamp"]
347 self.chat_wins[self.currentChat].update()
348 elif k == C('s'):
349 self.chatParams["short_nick"] = not self.chatParams["short_nick"]
350 self.chat_wins[self.currentChat].update()
351
352 ## misc ##
353 elif k == curses.KEY_RESIZE:
354 stdscr.erase()
355 height, width = stdscr.getmaxyx()
356 if height<5 and width<35:
357 stdscr.addstr("Pleeeeasse, I can't even breathe !")
358 else:
359 for win in self.listWins:
360 win.resizeAdapt()
361 for win in self.chat_wins.keys():
362 self.chat_wins[win].resizeAdapt()
363 self.sizer.update() # FIXME: everything need to be managed by the sizer
364
365 ## we now throw the key to win handlers ##
366 else:
367 for win in self.listWins:
368 if win.isActive():
369 win.handleKey(k)
370 if self.currentChat:
371 self.chat_wins[self.currentChat].handleKey(k)
372
373 def loopCB(self, source="", cb_condition=""):
374 """This callback is called by the main loop"""
375 #pressed = self.contactList.window.getch()
376 pressed = stdscr.getch()
377 if pressed != curses.ERR:
378 self.__key_handling(pressed)
379 self.editBar.replace_cur()
380 curses.doupdate()
381
382
383 return True
384
385
386 sat = SortilegeApp()
387 sat.start()