0
|
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() |