commit
64cc856ed1
|
@ -18,3 +18,6 @@ var/
|
||||||
# Installer logs
|
# Installer logs
|
||||||
pip-log.txt
|
pip-log.txt
|
||||||
pip-delete-this-directory.txt
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
pyvenv.cfg
|
||||||
|
bin/
|
||||||
|
|
11
.travis.yml
11
.travis.yml
|
@ -1,11 +1,18 @@
|
||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
- "2.7"
|
# https://endoflife.date/python
|
||||||
|
# goes away in sep 2020
|
||||||
|
- "3.5"
|
||||||
|
# goes away in dec 2021
|
||||||
|
- "3.6"
|
||||||
|
# goes away in jun 2023
|
||||||
|
- "3.7"
|
||||||
|
# goes away in oct 2024
|
||||||
|
- "3.8"
|
||||||
# command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors
|
# command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors
|
||||||
install:
|
install:
|
||||||
- pip install requests
|
- pip install requests
|
||||||
- pip install nose
|
- pip install nose
|
||||||
- pip install mixpanel-py
|
|
||||||
# command to run tests, e.g. python setup.py test
|
# command to run tests, e.g. python setup.py test
|
||||||
script: nosetests --nocapture
|
script: nosetests --nocapture
|
||||||
notifications:
|
notifications:
|
||||||
|
|
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -1,5 +1,16 @@
|
||||||
This file will only list released and supported versions, usually skipping over very minor updates.
|
This file will only list released and supported versions, usually skipping over very minor updates.
|
||||||
|
|
||||||
|
Unreleased
|
||||||
|
==========
|
||||||
|
|
||||||
|
4.0.0
|
||||||
|
=====
|
||||||
|
|
||||||
|
* Adds support for --firefox auto, picks the default firefox profile automatically
|
||||||
|
* Upgrades to Python 3.0. Python 2 is no longer supported
|
||||||
|
* Switches from PyGtk to PyGObject.
|
||||||
|
* AppIndicator is no longer supported, because it is Python 2 only
|
||||||
|
|
||||||
3.0.0
|
3.0.0
|
||||||
=====
|
=====
|
||||||
|
|
||||||
|
|
29
README.md
29
README.md
|
@ -59,32 +59,32 @@ HackerTray accepts its various options via the command line. Run `hackertray -h`
|
||||||
|
|
||||||
1. `-c`: Enables comments support. Clicking on links will also open the comments page on HN. Can be switched off via the UI, but the setting is not remembered.
|
1. `-c`: Enables comments support. Clicking on links will also open the comments page on HN. Can be switched off via the UI, but the setting is not remembered.
|
||||||
2. `--chrome PROFILE-PATH`: Specifying a profile path to a chrome directory will make HackerTray read the Chrome History file to mark links as read. Links are checked once every 5 minutes, which is when the History file is copied (to override the lock in case Chrome is open), searched using sqlite and deleted. This feature is still experimental.
|
2. `--chrome PROFILE-PATH`: Specifying a profile path to a chrome directory will make HackerTray read the Chrome History file to mark links as read. Links are checked once every 5 minutes, which is when the History file is copied (to override the lock in case Chrome is open), searched using sqlite and deleted. This feature is still experimental.
|
||||||
3. `--firefox PROFILE-PATH`: Specify path to a firefox profile directory. HackerTray will read your firefox history from this profile, and use it to mark links as read.
|
3. `--firefox PROFILE-PATH`: Specify path to a firefox profile directory. HackerTray will read your firefox history from this profile, and use it to mark links as read. Pass `auto` as PROFILE-PATH to automatically read the default profile and use that.
|
||||||
4. `--dnt`: Disable analytics. Hackertray will no longer collect any sort of analytics. I'd prefer it if you left out this switch, as it helps me improve hackertray by understanding how its being used.
|
|
||||||
|
|
||||||
Note that the `--chrome` and `--firefox` options are independent, and can be used together. However, they cannot be specified multiple times (so reading from 2 chrome profiles is not possible).
|
Note that the `--chrome` and `--firefox` options are independent, and can be used together. However, they cannot be specified multiple times (so reading from 2 chrome profiles is not possible).
|
||||||
|
|
||||||
### Google Chrome Profile Path
|
### Google Chrome Profile Path
|
||||||
|
|
||||||
Where your Profile is stored depends on which version of chrome you are using:
|
Where your Profile is stored depends on [which version of chrome you are using](https://chromium.googlesource.com/chromium/src.git/+/62.0.3202.58/docs/user_data_dir.md#linux):
|
||||||
|
|
||||||
- `google-chrome-stable`: `~/.config/google-chrome/Default/`
|
- [Chrome Stable] `~/.config/google-chrome/Default`
|
||||||
- `google-chrome-unstable`: `~/.config/google-chrome-unstable/Default/`
|
- [Chrome Beta] `~/.config/google-chrome-beta/Default`
|
||||||
- `chromium`: `~/.config/chromium/Default/`
|
- [Chrome Dev] `~/.config/google-chrome-unstable/Default`
|
||||||
|
- [Chromium] `~/.config/chromium/Default`
|
||||||
|
|
||||||
Replace `Default` with `Profile 1`, `Profile 2` or so on if you use multiple profiles on Chrome. Note that the `--chrome` option accepts a `PROFILE-PATH`, not the History file itself. Also note that sometimes `~` might not be set, so you might need to use the complete path (such as `/home/nemo/.config/google-chrome/Default/`).
|
Replace `Default` with `Profile 1`, `Profile 2` or so on if you use multiple profiles on Chrome. Note that the `--chrome` option accepts a `PROFILE-PATH`, not the History file itself. Also note that sometimes `~` might not be set, so you might need to use the complete path (such as `/home/nemo/.config/google-chrome/Default/`).
|
||||||
|
|
||||||
### Firefox Profile Path
|
### Firefox Profile Path
|
||||||
|
|
||||||
The default firefox profile path is `~/.mozilla/firefox/*.default`, where `*` denotes a random 8 digit string. You can also read `~/.mozilla/firefox/profiles.ini` to get a list of profiles.
|
The default firefox profile path is `~/.mozilla/firefox/*.default`, where `*` denotes a random 8 digit string. You can also read `~/.mozilla/firefox/profiles.ini` to get a list of profiles. Alternatively, just pass `auto` and HackerTray will pick the default profile automatically.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
1. Minimalist Approach to HN
|
1. Minimalist Approach to HN
|
||||||
2. Opens links in your default browser
|
2. Opens links in your default browser
|
||||||
3. Remembers which links you opened
|
3. Remembers which links you opened, even if you opened them outside of HackerTray
|
||||||
4. Shows Points/Comment count in a simple format
|
4. Shows Points/Comment count in a simple format
|
||||||
5. Reads your Google Chrome History file to determine which links you've already read (even if you may not have opened them via HackerTray)
|
5. Reads your Google Chrome/Firefox History file to determine which links you've already read (even if you may not have opened them via HackerTray)
|
||||||
|
|
||||||
### Troubleshooting
|
### Troubleshooting
|
||||||
|
|
||||||
|
@ -93,6 +93,8 @@ python-appindicator with
|
||||||
|
|
||||||
`sudo apt-get install python-appindicator`
|
`sudo apt-get install python-appindicator`
|
||||||
|
|
||||||
|
Note that appindicator is no longer supported in non-Ubuntu distros, because it only works on Python2.
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
To develop on hackertray, or to test out experimental versions, do the following:
|
To develop on hackertray, or to test out experimental versions, do the following:
|
||||||
|
@ -103,13 +105,16 @@ To develop on hackertray, or to test out experimental versions, do the following
|
||||||
|
|
||||||
## Analytics
|
## Analytics
|
||||||
|
|
||||||
To help improve the project and learn how its being used, I've added Analytics in hackertray. The `--dnt` switch disables all analytics so that you can opt-out if desired. All data is collected anonymously, with no machine id or user-identifying information being sent back. To learn more, and see which events are being tracked, see the [Analytics](https://github.com/captn3m0/hackertray/wiki/Analytics) wiki page.
|
On every launch, a request is made to `https://pypi.python.org/pypi/hackertray/json` to check the latest version.
|
||||||
|
|
||||||
|
**No more tracking**. All data every collected for this project has been deleted. You can see [the wiki](https://github.com/captn3m0/hackertray/wiki/Analytics) for what all was collected earlier.
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
- Mark Rickert for [Hacker Bar](http://hackerbarapp.com/).
|
- Mark Rickert for [Hacker Bar](http://hackerbarapp.com/) (No longer active)
|
||||||
- [Giridaran Manivannan](https://github.com/ace03uec) for troubleshooting instructions.
|
- [Giridaran Manivannan](https://github.com/ace03uec) for troubleshooting instructions.
|
||||||
|
- [@cheeaun](https://github.com/cheeaun) for the [Unofficial Hacker News API](https://github.com/cheeaun/node-hnapi/)
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
Licenced under the [MIT Licence](http://nemo.mit-license.org/).
|
Licenced under the [MIT Licence](https://nemo.mit-license.org/).
|
||||||
|
|
|
@ -2,40 +2,37 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import requests
|
import requests
|
||||||
import platform
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
if(os.environ.get('TRAVIS') != 'true'):
|
if(os.environ.get('TRAVIS') != 'true'):
|
||||||
import pygtk
|
import gi
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
pygtk.require('2.0')
|
from gi.repository import Gtk,GLib
|
||||||
import gtk
|
|
||||||
|
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import appindicator
|
import appindicator
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import appindicator_replacement as appindicator
|
from . import appindicator_replacement as appindicator
|
||||||
|
|
||||||
from appindicator_replacement import get_icon_filename
|
from .appindicator_replacement import get_icon_filename
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import argparse
|
import argparse
|
||||||
from os.path import expanduser
|
from os.path import expanduser
|
||||||
import signal
|
import signal
|
||||||
|
|
||||||
from hackernews import HackerNews
|
from .hackernews import HackerNews
|
||||||
from chrome import Chrome
|
from .chrome import Chrome
|
||||||
from firefox import Firefox
|
from .firefox import Firefox
|
||||||
from version import Version
|
from .version import Version
|
||||||
from analytics import Analytics
|
|
||||||
|
|
||||||
class HackerNewsApp:
|
class HackerNewsApp:
|
||||||
HN_URL_PREFIX = "https://news.ycombinator.com/item?id="
|
HN_URL_PREFIX = "https://news.ycombinator.com/item?id="
|
||||||
UPDATE_URL = "https://github.com/captn3m0/hackertray#upgrade"
|
UPDATE_URL = "https://github.com/captn3m0/hackertray#upgrade"
|
||||||
ABOUT_URL = "https://github.com/captn3m0/hackertray"
|
ABOUT_URL = "https://github.com/captn3m0/hackertray"
|
||||||
MIXPANEL_TOKEN = "51a04e37dad59393c7371407e84a8050"
|
|
||||||
def __init__(self, args):
|
def __init__(self, args):
|
||||||
# Load the database
|
# Load the database
|
||||||
home = expanduser("~")
|
home = expanduser("~")
|
||||||
|
@ -47,69 +44,55 @@ class HackerNewsApp:
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.db = set()
|
self.db = set()
|
||||||
|
|
||||||
# Setup analytics
|
|
||||||
self.tracker = Analytics(args.dnt, HackerNewsApp.MIXPANEL_TOKEN)
|
|
||||||
|
|
||||||
# create an indicator applet
|
# create an indicator applet
|
||||||
self.ind = appindicator.Indicator("Hacker Tray", "hacker-tray", appindicator.CATEGORY_APPLICATION_STATUS)
|
self.ind = appindicator.Indicator("Hacker Tray", "hacker-tray", appindicator.CATEGORY_APPLICATION_STATUS)
|
||||||
self.ind.set_status(appindicator.STATUS_ACTIVE)
|
self.ind.set_status(appindicator.STATUS_ACTIVE)
|
||||||
self.ind.set_icon(get_icon_filename("hacker-tray.png"))
|
self.ind.set_icon(get_icon_filename("hacker-tray.png"))
|
||||||
|
|
||||||
# create a menu
|
# create a menu
|
||||||
self.menu = gtk.Menu()
|
self.menu = Gtk.Menu()
|
||||||
|
|
||||||
# The default state is false, and it toggles when you click on it
|
# The default state is false, and it toggles when you click on it
|
||||||
self.commentState = args.comments
|
self.commentState = args.comments
|
||||||
|
|
||||||
# create items for the menu - refresh, quit and a separator
|
# create items for the menu - refresh, quit and a separator
|
||||||
menuSeparator = gtk.SeparatorMenuItem()
|
menuSeparator = Gtk.SeparatorMenuItem()
|
||||||
menuSeparator.show()
|
menuSeparator.show()
|
||||||
self.menu.append(menuSeparator)
|
self.menu.append(menuSeparator)
|
||||||
|
|
||||||
btnComments = gtk.CheckMenuItem("Show Comments")
|
btnComments = Gtk.CheckMenuItem("Show Comments")
|
||||||
btnComments.show()
|
btnComments.show()
|
||||||
btnComments.set_active(args.comments)
|
btnComments.set_active(args.comments)
|
||||||
btnComments.connect("activate", self.toggleComments)
|
btnComments.connect("activate", self.toggleComments)
|
||||||
self.menu.append(btnComments)
|
self.menu.append(btnComments)
|
||||||
|
|
||||||
btnAbout = gtk.MenuItem("About")
|
btnAbout = Gtk.MenuItem("About")
|
||||||
btnAbout.show()
|
btnAbout.show()
|
||||||
btnAbout.connect("activate", self.showAbout)
|
btnAbout.connect("activate", self.showAbout)
|
||||||
self.menu.append(btnAbout)
|
self.menu.append(btnAbout)
|
||||||
|
|
||||||
btnRefresh = gtk.MenuItem("Refresh")
|
btnRefresh = Gtk.MenuItem("Refresh")
|
||||||
btnRefresh.show()
|
btnRefresh.show()
|
||||||
# the last parameter is for not running the timer
|
# the last parameter is for not running the timer
|
||||||
btnRefresh.connect("activate", self.refresh, True, args.chrome)
|
btnRefresh.connect("activate", self.refresh, True, args.chrome)
|
||||||
self.menu.append(btnRefresh)
|
self.menu.append(btnRefresh)
|
||||||
|
|
||||||
if Version.new_available():
|
if Version.new_available():
|
||||||
btnUpdate = gtk.MenuItem("New Update Available")
|
btnUpdate = Gtk.MenuItem("New Update Available")
|
||||||
btnUpdate.show()
|
btnUpdate.show()
|
||||||
btnUpdate.connect('activate', self.showUpdate)
|
btnUpdate.connect('activate', self.showUpdate)
|
||||||
self.menu.append(btnUpdate)
|
self.menu.append(btnUpdate)
|
||||||
|
|
||||||
btnQuit = gtk.MenuItem("Quit")
|
btnQuit = Gtk.MenuItem("Quit")
|
||||||
btnQuit.show()
|
btnQuit.show()
|
||||||
btnQuit.connect("activate", self.quit)
|
btnQuit.connect("activate", self.quit)
|
||||||
self.menu.append(btnQuit)
|
self.menu.append(btnQuit)
|
||||||
|
|
||||||
self.menu.show()
|
self.menu.show()
|
||||||
|
|
||||||
self.ind.set_menu(self.menu)
|
self.ind.set_menu(self.menu)
|
||||||
self.refresh(chrome_data_directory=args.chrome, firefox_data_directory=args.firefox)
|
|
||||||
self.launch_analytics(args)
|
|
||||||
|
|
||||||
def launch_analytics(self, args):
|
if args.firefox == "auto":
|
||||||
# Now that we're all done with the boot, send a beacone home
|
args.firefox = Firefox.default_firefox_profile_path()
|
||||||
launch_data = vars(args)
|
self.refresh(chrome_data_directory=args.chrome, firefox_data_directory=args.firefox)
|
||||||
launch_data['version'] = Version.current()
|
|
||||||
launch_data['platform'] = platform.linux_distribution()
|
|
||||||
try:
|
|
||||||
launch_data['browser'] = subprocess.check_output(["xdg-settings","get","default-web-browser"]).strip()
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
launch_data['browser'] = "unknown"
|
|
||||||
self.tracker.track('launch', launch_data)
|
|
||||||
|
|
||||||
def toggleComments(self, widget):
|
def toggleComments(self, widget):
|
||||||
"""Whether comments page is opened or not"""
|
"""Whether comments page is opened or not"""
|
||||||
|
@ -120,13 +103,10 @@ class HackerNewsApp:
|
||||||
webbrowser.open(HackerNewsApp.UPDATE_URL)
|
webbrowser.open(HackerNewsApp.UPDATE_URL)
|
||||||
# Remove the update button once clicked
|
# Remove the update button once clicked
|
||||||
self.menu.remove(widget)
|
self.menu.remove(widget)
|
||||||
self.tracker.visit(HackerNewsApp.UPDATE_URL)
|
|
||||||
|
|
||||||
|
|
||||||
def showAbout(self, widget):
|
def showAbout(self, widget):
|
||||||
"""Handle the about btn"""
|
"""Handle the about btn"""
|
||||||
webbrowser.open(HackerNewsApp.ABOUT_URL)
|
webbrowser.open(HackerNewsApp.ABOUT_URL)
|
||||||
self.tracker.visit(HackerNewsApp.ABOUT_URL)
|
|
||||||
|
|
||||||
# ToDo: Handle keyboard interrupt properly
|
# ToDo: Handle keyboard interrupt properly
|
||||||
def quit(self, widget, data=None):
|
def quit(self, widget, data=None):
|
||||||
|
@ -138,12 +118,11 @@ class HackerNewsApp:
|
||||||
with open(home + '/.hackertray.json', 'w+') as file:
|
with open(home + '/.hackertray.json', 'w+') as file:
|
||||||
file.write(json.dumps(l))
|
file.write(json.dumps(l))
|
||||||
|
|
||||||
gtk.main_quit()
|
Gtk.main_quit()
|
||||||
self.tracker.track('quit')
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
signal.signal(signal.SIGINT, self.quit)
|
signal.signal(signal.SIGINT, self.quit)
|
||||||
gtk.main()
|
Gtk.main()
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def open(self, widget, event=None, data=None):
|
def open(self, widget, event=None, data=None):
|
||||||
|
@ -160,7 +139,6 @@ class HackerNewsApp:
|
||||||
|
|
||||||
if self.commentState:
|
if self.commentState:
|
||||||
webbrowser.open(self.HN_URL_PREFIX + str(widget.hn_id))
|
webbrowser.open(self.HN_URL_PREFIX + str(widget.hn_id))
|
||||||
self.tracker.visit(widget.url)
|
|
||||||
|
|
||||||
def addItem(self, item):
|
def addItem(self, item):
|
||||||
"""Adds an item to the menu"""
|
"""Adds an item to the menu"""
|
||||||
|
@ -168,7 +146,7 @@ class HackerNewsApp:
|
||||||
if item['points'] == 0 or item['points'] is None:
|
if item['points'] == 0 or item['points'] is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
i = gtk.CheckMenuItem(
|
i = Gtk.CheckMenuItem(
|
||||||
"(" + str(item['points']).zfill(3) + "/" + str(item['comments_count']).zfill(3) + ") " + item['title'])
|
"(" + str(item['points']).zfill(3) + "/" + str(item['comments_count']).zfill(3) + ") " + item['title'])
|
||||||
|
|
||||||
visited = item['history'] or item['id'] in self.db
|
visited = item['history'] or item['id'] in self.db
|
||||||
|
@ -184,7 +162,6 @@ class HackerNewsApp:
|
||||||
i.show()
|
i.show()
|
||||||
|
|
||||||
def refresh(self, widget=None, no_timer=False, chrome_data_directory=None, firefox_data_directory=None):
|
def refresh(self, widget=None, no_timer=False, chrome_data_directory=None, firefox_data_directory=None):
|
||||||
|
|
||||||
"""Refreshes the menu """
|
"""Refreshes the menu """
|
||||||
try:
|
try:
|
||||||
# Create an array of 20 false to denote matches in History
|
# Create an array of 20 false to denote matches in History
|
||||||
|
@ -211,11 +188,11 @@ class HackerNewsApp:
|
||||||
self.addItem(item)
|
self.addItem(item)
|
||||||
# Catch network errors
|
# Catch network errors
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
print "[+] There was an error in fetching news items"
|
print("[+] There was an error in fetching news items")
|
||||||
finally:
|
finally:
|
||||||
# Call every 10 minutes
|
# Call every 10 minutes
|
||||||
if not no_timer:
|
if not no_timer:
|
||||||
gtk.timeout_add(10 * 30 * 1000, self.refresh, widget, no_timer, chrome_data_directory)
|
GLib.timeout_add(10 * 30 * 1000, self.refresh, widget, no_timer, chrome_data_directory)
|
||||||
|
|
||||||
# Merges two boolean arrays, using OR operation against each pair
|
# Merges two boolean arrays, using OR operation against each pair
|
||||||
def mergeBoolArray(self, original, patch):
|
def mergeBoolArray(self, original, patch):
|
||||||
|
@ -223,13 +200,13 @@ class HackerNewsApp:
|
||||||
original[index] = original[index] or patch[index]
|
original[index] = original[index] or patch[index]
|
||||||
return original
|
return original
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description='Hacker News in your System Tray')
|
parser = argparse.ArgumentParser(description='Hacker News in your System Tray')
|
||||||
parser.add_argument('-v', '--version', action='version', version=Version.current())
|
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")
|
parser.add_argument('-c', '--comments', dest='comments', action='store_true', help="Load the HN comments link for the article as well")
|
||||||
parser.add_argument('--chrome', dest='chrome', help="Specify a Google Chrome Profile directory to use for matching chrome history")
|
parser.add_argument('--chrome', dest='chrome', help="Specify a Google Chrome Profile directory to use for matching chrome history")
|
||||||
parser.add_argument('--firefox', dest='firefox', help="Specify a Firefox Profile directory to use for matching firefox history")
|
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")
|
||||||
parser.add_argument('--dnt', dest='dnt',action='store_true', help="Disable all analytics (Do Not Track)")
|
|
||||||
parser.set_defaults(comments=False)
|
parser.set_defaults(comments=False)
|
||||||
parser.set_defaults(dnt=False)
|
parser.set_defaults(dnt=False)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
from mixpanel import Mixpanel
|
|
||||||
|
|
||||||
class Analytics:
|
|
||||||
# Setup analytics.
|
|
||||||
# dnt - do not track. Disables tracking if True
|
|
||||||
# token - The mixpanel token
|
|
||||||
def __init__(self, dnt, token):
|
|
||||||
self.dnt = dnt
|
|
||||||
self.tracker = Mixpanel(token)
|
|
||||||
if(self.dnt == True):
|
|
||||||
print "[+] Analytics disabled"
|
|
||||||
# Track an event
|
|
||||||
# event - string containing the event name
|
|
||||||
# data - data related to the event, defaults to {}
|
|
||||||
def track(self, event, data = {}):
|
|
||||||
if(self.dnt == False):
|
|
||||||
# All events are tracked anonymously
|
|
||||||
self.tracker.track("anonymous", event, data)
|
|
||||||
# Track a visit to a URL
|
|
||||||
# The url maybe an HN submission or
|
|
||||||
# some meta-url pertaining to hackertray
|
|
||||||
def visit(self, url):
|
|
||||||
self.track('visit', {"link":url})
|
|
|
@ -9,8 +9,7 @@
|
||||||
#=========================
|
#=========================
|
||||||
|
|
||||||
# We require PyGTK
|
# We require PyGTK
|
||||||
import gtk
|
from gi.repository import Gtk,GLib
|
||||||
import gobject
|
|
||||||
|
|
||||||
# We also need os and sys
|
# We also need os and sys
|
||||||
import os
|
import os
|
||||||
|
@ -35,13 +34,14 @@ def get_icon_filename(icon_name):
|
||||||
# The main class
|
# The main class
|
||||||
class Indicator:
|
class Indicator:
|
||||||
# Constructor
|
# Constructor
|
||||||
|
|
||||||
def __init__(self, unknown, icon, category):
|
def __init__(self, unknown, icon, category):
|
||||||
# Store the settings
|
# Store the settings
|
||||||
self.inactive_icon = get_icon_filename(icon)
|
self.inactive_icon = get_icon_filename(icon)
|
||||||
self.active_icon = "" # Blank until the user calls set_attention_icon
|
self.active_icon = "" # Blank until the user calls set_attention_icon
|
||||||
|
|
||||||
# Create the status icon
|
# Create the status icon
|
||||||
self.icon = gtk.StatusIcon()
|
self.icon = Gtk.StatusIcon()
|
||||||
|
|
||||||
# Initialize to the default icon
|
# Initialize to the default icon
|
||||||
self.icon.set_from_file(self.inactive_icon)
|
self.icon.set_from_file(self.inactive_icon)
|
||||||
|
@ -80,7 +80,7 @@ class Indicator:
|
||||||
|
|
||||||
def show_menu(self, widget):
|
def show_menu(self, widget):
|
||||||
# Show the menu
|
# Show the menu
|
||||||
self.menu.popup(None, None, None, 0, 0)
|
self.menu.popup(None, None, None, 0, 0, Gtk.get_current_event_time())
|
||||||
|
|
||||||
# Get the location and size of the window
|
# Get the location and size of the window
|
||||||
mouse_rect = self.menu.get_window().get_frame_extents()
|
mouse_rect = self.menu.get_window().get_frame_extents()
|
||||||
|
@ -91,7 +91,7 @@ class Indicator:
|
||||||
self.bottom = self.y + mouse_rect.height
|
self.bottom = self.y + mouse_rect.height
|
||||||
|
|
||||||
# Set a timer to poll the menu
|
# Set a timer to poll the menu
|
||||||
self.timer = gobject.timeout_add(100, self.check_mouse)
|
self.timer = GLib.timeout_add(100, self.check_mouse)
|
||||||
|
|
||||||
def check_mouse(self):
|
def check_mouse(self):
|
||||||
if not self.menu.get_window().is_visible():
|
if not self.menu.get_window().is_visible():
|
||||||
|
@ -100,9 +100,9 @@ class Indicator:
|
||||||
# Now check the global mouse coords
|
# Now check the global mouse coords
|
||||||
root = self.menu.get_screen().get_root_window()
|
root = self.menu.get_screen().get_root_window()
|
||||||
|
|
||||||
x, y, z = root.get_pointer()
|
_,x,y,_ = root.get_pointer()
|
||||||
|
|
||||||
if x < self.x or x > self.right or y < self.y or y > self.bottom:
|
if (x < (self.x-10)) or (x > self.right) or (y < (self.y+10)) or (y > self.bottom):
|
||||||
self.hide_menu()
|
self.hide_menu()
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
from __future__ import print_function
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import shutil
|
import shutil
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
class Chrome:
|
class Chrome:
|
||||||
HISTORY_TMP_LOCATION = '/tmp/hackertray.chrome'
|
HISTORY_TMP_LOCATION = '/tmp/hackertray.chrome'
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def search(urls, config_folder_path):
|
def search(urls, config_folder_path):
|
||||||
Chrome.setup(config_folder_path)
|
Chrome.setup(config_folder_path)
|
||||||
|
@ -20,6 +22,7 @@ class Chrome:
|
||||||
result.append(True)
|
result.append(True)
|
||||||
os.remove(Chrome.HISTORY_TMP_LOCATION)
|
os.remove(Chrome.HISTORY_TMP_LOCATION)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def setup(config_folder_path):
|
def setup(config_folder_path):
|
||||||
file_name = os.path.abspath(config_folder_path+'/History')
|
file_name = os.path.abspath(config_folder_path+'/History')
|
||||||
|
|
|
@ -1,12 +1,33 @@
|
||||||
from __future__ import print_function
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import shutil
|
import shutil
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
import configparser
|
||||||
|
|
||||||
class Firefox:
|
class Firefox:
|
||||||
HISTORY_TMP_LOCATION = '/tmp/hackertray.firefox'
|
HISTORY_TMP_LOCATION = '/tmp/hackertray.firefox'
|
||||||
HISTORY_FILE_NAME = '/places.sqlite'
|
HISTORY_FILE_NAME = '/places.sqlite'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def default_firefox_profile_path():
|
||||||
|
profile_file_path = str(Path.home().joinpath(".mozilla/firefox/profiles.ini"))
|
||||||
|
profile_path = None
|
||||||
|
if (os.path.exists(profile_file_path)):
|
||||||
|
parser = configparser.ConfigParser()
|
||||||
|
parser.read(profile_file_path)
|
||||||
|
for section in parser.sections():
|
||||||
|
if parser.has_option(section,"Default") and parser[section]["Default"] == "1":
|
||||||
|
if parser.has_option(section,"IsRelative") and parser[section]["IsRelative"] == "1":
|
||||||
|
profile_path = str(Path.home().joinpath(".mozilla/firefox/").joinpath(parser[section]["Path"]))
|
||||||
|
else:
|
||||||
|
profile_path = parser[section]["Path"]
|
||||||
|
if profile_path and Path.is_dir(Path(profile_path)):
|
||||||
|
return profile_path
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Couldn't find default Firefox profile")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def search(urls, config_folder_path):
|
def search(urls, config_folder_path):
|
||||||
Firefox.setup(config_folder_path)
|
Firefox.setup(config_folder_path)
|
||||||
|
@ -21,10 +42,11 @@ class Firefox:
|
||||||
result.append(True)
|
result.append(True)
|
||||||
os.remove(Firefox.HISTORY_TMP_LOCATION)
|
os.remove(Firefox.HISTORY_TMP_LOCATION)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def setup(config_folder_path):
|
def setup(config_folder_path):
|
||||||
file_name = os.path.abspath(config_folder_path + Firefox.HISTORY_FILE_NAME)
|
file_name = os.path.abspath(config_folder_path + Firefox.HISTORY_FILE_NAME)
|
||||||
if not os.path.isfile(file_name):
|
if not os.path.isfile(file_name):
|
||||||
print("ERROR: ", "Could not find Firefox history file", file=sys.stderr)
|
print("ERROR: Could not find Firefox history file, using %s" % file_name)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
shutil.copyfile(file_name, Firefox.HISTORY_TMP_LOCATION)
|
shutil.copyfile(file_name, Firefox.HISTORY_TMP_LOCATION)
|
|
@ -7,6 +7,7 @@ urls = [
|
||||||
|
|
||||||
|
|
||||||
class HackerNews:
|
class HackerNews:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def getHomePage():
|
def getHomePage():
|
||||||
random.shuffle(urls)
|
random.shuffle(urls)
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import requests
|
import requests
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
|
||||||
|
|
||||||
class Version:
|
class Version:
|
||||||
PYPI_URL = "https://pypi.python.org/pypi/hackertray/json"
|
PYPI_URL = "https://pypi.python.org/pypi/hackertray/json"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def latest():
|
def latest():
|
||||||
res = requests.get(Version.PYPI_URL).json()
|
res = requests.get(Version.PYPI_URL).json()
|
||||||
|
@ -18,10 +20,10 @@ class Version:
|
||||||
current = Version.current()
|
current = Version.current()
|
||||||
try:
|
try:
|
||||||
if pkg_resources.parse_version(latest) > pkg_resources.parse_version(current):
|
if pkg_resources.parse_version(latest) > pkg_resources.parse_version(current):
|
||||||
print "[+] New version " + latest + " is available"
|
print("[+] New version " + latest + " is available")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
print "[+] There was an error in trying to fetch updates"
|
print("[+] There was an error in trying to fetch updates")
|
||||||
return False
|
return False
|
14
setup.py
14
setup.py
|
@ -3,18 +3,15 @@ import sys
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
from setuptools import find_packages
|
from setuptools import find_packages
|
||||||
|
|
||||||
|
|
||||||
requirements = ['requests']
|
requirements = ['requests']
|
||||||
if sys.version_info < (2, 7):
|
|
||||||
requirements.append('argparse')
|
|
||||||
|
|
||||||
setup(name='hackertray',
|
setup(name='hackertray',
|
||||||
version='3.0.0',
|
version='4.0.0',
|
||||||
description='Hacker News app that sits in your System Tray',
|
description='Hacker News app that sits in your System Tray',
|
||||||
long_description='HackerTray is a simple Hacker News Linux application that lets you view top HN stories in your System Tray. It relies on appindicator, so it is not guaranteed to work on all systems. It also provides a Gtk StatusIcon fallback in case AppIndicator is not available.',
|
long_description='HackerTray is a simple Hacker News Linux application that lets you view top HN stories in your System Tray. It supports appindicator and falls back to Gtk StatusIcon otherwise.',
|
||||||
keywords='hacker news hn tray system tray icon hackertray',
|
keywords='hacker news hn tray system tray icon hackertray',
|
||||||
url='http://captnemo.in/hackertray',
|
url='https://captnemo.in/hackertray',
|
||||||
author='Abhay Rana',
|
author='Abhay Rana (Nemo)',
|
||||||
author_email='me@captnemo.in',
|
author_email='me@captnemo.in',
|
||||||
license='MIT',
|
license='MIT',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
|
@ -22,8 +19,7 @@ setup(name='hackertray',
|
||||||
'hackertray.data': ['hacker-tray.png']
|
'hackertray.data': ['hacker-tray.png']
|
||||||
},
|
},
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'requests>=2.2.1',
|
'requests>=2.23.0'
|
||||||
'mixpanel-py>=3.0.0'
|
|
||||||
],
|
],
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': ['hackertray = hackertray:main'],
|
'console_scripts': ['hackertray = hackertray:main'],
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import unittest
|
import unittest
|
||||||
import os
|
import os
|
||||||
|
import pathlib
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from hackertray import Firefox
|
from hackertray import Firefox
|
||||||
|
|
||||||
class ChromeTest(unittest.TestCase):
|
class FirefoxTest(unittest.TestCase):
|
||||||
def runTest(self):
|
def test_history(self):
|
||||||
config_folder_path = os.getcwd()+'/test/'
|
config_folder_path = os.getcwd()+'/test/'
|
||||||
data = Firefox.search([
|
data = Firefox.search([
|
||||||
"http://www.hckrnews.com/",
|
"http://www.hckrnews.com/",
|
||||||
|
@ -13,3 +15,18 @@ class ChromeTest(unittest.TestCase):
|
||||||
"http://invalid_url/"],
|
"http://invalid_url/"],
|
||||||
config_folder_path)
|
config_folder_path)
|
||||||
self.assertTrue(data == [True,True,True,False])
|
self.assertTrue(data == [True,True,True,False])
|
||||||
|
|
||||||
|
def test_default(self):
|
||||||
|
test_default_path = Path.home().joinpath(".mozilla/firefox/x0ran0o9.default")
|
||||||
|
if(os.environ.get('TRAVIS') == 'true'):
|
||||||
|
if not os.path.exists(str(test_default_path)):
|
||||||
|
os.makedirs(str(test_default_path))
|
||||||
|
with open(str(Path.home().joinpath('.mozilla/firefox/profiles.ini')), 'w') as f:
|
||||||
|
f.write("""
|
||||||
|
[Profile1]
|
||||||
|
Name=default
|
||||||
|
IsRelative=1
|
||||||
|
Path=x0ran0o9.default
|
||||||
|
Default=1
|
||||||
|
""")
|
||||||
|
self.assertTrue(Firefox.default_firefox_profile_path()==str(Path.home().joinpath(".mozilla/firefox/x0ran0o9.default")))
|
||||||
|
|
Loading…
Reference in New Issue