#!/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. '''