2013-12-12 21:38:31 +00:00
#!/usr/bin/env python
2013-11-28 08:33:25 +00:00
2014-06-07 05:18:04 +00:00
import os
2014-09-24 06:53:54 +00:00
import requests
2014-09-30 19:08:25 +00:00
import subprocess
2014-06-07 05:18:04 +00:00
2017-12-24 13:25:20 +00:00
if ( os . environ . get ( ' TRAVIS ' ) != ' true ' ) :
2020-06-14 18:33:57 +00:00
import gi
gi . require_version ( ' Gtk ' , ' 3.0 ' )
from gi . repository import Gtk , GLib
2014-06-06 21:38:31 +00:00
import webbrowser
2014-06-07 05:27:48 +00:00
try :
import appindicator
except ImportError :
2017-12-24 11:13:07 +00:00
from . import appindicator_replacement as appindicator
2014-06-07 05:27:48 +00:00
2017-12-24 11:13:07 +00:00
from . appindicator_replacement import get_icon_filename
2017-10-03 12:07:03 +00:00
2013-11-28 08:33:25 +00:00
import json
2013-12-01 11:15:12 +00:00
import argparse
2013-11-28 08:33:25 +00:00
from os . path import expanduser
2013-12-01 12:22:59 +00:00
import signal
2013-11-28 08:33:25 +00:00
2017-12-24 11:13:07 +00:00
from . hackernews import HackerNews
from . chrome import Chrome
from . firefox import Firefox
from . version import Version
2013-12-12 21:38:31 +00:00
2017-12-24 13:25:20 +00:00
2013-11-28 08:33:25 +00:00
class HackerNewsApp :
2013-12-12 21:38:31 +00:00
HN_URL_PREFIX = " https://news.ycombinator.com/item?id= "
2014-09-25 16:50:45 +00:00
UPDATE_URL = " https://github.com/captn3m0/hackertray#upgrade "
ABOUT_URL = " https://github.com/captn3m0/hackertray "
2017-12-24 13:25:20 +00:00
2014-06-07 06:54:37 +00:00
def __init__ ( self , args ) :
2017-12-24 13:25:20 +00:00
# Load the database
2013-12-12 21:38:31 +00:00
home = expanduser ( " ~ " )
with open ( home + ' /.hackertray.json ' , ' a+ ' ) as content_file :
content_file . seek ( 0 )
content = content_file . read ( )
try :
self . db = set ( json . loads ( content ) )
except ValueError :
self . db = set ( )
# create an indicator applet
self . ind = appindicator . Indicator ( " Hacker Tray " , " hacker-tray " , appindicator . CATEGORY_APPLICATION_STATUS )
self . ind . set_status ( appindicator . STATUS_ACTIVE )
self . ind . set_icon ( get_icon_filename ( " hacker-tray.png " ) )
# create a menu
2020-06-14 18:33:57 +00:00
self . menu = Gtk . Menu ( )
2013-12-12 21:38:31 +00:00
2017-12-24 13:25:20 +00:00
# The default state is false, and it toggles when you click on it
2014-06-07 06:54:37 +00:00
self . commentState = args . comments
2013-12-12 21:38:31 +00:00
# create items for the menu - refresh, quit and a separator
2020-06-14 18:33:57 +00:00
menuSeparator = Gtk . SeparatorMenuItem ( )
2013-12-12 21:38:31 +00:00
menuSeparator . show ( )
self . menu . append ( menuSeparator )
2020-06-14 18:33:57 +00:00
btnComments = Gtk . CheckMenuItem ( " Show Comments " )
2013-12-12 21:38:31 +00:00
btnComments . show ( )
2014-06-07 06:54:37 +00:00
btnComments . set_active ( args . comments )
2020-06-14 19:59:57 +00:00
btnComments . set_draw_as_radio ( True )
2013-12-12 21:38:31 +00:00
btnComments . connect ( " activate " , self . toggleComments )
self . menu . append ( btnComments )
2020-06-14 18:33:57 +00:00
btnAbout = Gtk . MenuItem ( " About " )
2013-12-12 21:38:31 +00:00
btnAbout . show ( )
btnAbout . connect ( " activate " , self . showAbout )
self . menu . append ( btnAbout )
2020-06-14 18:33:57 +00:00
btnRefresh = Gtk . MenuItem ( " Refresh " )
2013-12-12 21:38:31 +00:00
btnRefresh . show ( )
2017-12-24 13:25:20 +00:00
# the last parameter is for not running the timer
2014-06-07 09:26:16 +00:00
btnRefresh . connect ( " activate " , self . refresh , True , args . chrome )
2013-12-12 21:38:31 +00:00
self . menu . append ( btnRefresh )
2014-09-25 16:50:45 +00:00
if Version . new_available ( ) :
2020-06-14 18:33:57 +00:00
btnUpdate = Gtk . MenuItem ( " New Update Available " )
2014-09-25 16:50:45 +00:00
btnUpdate . show ( )
2017-12-24 13:25:20 +00:00
btnUpdate . connect ( ' activate ' , self . showUpdate )
2014-09-25 16:50:45 +00:00
self . menu . append ( btnUpdate )
2020-06-14 18:33:57 +00:00
btnQuit = Gtk . MenuItem ( " Quit " )
2013-12-12 21:38:31 +00:00
btnQuit . show ( )
btnQuit . connect ( " activate " , self . quit )
self . menu . append ( btnQuit )
self . menu . show ( )
self . ind . set_menu ( self . menu )
2020-06-14 18:46:34 +00:00
if args . firefox == " auto " :
args . firefox = Firefox . default_firefox_profile_path ( )
2014-10-03 13:27:42 +00:00
self . refresh ( chrome_data_directory = args . chrome , firefox_data_directory = args . firefox )
2013-12-12 21:38:31 +00:00
def toggleComments ( self , widget ) :
""" Whether comments page is opened or not """
self . commentState = not self . commentState
2017-12-24 13:25:20 +00:00
def showUpdate ( self , widget ) :
2014-09-25 16:50:45 +00:00
""" Handle the update button """
webbrowser . open ( HackerNewsApp . UPDATE_URL )
# Remove the update button once clicked
self . menu . remove ( widget )
2013-12-12 21:38:31 +00:00
def showAbout ( self , widget ) :
""" Handle the about btn """
2014-09-25 16:50:45 +00:00
webbrowser . open ( HackerNewsApp . ABOUT_URL )
2013-12-12 21:38:31 +00:00
2017-12-24 13:25:20 +00:00
# ToDo: Handle keyboard interrupt properly
2013-12-12 21:38:31 +00:00
def quit ( self , widget , data = None ) :
""" Handler for the quit button """
l = list ( self . db )
home = expanduser ( " ~ " )
2017-12-24 13:25:20 +00:00
# truncate the file
2013-12-12 21:38:31 +00:00
with open ( home + ' /.hackertray.json ' , ' w+ ' ) as file :
file . write ( json . dumps ( l ) )
2020-06-14 18:33:57 +00:00
Gtk . main_quit ( )
2013-12-12 21:38:31 +00:00
def run ( self ) :
signal . signal ( signal . SIGINT , self . quit )
2020-06-14 18:33:57 +00:00
Gtk . main ( )
2013-12-12 21:38:31 +00:00
return 0
2020-06-14 19:59:57 +00:00
def open ( self , widget , * * args ) :
2013-12-12 21:38:31 +00:00
""" Opens the link in the web browser """
2017-12-24 13:25:20 +00:00
# We disconnect and reconnect the event in case we have
# to set it to active and we don't want the signal to be processed
2013-12-12 21:38:31 +00:00
if not widget . get_active ( ) :
widget . disconnect ( widget . signal_id )
widget . set_active ( True )
widget . signal_id = widget . connect ( ' activate ' , self . open )
self . db . add ( widget . item_id )
webbrowser . open ( widget . url )
2020-06-14 19:59:57 +00:00
# TODO: Add support for Shift+Click or Right Click
# to do the opposite of the current commentState setting
2013-12-12 21:38:31 +00:00
if self . commentState :
2017-10-03 12:07:03 +00:00
webbrowser . open ( self . HN_URL_PREFIX + str ( widget . hn_id ) )
2013-12-12 21:38:31 +00:00
def addItem ( self , item ) :
""" Adds an item to the menu """
2017-12-24 13:25:20 +00:00
# This is in the case of YC Job Postings, which we skip
2013-12-12 21:38:31 +00:00
if item [ ' points ' ] == 0 or item [ ' points ' ] is None :
return
2020-06-14 18:33:57 +00:00
i = Gtk . CheckMenuItem (
2013-12-12 21:38:31 +00:00
" ( " + str ( item [ ' points ' ] ) . zfill ( 3 ) + " / " + str ( item [ ' comments_count ' ] ) . zfill ( 3 ) + " ) " + item [ ' title ' ] )
2014-06-07 06:54:37 +00:00
visited = item [ ' history ' ] or item [ ' id ' ] in self . db
i . set_active ( visited )
2013-12-12 21:38:31 +00:00
i . url = item [ ' url ' ]
2014-09-27 04:41:32 +00:00
tooltip = " {url} \n Posted by {user} {timeago} " . format ( url = item [ ' url ' ] , user = item [ ' user ' ] , timeago = item [ ' time_ago ' ] )
i . set_tooltip_text ( tooltip )
2013-12-12 21:38:31 +00:00
i . signal_id = i . connect ( ' activate ' , self . open )
i . hn_id = item [ ' id ' ]
i . item_id = item [ ' id ' ]
self . menu . prepend ( i )
i . show ( )
2014-10-03 13:27:42 +00:00
def refresh ( self , widget = None , no_timer = False , chrome_data_directory = None , firefox_data_directory = None ) :
2014-09-24 06:53:54 +00:00
""" Refreshes the menu """
try :
2014-10-03 13:27:42 +00:00
# Create an array of 20 false to denote matches in History
searchResults = [ False ] * 20
2014-09-24 06:53:54 +00:00
data = list ( reversed ( HackerNews . getHomePage ( ) [ 0 : 20 ] ) )
2014-10-03 13:27:42 +00:00
urls = [ item [ ' url ' ] for item in data ]
2014-06-07 06:54:37 +00:00
if ( chrome_data_directory ) :
2014-10-03 13:27:42 +00:00
searchResults = self . mergeBoolArray ( searchResults , Chrome . search ( urls , chrome_data_directory ) )
if ( firefox_data_directory ) :
searchResults = self . mergeBoolArray ( searchResults , Firefox . search ( urls , firefox_data_directory ) )
2014-09-24 06:53:54 +00:00
2017-12-24 13:25:20 +00:00
# Remove all the current stories
2014-09-24 06:53:54 +00:00
for i in self . menu . get_children ( ) :
if hasattr ( i , ' url ' ) :
self . menu . remove ( i )
2017-12-24 13:25:20 +00:00
# Add back all the refreshed news
2014-09-24 06:53:54 +00:00
for index , item in enumerate ( data ) :
2014-10-03 13:27:42 +00:00
item [ ' history ' ] = searchResults [ index ]
2017-10-03 12:07:03 +00:00
if item [ ' url ' ] . startswith ( ' item?id= ' ) :
item [ ' url ' ] = " https://news.ycombinator.com/ " + item [ ' url ' ]
2014-09-24 06:53:54 +00:00
self . addItem ( item )
# Catch network errors
except requests . exceptions . RequestException as e :
2017-12-24 11:13:07 +00:00
print ( " [+] There was an error in fetching news items " )
2014-09-24 06:53:54 +00:00
finally :
# Call every 10 minutes
if not no_timer :
2020-06-14 18:33:57 +00:00
GLib . timeout_add ( 10 * 30 * 1000 , self . refresh , widget , no_timer , chrome_data_directory )
2014-09-24 06:53:54 +00:00
2014-10-03 13:27:42 +00:00
# Merges two boolean arrays, using OR operation against each pair
def mergeBoolArray ( self , original , patch ) :
for index , var in enumerate ( original ) :
original [ index ] = original [ index ] or patch [ index ]
return original
2013-11-28 08:33:25 +00:00
2017-12-24 13:25:20 +00:00
2013-11-28 08:33:25 +00:00
def main ( ) :
2013-12-12 21:38:31 +00:00
parser = argparse . ArgumentParser ( description = ' Hacker News in your System Tray ' )
2017-12-24 13:25:20 +00:00
parser . add_argument ( ' -v ' , ' --version ' , action = ' version ' , version = Version . current ( ) )
parser . add_argument ( ' -c ' , ' --comments ' , dest = ' comments ' , action = ' store_true ' , help = " Load the HN comments link for the article as well " )
2014-06-07 06:54:37 +00:00
parser . add_argument ( ' --chrome ' , dest = ' chrome ' , help = " Specify a Google Chrome Profile directory to use for matching chrome history " )
2020-06-14 18:46:34 +00:00
parser . add_argument ( ' --firefox ' , dest = ' firefox ' , help = " Specify a Firefox Profile directory to use for matching firefox history. Pass auto to automatically pick the default profile " )
2014-01-29 14:34:10 +00:00
parser . set_defaults ( comments = False )
2014-09-30 18:00:36 +00:00
parser . set_defaults ( dnt = False )
2014-01-29 14:34:10 +00:00
args = parser . parse_args ( )
2014-06-07 06:54:37 +00:00
indicator = HackerNewsApp ( args )
2013-12-12 21:38:31 +00:00
indicator . run ( )