threading fixes, so it runs on windows
[minibook.git] / minibook.py
blobac1b86e4b9bd4c71113f6e3ad57679ffc2329d56
1 #!/usr/bin/env python
2 """ Minibook: the Facebook(TM) status updater
3 (C) 2009 Gergely Imreh <imrehg@gmail.com>
4 """
6 VERSION = '0.1.0'
7 APPNAME = 'minibook'
9 import pygtk
10 pygtk.require('2.0')
11 import gtk
12 import gobject
13 try:
14 from facebook import Facebook
15 except:
16 print "Pyfacebook is not available, cannot run."
17 exit(1)
19 import threading
21 gobject.threads_init()
22 gtk.gdk.threads_init()
23 gtk.gdk.threads_enter()
25 try:
26 import gtkspell
27 spelling_support = True
28 except:
29 spelling_support = False
32 class Columns:
33 (ID, STATUS, DATETIME, REPLIES, LIKES) = range(5)
36 #-------------------------------------------------
37 # From http://edsiper.linuxchile.cl/blog/?p=152
38 # to mitigate TreeView + threads problems
39 #-------------------------------------------------
41 class _IdleObject(gobject.GObject):
42 """
43 Override gobject.GObject to always emit signals in the main thread
44 by emmitting on an idle handler
45 """
47 def __init__(self):
48 gobject.GObject.__init__(self)
50 def emit(self, *args):
51 gobject.idle_add(gobject.GObject.emit, self, *args)
54 #-------------------------------------------------
55 # Thread support
56 #-------------------------------------------------
58 class _WorkerThread(threading.Thread, _IdleObject):
59 """A single working thread."""
61 __gsignals__ = {
62 "completed": (
63 gobject.SIGNAL_RUN_LAST,
64 gobject.TYPE_NONE,
65 (gobject.TYPE_PYOBJECT, )),
66 "exception": (
67 gobject.SIGNAL_RUN_LAST,
68 gobject.TYPE_NONE,
69 (gobject.TYPE_PYOBJECT, ))}
71 def __init__(self, function, *args, **kwargs):
72 threading.Thread.__init__(self)
73 _IdleObject.__init__(self)
74 self._function = function
75 self._args = args
76 self._kwargs = kwargs
78 def run(self):
79 # call the function
80 print('Thread %s calling %s', self.name, str(self._function))
82 args = self._args
83 kwargs = self._kwargs
85 try:
86 result = self._function(*args, **kwargs)
87 except Exception, exc: # Catch ALL exceptions
88 # TODO: Check if this catch all warnins too!
89 print('Exception %s' % str(exc))
90 self.emit("exception", exc)
91 return
93 print('Thread %s completed', self.name)
95 self.emit("completed", result)
96 return
99 class _ThreadManager(object):
100 """Manages the threads."""
102 def __init__(self, max_threads=2):
103 """Start the thread pool. The number of threads in the pool is defined
104 by `pool_size`, defaults to 2."""
105 self._max_threads = max_threads
106 self._thread_pool = []
107 self._running = []
108 self._thread_id = 0
110 return
112 def _remove_thread(self, widget, arg=None):
113 """Called when the thread completes. We remove it from the thread list
114 (dictionary, actually) and start the next thread (if there is one)."""
116 # not actually a widget. It's the object that emitted the signal, in
117 # this case, the _WorkerThread object.
118 thread_id = widget.name
120 print('Thread %s completed, %d threads in the queue' % (thread_id,
121 len(self._thread_pool)))
123 self._running.remove(thread_id)
125 if self._thread_pool:
126 if len(self._running) < self._max_threads:
127 next = self._thread_pool.pop()
128 print('Dequeuing thread %s', next.name)
129 self._running.append(next.name)
130 next.start()
132 return
134 def add_work(self, complete_cb, exception_cb, func, *args, **kwargs):
135 """Add a work to the thread list."""
137 thread = _WorkerThread(func, *args, **kwargs)
138 thread_id = '%s' % (self._thread_id)
140 thread.connect('completed', complete_cb)
141 thread.connect('completed', self._remove_thread)
142 thread.connect('exception', exception_cb)
143 thread.setName(thread_id)
145 if len(self._running) < self._max_threads:
146 self._running.append(thread_id)
147 thread.start()
148 else:
149 running_names = ', '.join(self._running)
150 print('Threads %s running, adding %s to the queue',
151 running_names, thread_id)
152 self._thread_pool.append(thread)
154 self._thread_id += 1
155 return
158 class MainWindow:
159 """The main application interface"""
161 def enter_callback(self, widget, entry):
162 entry_text = entry.get_buffer().get_text()
163 print "Entry contents: %s\n" % entry_text
165 def sendupdate(self):
166 textfield = self.entry.get_buffer()
167 start = textfield.get_start_iter()
168 end = textfield.get_end_iter()
169 entry_text = textfield.get_text(start, end)
170 if entry_text != "":
171 print "Sent entry contents: %s\n" % entry_text
172 self._facebook.status.set([entry_text], [self._facebook.uid])
174 textfield.set_text("")
176 def getupdates(self):
177 list = self._facebook.status.get([self._facebook.uid], [10])
178 status_list = []
179 for status in list:
180 print status
181 status_list.append((status['status_id'],
182 status['message'],
183 status['time'],
184 '0',
185 '0'))
186 for data in status_list:
187 self.liststore.append(data)
189 def post_updates(self, widget, results):
190 print("Update result: %s" % (str(results)))
191 return
193 def except_updates(self, widget, exception):
194 print("Update exception: %s" % (str(exception)))
196 def count(self, text):
197 start = text.get_start_iter()
198 end = text.get_end_iter()
199 thetext = text.get_text(start, end)
200 self.count_label.set_text('(%d)' % (160 - len(thetext)))
201 return True
203 def __init__(self, facebook):
204 global spelling_support
206 # create a new window
207 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
208 self.window.set_size_request(400, 250)
209 self.window.set_title("Minibook")
210 self.window.connect("delete_event", lambda w, e: gtk.main_quit())
212 vbox = gtk.VBox(False, 0)
213 self.window.add(vbox)
214 vbox.show()
216 self.create_grid()
217 self.statuslist_window = gtk.ScrolledWindow()
218 self.statuslist_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
219 self.statuslist_window.add(self.treeview)
220 self.treeview.show()
221 self.statuslist_window.show()
222 vbox.add(self.statuslist_window)
224 hbox = gtk.HBox(False, 0)
226 label = gtk.Label("What's on your mind?")
227 hbox.pack_start(label, True, True, 0)
228 label.show()
229 self.count_label = gtk.Label("(160)")
230 hbox.pack_start(self.count_label, True, True, 0)
231 self.count_label.show()
232 vbox.add(hbox)
233 hbox.show()
235 self.entry = gtk.TextView()
236 text = self.entry.get_buffer()
237 text.connect('changed', self.count)
238 vbox.pack_start(self.entry, True, True, 0)
239 self.entry.show()
241 hbox = gtk.HBox(False, 0)
242 vbox.add(hbox)
243 hbox.show()
245 button = gtk.Button(stock=gtk.STOCK_CLOSE)
246 button.connect("clicked", lambda w: gtk.main_quit())
247 hbox.pack_start(button, True, True, 0)
248 button.set_flags(gtk.CAN_DEFAULT)
249 button.grab_default()
250 button.show()
252 button = gtk.Button(stock=gtk.STOCK_ADD)
253 button.connect("clicked", lambda w: self.sendupdate())
254 hbox.pack_start(button, True, True, 0)
255 button.set_flags(gtk.CAN_DEFAULT)
256 button.grab_default()
257 button.show()
259 if spelling_support:
260 try:
261 spelling = gtkspell.Spell(self.entry, 'en')
262 except:
263 spelling_support = False
265 self.window.show()
266 self._facebook = facebook
268 self._app_icon = 'minibook.png'
269 self._systray = gtk.StatusIcon()
270 self._systray.set_from_file(self._app_icon)
271 self._systray.set_tooltip('%s\n' \
272 'Left-click: toggle window hiding' % (APPNAME))
273 self._systray.connect('activate', self.systray_click)
274 self._systray.set_visible(True)
276 self._threads = _ThreadManager()
278 self.userinfo = self._facebook.users.getInfo([self._facebook.uid], \
279 ['name'])[0]
280 self._threads.add_work(self.post_updates,
281 self.except_updates,
282 self.getupdates)
284 def systray_click(self, widget, user_param=None):
285 if self.window.get_property('visible'):
286 self.window.hide()
287 else:
288 self.window.deiconify()
289 self.window.present()
291 def create_grid(self):
292 self.liststore = gtk.ListStore(gobject.TYPE_STRING,
293 gobject.TYPE_STRING,
294 gobject.TYPE_STRING,
295 gobject.TYPE_STRING,
296 gobject.TYPE_STRING)
297 self.treeview = gtk.TreeView(self.liststore)
298 self.treeview.set_property('headers-visible', False)
299 self.treeview.set_rules_hint(True)
301 self.status_renderer = gtk.CellRendererText()
302 #~ self.status_renderer.set_property('wrap-mode', gtk.WRAP_WORD)
303 self.status_renderer.set_property('wrap-width', 350)
304 self.status_renderer.set_property('width', 10)
306 self.status_column = gtk.TreeViewColumn('Message', \
307 self.status_renderer, text=1)
308 self.status_column.set_cell_data_func(self.status_renderer, \
309 self.status_format)
310 self.treeview.append_column(self.status_column)
311 self.treeview.set_resize_mode(gtk.RESIZE_IMMEDIATE)
313 def status_format(self, column, cell, store, position):
314 status = store.get_value(position, Columns.STATUS)
315 name = self.userinfo['name']
316 markup = '%s %s' % \
317 (name, status)
318 cell.set_property('markup', markup)
319 return
322 def main(facebook):
323 gtk.main()
324 gtk.gdk.threads_leave()
325 return 0
327 if __name__ == "__main__":
328 try:
329 config_file = open("config", "r")
330 api_key = config_file.readline()[:-1]
331 secret_key = config_file.readline()[:-1]
332 except Exception, e:
333 exit('Error while loading config file: %s' % (str(e)))
334 facebook = Facebook(api_key, secret_key)
335 facebook.auth.createToken()
336 facebook.login()
338 # Delay dialog to allow for login in browser
339 dia = gtk.Dialog('minibook: login',
340 None,
341 gtk.DIALOG_MODAL | \
342 gtk.DIALOG_DESTROY_WITH_PARENT | \
343 gtk.DIALOG_NO_SEPARATOR,
344 ("Logged In", gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CLOSE))
345 label = gtk.Label("Click after logging in to Facebook in your browser:")
346 dia.vbox.pack_start(label, True, True, 10)
347 label.show()
348 dia.show()
349 result = dia.run()
350 if result == gtk.RESPONSE_CLOSE:
351 print "Bye"
352 exit(0)
353 dia.destroy()
355 facebook.auth.getSession()
356 print 'Session Key: ', facebook.session_key
357 print 'Your UID: ', facebook.uid
359 MainWindow(facebook)
360 main(facebook)