#!/usr/bin/env python # -*- coding: UTF8 -*- #*************************************************************************** #* * #* This program is free software; you can redistribute it and/or modify * #* it under the terms of the GNU General Public License (GPL) * #* as published by the Free Software Foundation; either version 2 of * #* the License, or (at your option) any later version. * #* for detail see the LICENCE text file. * #* * #* This program is distributed in the hope that it will be useful, * #* but WITHOUT ANY WARRANTY; without even the implied warranty of * #* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * #* GNU Library General Public License for more details. * #* * #* You should have received a copy of the GNU Library General Public * #* License along with this program; if not, write to the Free Software * #* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * #* USA * #* * #*************************************************************************** ''' Fluxtwitter 0.4 - 24.04.2010 author: Yorik van Have url: http://yorik.orgfree.com A very simple app to connect to your twitter account, fetch your friends timeline and display it in a pidgin-like window. History: 0.1 - 12.08.2009 - First version 0.2 - 19.11.2009 - Added fluxbox pseudo-transparency support 0.3 - 10.04.2010 - Tweets now accumulate until you open the window 0.4 - 24.04.2010 - Using monsterID in case twitter avatar doesn't work ''' import os, gtk, gobject, twitter, urllib, re, subprocess, time, string, md5 # defaults USERNAME = "user" # username PASSWORD = "pass" # password DISPLAYTWEETS = 8 # minimum number of tweets displayed INTERVAL = 120 # update interval in seconds BROWSER = "x-www-browser" # default browser TOOLBARHEIGHT = 19 # height of your dektop toolbar, for calculating bg offset TRANSPARENCY = 50 # transparency level COMPOSITE = True # if we apply pseudo-transparency or not COMPOSITECOLOR = 0xffffff00 # color to composite the bg image to STACKMODE = True # if true, tweets will stack up until you read them fluxicon=[ "16 16 17 1", " c None", ". c #060911", "+ c #011136", "@ c #022274", "# c #002CA7", "$ c #1E2F58", "% c #313434", "& c #3D360B", "* c #0045DE", "= c #505461", "- c #7A6700", "; c #8B8B8B", "> c #B89F00", ", c #B6B8B5", "' c #F9D900", ") c #DCDCD7", "! c #FBFDFA", "..++++.. ", " @#######@+ ", " @#@$$$@##@$. ", " @@=)!);@=));. ", " @=!!!!!,;)!!=. ", " @,!!!!%%%%!!;+ ", " @,!!!)....,!;@ ", "+#;!!!!....)!$#+", "+#$!!!!;.+$;%@@.", " ##=)!!!=$&->>>&", " @*#$;;&>''''''&", " +**#.-''''''''&", " @**#=->''''''&", " @*#@#@&>'''> ", " .@#***$-'>& ", " +++ && " ] fluxiconalt=[ "16 16 17 1", " c None", ". c #753F00", "+ c #804300", "@ c #884900", "# c #914B00", "$ c #9D5300", "% c #AC5C01", "& c #B15F00", "* c #BB6201", "= c #C06500", "- c #D17000", "; c #DA7500", "> c #DE7800", ", c #E67903", "' c #EE7E00", ") c #F68500", "! c #FE8600", " %>,,,>=@ ", " ! +*'> ", " &@ .-% %!@ ", " > >- #'.)$&!@ ", " '#& ! @! ", " ,, ,-!> '; ", " >, !)') ,, ", " ,' !=!) .-, ", " ,%% *!)'>!>! ", " ' ' ')'-$.*#", " ' >>,)* +=", " #= !% $%", " ' ,)# ; ", " '+>'$-)@ ' ", " >'+ !' ' ", " =,,>+ )@ " ] iconnew = [ "16 16 17 1", " c None", ". c #080A02", "+ c #102E06", "@ c #174A00", "# c #444A3A", "$ c #545200", "% c #2C7200", "& c #72756B", "* c #7C7B7D", "= c #62A200", "- c #8A8C89", "; c #B69B00", "> c #AFAFB0", ", c #93E100", "' c #C4C3C5", ") c #FCDB00", "! c #F7F9F6", " .++++++. ", " @%%%%%%%@. ", " +%@++@%%@+. ", " @@>!!!#@'!'. ", " %*!!!!!*!!!*. ", " .@'!!!'.*.!!'+ ", " .@!!!!-...-!'% ", " .%>!!!>...>!#,.", " .=#!!!!#.%&#@@.", " .==*!!!'+$$;))$", " ===+&#$))))))$", " +,,%$))))))))$", " =,,%$;)))))).", " =,%$=$;))); ", " .=,,,=$)). ", " .$+ .+ " ] iconnewalt = [ "16 16 17 1", " c None", ". c #130D07", "+ c #251300", "@ c #412500", "# c #6F4203", "$ c #946405", "% c #8C6B41", "& c #BE7400", "* c #A17F48", "= c #B78A00", "- c #9D9489", "; c #F89400", "> c #E7BF00", ", c #FDBF00", "' c #D8CFC7", ") c #FEDC00", "! c #FCFEFA", " +++@++. ", " @;;;;;;&$+ ", " .;;;&&;;;&# ", " +;&-!'%;$''# ", " @;'!!!!%!!!'@ ", " #$!!!!--.-!!$+ ", " $*!!!!....!!$# ", " &$!!!!....!!=$ ", " &&'!!!-.$*-#=$ ", " $,$!!!!-$==>)= ", " @,,&*-$>)))))>.", " =,,$>)))))))>.", " @,,,=$>)))))= ", " @>=$==$>)))@ ", " +$>)))$=)$ ", " .+@+ @ " ] class TwitterStatusIcon(gtk.StatusIcon): def __init__(self): gtk.StatusIcon.__init__(self) # creating the status icon with its menu menu = ''' ''' actions = [ ('Menu', None, 'Menu'), ('Update', gtk.STOCK_REFRESH, '_Update now', None, 'Update', self.update), ('Settings', gtk.STOCK_PREFERENCES, '_Settings...', None, 'Settings', self.config), ('About', gtk.STOCK_ABOUT, '_About...', None, 'About Fluxtwitter', self.about), ('Close', gtk.STOCK_CLOSE, '_Close', None, 'Close', self.close)] ag = gtk.ActionGroup('Actions') ag.add_actions(actions) self.manager = gtk.UIManager() self.manager.insert_action_group(ag, 0) self.manager.add_ui_from_string(menu) self.menu = self.manager.get_widget('/Twitter/Menu/About').props.parent self.icon = gtk.gdk.pixbuf_new_from_xpm_data(fluxicon) self.iconnew = gtk.gdk.pixbuf_new_from_xpm_data(iconnew) self.set_from_pixbuf(self.icon) self.getconfig() self.set_tooltip(self.username + "'s timeline") self.set_visible(True) self.isTweet = False self.connect('popup-menu', self.popup_menu) self.connect('activate', self.showtimeline) self.api = twitter.Api(username=self.username,password=self.password) # creating the main dialog self.tweetdialog = gtk.Window() self.tweetdialog.connect("destroy",self.showtimeline) self.tweetdialog.connect("delete-event",self.showtimeline) self.tweetdialog.connect('configure-event', self.updateBackground) self.tweetdialog.set_title(self.username) self.tweetdialog.set_icon(self.icon) self.tweetdialog.set_border_width(5) self.tweetdialog.set_size_request(280, 500) self.layout = gtk.ScrolledWindow() self.layout.set_policy(gtk.POLICY_NEVER,gtk.POLICY_AUTOMATIC) self.vbox = gtk.VBox() self.layout.add_with_viewport(self.vbox) self.tweetdialog.add(self.layout) self.layout.get_child().set_shadow_type(gtk.SHADOW_NONE) self.layout.connect('scroll-child', self.updateBackground) self.tweets = [] self.table = None self.iteration = 1 self.updateBackground() self.timeout = gobject.timeout_add(self.interval*1000,self.update) self.update() def update(self, data=None): # updating from twitter print "iteration",self.iteration,": fetching",self.timeout,"tweets on",time.strftime('%X %x %Z') try: statuses = self.api.GetFriendsTimeline(count=self.displaytweets) except: print "Error: Couldn't connect to Twitter server." return True # even if we cannot connect, we continue trying next time self.iteration += 1 extras = 0 if (not self.tweets): extras = len(statuses) self.set_from_pixbuf(self.iconnew) elif (statuses[0].id != self.tweets[0]['id']): self.set_from_pixbuf(self.iconnew) for i in range(len(statuses)): if statuses[i].id == self.tweets[0]['id']: extras = i break if not extras: print "no new tweet to display" return True print 'list currently has',len(self.tweets),' - adding',extras for i in range(extras-1,-1,-1): # retrieving all we need from the tweet iconurl = statuses[i].user.GetProfileImageUrl() iconfile=urllib.urlopen(iconurl) print 'extra tweet',i,"from ",statuses[i].user.name," : ",statuses[i].text," ",iconurl pbl = gtk.gdk.PixbufLoader() pbl.write(iconfile.read()) tweetpb = pbl.get_pixbuf() pbl.close() if not tweetpb: # if icon is invalid, try to get a monsterid h = md5.new() h.update(statuses[i].user.screen_name) v = h.hexdigest() url = "http://friedcellcollective.net/monsterid/"+v+"/48" iconfile=urllib.urlopen(url) pbl = gtk.gdk.PixbufLoader() pbl.write(iconfile.read()) tweetpb = pbl.get_pixbuf() pbl.close() if not tweetpb: # if everything fails, use default icon tweetpb = self.icon tweetpb = tweetpb.scale_simple(48,48,gtk.gdk.INTERP_BILINEAR) tweettext = statuses[i].text.replace('ç','c') tweettext = tweettext.replace('ã','a') tweettext = tweettext.replace("'","") tweettext = tweettext.replace("&","&") pat1 = re.compile(r"(^|[\n ])(([\w]+?://[\w\#$%&~.\-;:=,?@\[\]+]*)(/[\w\#$%&~/.\-;:=,?@\[\]+]*)?)", re.IGNORECASE | re.DOTALL) pat2 = re.compile(r"(^|[\n ])(@([\w\#$%&~.\-;:=,?@\[\]+]*)(/[\w\#$%&~/.\-;:=,?@\[\]+]*)?)", re.IGNORECASE | re.DOTALL) pat3 = re.compile(r"(^|[\n ])(#([\w\#$%&~.\-;:=,?@\[\]+]*)(/[\w\#$%&~/.\-;:=,?@\[\]+]*)?)", re.IGNORECASE | re.DOTALL) tweettext = pat1.sub(r'\1\3', tweettext) tweettext = pat2.sub(r'\1\2', tweettext) tweettext = pat3.sub(r'\1\2', tweettext) # adding to our tweet list thistweet = {'id':statuses[i].id, 'user':statuses[i].user.screen_name, 'tweet':tweettext, 'icon':tweetpb} self.tweets.insert(0,thistweet) #adding our new tweet to the top of the list # if list window is visible, don't stack if self.isTweet: self.tweets = self.tweets[:self.displaytweets] # dont rebuild if window is open (buggy) if not self.isTweet: self.rebuildTable() return True def rebuildTable(self): print "building table with",len(self.tweets),"items" newtable = gtk.Table(len(self.tweets),2) newtable.set_row_spacings(10) for i in range(len(self.tweets)): label = gtk.Label() label.set_line_wrap(True) label.set_width_chars(25) label.set_markup(self.tweets[i]['tweet']) label.set_selectable(True) # label.connect("activate-current-link",self.clicked) # not really working newtable.attach(label,1,2,i,i+1) icon = gtk.Image() icon.set_from_pixbuf(self.tweets[i]['icon']) icon.set_tooltip_text(self.tweets[i]['user']) button = gtk.Button() button.set_relief(gtk.RELIEF_NONE) button.set_image(icon) button.set_name(str(i)) button.connect("clicked",self.clicked) button.set_focus_on_click(False) newtable.attach(button,0,1,i,i+1) if self.table: self.vbox.remove(self.table) self.vbox.pack_start(newtable) self.table = newtable def getconfig(self): self.username = USERNAME self.password = PASSWORD self.displaytweets = DISPLAYTWEETS self.browser = BROWSER self.interval = INTERVAL self.composite = COMPOSITE self.transparency = TRANSPARENCY self.toolbarheight = TOOLBARHEIGHT self.compositecolor = COMPOSITECOLOR self.stackmode = STACKMODE configfile = os.path.expanduser('~') + os.sep + '.fluxtwitterrc' if os.path.isfile(configfile): file = open(configfile) for line in file: if not("#" in line): key, value = line.split("=", 1) key = key.strip() value = value.strip() if key == "username": self.username = value elif key == "password": self.password = value elif key == "displaytweets": self.displaytweets = int(value) elif key == "browser": self.browser = value elif key == "interval": self.interval = int(value) elif key == "composite": self.composite = bool(value) elif key == "transparency": self.transparency = int(value) elif key == "toolbarheight": self.toolbarheight = int(value) elif key == "compositecolor": self.compositecolor = string.atoi(value,0) elif key == "stackmode": self.stackmode = bool(value) file.close() else: print "Creating config file..." self.writeconfig() def writeconfig(self): configfile = os.path.expanduser('~') + os.sep + '.fluxtwitterrc' file = open(configfile,'wb') file.write('# Fluxtwitter configuration file\n') file.write('# This is your twitter username\n') file.write('username = ' + self.username + '\n') file.write('# This is your twitter password\n') file.write('password = ' + self.password + '\n') file.write('# Number of tweets displayed (default 8)\n') file.write('displaytweets = ' + str(self.displaytweets) + '\n') file.write('# Browser command to open links (default x-www-browser) \n') file.write('browser = ' + self.browser + '\n') file.write('# Interval in seconds between twitter updates (default 120)\n') file.write('interval = ' + str(self.interval) + '\n') file.write('# Do we use pseudo-transparency?\n') file.write('composite = '+ str(self.composite) + '\n') file.write('# Amount of image fading in percent\n') file.write('transparency = '+ str(self.transparency) + '\n') file.write('# Color to composite background with (0x00000000)\n') file.write('compositecolor ='+str(self.compositecolor) + '\n') file.write('# Window titlebar height correction in pixels\n') file.write('toolbarheight = ' + str(self.toolbarheight) + '\n') file.write('# Stack mode (if tweets will stack until you read them\n') file.write('stackmode = ' + str(self.stackmode) + '\n') file.close() def config(self,data): dialog = gtk.Dialog() dialog.set_title('Fluxtwitter settings') table = gtk.Table(10,2) c1 = gtk.Entry() c1.set_text(self.username) c1.set_tooltip_text('Your twitter username') c2 = gtk.Entry() c2.set_text(self.password) c2.set_visibility(False) c2.set_tooltip_text('Your twitter password') c3 = gtk.Entry() c3.set_text(str(self.displaytweets)) c3.set_tooltip_text('Number of tweets to display') c4 = gtk.Entry() c4.set_text(self.browser) c4.set_tooltip_text('Web browser to open links in') c5 = gtk.Entry() c5.set_text(str(self.interval)) c5.set_tooltip_text('Interval in seconds between updates') c6 = gtk.ToggleButton() c6.set_active(self.composite) c6.set_tooltip_text('Check this to use fluxbox pseudo-transparency') c7 = gtk.Entry() c7.set_text(str(self.transparency)) c7.set_tooltip_text('Fading level in percents') c8 = gtk.Entry() c8.set_text(str(self.toolbarheight)) c8.set_tooltip_text('Vertical correction in pixels') c9 = gtk.Entry() c9.set_text(str(self.compositecolor)) c9.set_tooltip_text('Composite color for transparency') c10 = gtk.ToggleButton() c10.set_active(self.stackmode) c10.set_tooltip_text('Check this for tweets to stack until you read them') table.attach(gtk.Label('Username '),0,1,0,1) table.attach(c1,1,2,0,1) table.attach(gtk.Label('Password '),0,1,1,2) table.attach(c2,1,2,1,2) table.attach(gtk.Label('Nr of tweets '),0,1,2,3) table.attach(c3,1,2,2,3) table.attach(gtk.Label('Web browser '),0,1,3,4) table.attach(c4,1,2,3,4) table.attach(gtk.Label('Interval '),0,1,4,5) table.attach(c5,1,2,4,5) table.attach(gtk.Label('Pseudo-transparency '),0,1,5,6) table.attach(c6,1,2,5,6) table.attach(gtk.Label('Fading '),0,1,6,7) table.attach(c7,1,2,6,7) table.attach(gtk.Label('Vertical correction '),0,1,7,8) table.attach(c8,1,2,7,8) table.attach(gtk.Label('Composite Color '),0,1,8,9) table.attach(c9,1,2,8,9) table.attach(gtk.Label('Stack Mode '),0,1,9,10) table.attach(c10,1,2,9,10) dialog.vbox.pack_start(table) dialog.show_all() cancel_button = dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) ok_button = dialog.add_button(gtk.STOCK_OK,gtk.RESPONSE_OK) ok_button.grab_default() resp = dialog.run() if resp == gtk.RESPONSE_OK: self.username = c1.get_text() self.password = c2.get_text() self.displaytweets = int(c3.get_text()) self.browser = c4.get_text() self.interval = int(c5.get_text()) self.composite = c6.get_active() self.transparency = int(c7.get_text()) self.toolbarheight = int(c8.get_text()) self.compositecolor = string.atoi(c9.get_text(),0) self.stackmode = c10.get_active() self.writeconfig() dialog.destroy() def updateBackground(self,args=None,stuff=None): if self.composite: x,y = self.tweetdialog.get_position() w,h = self.tweetdialog.get_size() bgfile = os.path.expanduser('~') + os.sep + '.fluxbox/lastwallpaper' if os.path.isfile(bgfile): wpfile = open(bgfile) pb=gtk.gdk.pixbuf_new_from_file(wpfile.read().split('|')[1]) wpfile.close() crop = gtk.gdk.Pixbuf( gtk.gdk.COLORSPACE_RGB, False, 8, w, h ) pb.copy_area(x, y+self.toolbarheight, w, h, crop, 0, 0) mask = crop.copy() mask.fill(self.compositecolor) opacity = int((self.transparency/100)*255) mask.composite(crop, 0, 0, w, h, 0, 0, 1, 1, gtk.gdk.INTERP_BILINEAR, 127) pm,m = crop.render_pixmap_and_mask(255) style = self.tweetdialog.get_style().copy() style.bg_pixmap[gtk.STATE_NORMAL] = pm self.tweetdialog.set_style(style) self.layout.get_child().set_style(style) def clicked(self,data,url=None): if data.name: url="http://www.twitter.com/"+self.tweets[int(data.name)]['user'] print "clicked link:",url self.tweetdialog.hide() self.isTweet = False self.tweets = self.tweets[:self.displaytweets] self.rebuildTable() subprocess.Popen([self.browser,url],shell=False) def close(self, data): gobject.source_remove(self.timeout) gtk.main_quit() def popup_menu(self, status, button, time): self.menu.popup(None, None, None, button, time) def about(self, data): dialog = gtk.AboutDialog() dialog.set_name('Fluxtwitter') dialog.set_version('0.2') dialog.set_comments('A system tray icon displaying twitter feed') dialog.set_website('http://yorik.uncreated.net') dialog.run() dialog.destroy() def showtimeline(self,data,event=None): if self.isTweet: self.tweetdialog.hide() self.isTweet = False self.tweets = self.tweets[:self.displaytweets] self.rebuildTable() else: self.isTweet = True self.tweetdialog.show_all() self.set_from_pixbuf(self.icon) return True if __name__ == '__main__': TwitterStatusIcon() gtk.main() note1 = ''' Since 2.18, GTK+ supports markup for clickable hyperlinks in addition to regular Pango markup. The markup for links is borrowed from HTML, using the a with href and title attributes. GTK+ renders links similar to the way they appear in web browsers, with colored, underlined text. The title attribute is displayed as a tooltip on the link. An example looks like this: gtk_label_set_markup (label, "Go to the GTK+ website for more..."); It is possible to implement custom handling for links and their tooltips with the "activate-link" signal and the gtk_label_get_current_uri() function. '''