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