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