Mercurial > libervia-backend
comparison 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 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:c4bc297b82f0 |
---|---|
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 newMessage(self, from_jid, msg, type, to_jid): | |
277 QuickApp.newMessage(self, from_jid, msg, type, to_jid) | |
278 sender=JID(from_jid) | |
279 addr=JID(to_jid) | |
280 win = addr if sender.short == self.whoami.short else sender #FIXME: duplicate code with QuickApp | |
281 if (self.currentChat==None): | |
282 self.currentChat=win.short | |
283 self.showChat(win.short) | |
284 | |
285 # we show the window in the status bar | |
286 if not self.currentChat == win.short: | |
287 self.statusBar.add_item(win.short) | |
288 self.statusBar.show() | |
289 self.sizer.update() | |
290 self.statusBar.update() | |
291 | |
292 self.editBar.replace_cur() | |
293 curses.doupdate() | |
294 | |
295 def onResize(self, sig, stack): | |
296 """Called on SIGWINCH. | |
297 resize the screen and launch the loop""" | |
298 height, width = ttysize() | |
299 curses.resizeterm(height, width) | |
300 gobject.idle_add(self.callOnceLoop) | |
301 | |
302 def callOnceLoop(self): | |
303 """Call the loop and return false (for not being called again by gobject mainloop). | |
304 Usefull for calling loop when there is no input in stdin""" | |
305 self.loopCB() | |
306 return False | |
307 | |
308 def __key_handling(self, k): | |
309 """Handle key and transmit to active window.""" | |
310 | |
311 ### General keys, handled _everytime_ ### | |
312 if k == C('x'): | |
313 if os.getenv('TERM')=='screen': | |
314 os.system('screen -X remove') | |
315 else: | |
316 self.loop.quit() | |
317 | |
318 ## windows navigation | |
319 elif k == C('l') and not self.contactList.isHidden(): | |
320 """We go to the contact list""" | |
321 self.contactList.activate(not self.contactList.isActive()) | |
322 if self.currentChat: | |
323 self.editBar.activate(not self.contactList.isActive()) | |
324 | |
325 elif k == curses.KEY_F2: | |
326 self.contactList.hide(not self.contactList.isHidden()) | |
327 if self.contactList.isHidden(): | |
328 self.contactList.activate(False) #TODO: auto deactivation when hiding ? | |
329 if self.currentChat: | |
330 self.editBar.activate(True) | |
331 self.sizer.update() | |
332 | |
333 ## Chat Params ## | |
334 elif k == C('c'): | |
335 self.chatParams["color"] = not self.chatParams["color"] | |
336 self.color(self.chatParams["color"]) | |
337 elif k == C('t'): | |
338 self.chatParams["timestamp"] = not self.chatParams["timestamp"] | |
339 self.chat_wins[self.currentChat].update() | |
340 elif k == C('s'): | |
341 self.chatParams["short_nick"] = not self.chatParams["short_nick"] | |
342 self.chat_wins[self.currentChat].update() | |
343 | |
344 ## misc ## | |
345 elif k == curses.KEY_RESIZE: | |
346 stdscr.erase() | |
347 height, width = stdscr.getmaxyx() | |
348 if height<5 and width<35: | |
349 stdscr.addstr("Pleeeeasse, I can't even breathe !") | |
350 else: | |
351 for win in self.listWins: | |
352 win.resizeAdapt() | |
353 for win in self.chat_wins.keys(): | |
354 self.chat_wins[win].resizeAdapt() | |
355 self.sizer.update() # FIXME: everything need to be managed by the sizer | |
356 | |
357 ## we now throw the key to win handlers ## | |
358 else: | |
359 for win in self.listWins: | |
360 if win.isActive(): | |
361 win.handleKey(k) | |
362 if self.currentChat: | |
363 self.chat_wins[self.currentChat].handleKey(k) | |
364 | |
365 def loopCB(self, source="", cb_condition=""): | |
366 """This callback is called by the main loop""" | |
367 #pressed = self.contactList.window.getch() | |
368 pressed = stdscr.getch() | |
369 if pressed != curses.ERR: | |
370 self.__key_handling(pressed) | |
371 self.editBar.replace_cur() | |
372 curses.doupdate() | |
373 | |
374 | |
375 return True | |
376 | |
377 | |
378 sat = SortilegeApp() | |
379 sat.start() |