Compare commits

...

56 Commits

Author SHA1 Message Date
Nemo ef43ce5942
Create FUNDING.yml 2022-05-30 14:53:02 +05:30
Nemo 52a2dbc94b Adds complete MIT License 2021-03-19 20:59:30 +05:30
Nemo be8b61530f [doc] Adds note about --reverse flag 2020-06-16 03:35:57 +05:30
Nemo 249f3f1b84 Version Bump (4.0.2) 2020-06-15 18:09:14 +05:30
Nemo b751bcd74c Switches to a label to make points take fixed-width 2020-06-15 18:09:11 +05:30
Nemo 30a38bd769 Adds a --reverse flag for users with status bar at bottom 2020-06-15 18:09:08 +05:30
Nemo a78796e029 Fixes hover boundaries
Closes #33

This removes the hover boundaries entirely, so you must click elsewhere
to close the menu now, which I think is better behaviour
2020-06-15 18:09:04 +05:30
Nemo c10614c2bd
Merge pull request #32 from captn3m0/coveralls
[ci] Adds coveralls coverage tracking
2020-06-15 03:27:13 +05:30
Nemo 5736b65bb4 [ci] Adds coveralls 2020-06-15 03:16:01 +05:30
Nemo 3117a7948e Minor README updates for v4 2020-06-15 01:41:37 +05:30
Nemo 0a48248739 Do a minor 4.0.1 release 2020-06-15 01:35:07 +05:30
Nemo 74d26ccffa Change comments to a radio button 2020-06-15 01:34:21 +05:30
Nemo 64cc856ed1
Merge pull request #30 from captn3m0/python3
Python 3 and v4.0.0
2020-06-15 00:47:22 +05:30
Nemo a37ef7236b [tests] Fix tests on Travis 2020-06-15 00:44:33 +05:30
Nemo 1a29800787 Version Bump 2020-06-15 00:27:10 +05:30
Nemo f1141b2335 [docs] Update docs for new release 2020-06-15 00:20:14 +05:30
Nemo ff57182c3c Support default firefox profile 2020-06-15 00:17:04 +05:30
Nemo 01ad187150 Finish PyGtk upgrade (7 years in the making) 2020-06-15 00:03:57 +05:30
Nemo f68ea6e9c4 Adds support for default firefox profile 2020-06-15 00:03:39 +05:30
Nemo 6abaa46c8f Removes tracking completely 2020-06-14 22:38:14 +05:30
Nemo 3afba6f3eb pep8 changes 2020-06-14 22:34:37 +05:30
Nemo e34d767ef0 [ci] Adds python3 in travis 2020-06-14 22:34:37 +05:30
Nemo 678b0090c4 Upgrade to Python 3 2020-06-14 22:34:37 +05:30
Nemo 297ef91497 Removes gratipay links 2018-07-31 08:02:52 +05:30
Nemo fbd2227a3c Merge pull request #29 from artiya4u/master
Fix cannot open "Ask HN" stories. #28
2017-10-04 22:16:58 +05:30
Artiya T e81b98929b Fix cannot open "Ask HN" stories. 2017-10-03 19:07:03 +07:00
Santiago Castro 97c3414cb0 Fix broken Markdown headings 2017-04-24 13:47:26 +05:30
Abhay Rana aef61a556c Updates README (gittip->gratipay)
- Also renames PROFILE_PATH to PROFILE-PATH
2014-10-14 15:22:58 +05:30
Abhay Rana 8f17672014 Adds a changelog. 2014-10-03 19:21:05 +05:30
Abhay Rana a1b500e58d Bumps version to 3.0.0 2014-10-03 19:13:08 +05:30
Abhay Rana 591f6454f2 Merge branch 'analytics'
Conflicts:
	README.md
	hackertray/__init__.py
2014-10-03 19:11:49 +05:30
Abhay Rana 4224c9c198 Merge branch 'firefox'
Conflicts:
	hackertray/chrome.py
2014-10-03 19:10:06 +05:30
Abhay Rana c7a1c653d9 Uses constants in chrome config 2014-10-03 19:09:35 +05:30
Abhay Rana a5a4b54d97 Adds firefox flag instructions and notes to readme 2014-10-03 19:08:10 +05:30
Abhay Rana 6255917af4 Adds editorconfig to take care of indentation. 2014-10-03 18:58:04 +05:30
Abhay Rana bd322e5191 Uses firefox module in the main app. 2014-10-03 18:57:42 +05:30
Abhay Rana 0b0b5c92b8 Fixes some indentation in chrome module 2014-10-03 16:26:35 +05:30
Abhay Rana c7cea3d278 Adds firefox module that checks urls 2014-10-03 16:26:20 +05:30
Abhay Rana 0c314e690b Fixes a minor bug with the visit event
- Analytics is no longer static.
2014-10-02 13:28:39 +05:30
Abhay Rana 4edb2a2053 Fixes link to analytics wiki page 2014-10-01 16:25:07 +05:30
Abhay Rana b9ebeaebce Tracks url visit event 2014-10-01 07:44:32 +05:30
Abhay Rana 96a2364c04 Adds information about --dnt switch 2014-10-01 06:51:29 +05:30
Abhay Rana 5a73dd9d93 Adds analytics note to README 2014-10-01 06:40:05 +05:30
Abhay Rana 11c02ba898 Adds browser and platform info to analytics 2014-10-01 00:38:25 +05:30
Abhay Rana 6c93751d73 Fixes travis build by installing mixpanel 2014-10-01 00:14:23 +05:30
Abhay Rana 93c9647b3b Adds basic analytics framework (mixpanel)
- Tracking launch event, with arguments passed
2014-09-30 23:30:36 +05:30
Abhay Rana d9e6e51722 Adds tooltips to each menu item containing URL, timestamp and user info 2014-09-27 10:11:32 +05:30
Abhay Rana c4b195af3d Disables travis success emails 2014-09-27 09:35:44 +05:30
Abhay Rana ecd2011875 Bumps version 2014-09-27 09:24:22 +05:30
Abhay Rana 607a9804f3 Merge branch 'proxy'
Fixes #22
2014-09-27 09:22:46 +05:30
Abhay Rana 279034854c Removes a debug print statement.
- Bumps version to 2.3.1
2014-09-25 22:25:47 +05:30
Abhay Rana 59def73822 Bumps version. 2014-09-25 22:23:09 +05:30
Abhay Rana d2246edd4f Updates README to reflect update checker. 2014-09-25 22:22:02 +05:30
Abhay Rana 76b64e7fb6 Adds update checking feature.
- Uses the PyPi json API
- Slows down the boot a bit.
- Creates hard dependency on setuptools (which was earlier optional)
2014-09-25 22:20:45 +05:30
Abhay Rana 912aa745d8 Bumps version 2014-09-24 12:40:10 +05:30
Abhay Rana 0b1c1bf2ad Upgrades requests version to get HTTPS Proxy support 2014-06-12 01:53:58 +05:30
18 changed files with 415 additions and 171 deletions

19
.editorconfig Normal file
View File

@ -0,0 +1,19 @@
# EditorConfig is awesome: http://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
# 4 space indentation
[*.py]
indent_style = space
indent_size = 4
# Matches the exact files either package.json or .travis.yml
[{package.json,.travis.yml}]
indent_style = space
indent_size = 2

3
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,3 @@
ko_fi: captn3m0
liberapay: captn3m0
github: captn3m0

6
.gitignore vendored
View File

@ -17,4 +17,8 @@ var/
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
pip-delete-this-directory.txt
pyvenv.cfg
bin/
.coverage

View File

@ -1,9 +1,24 @@
language: 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
install:
install:
- pip install requests
- pip install nose
- pip install coverage
- pip install coveralls
# command to run tests, e.g. python setup.py test
script: nosetests --nocapture
script: coverage run --source=hackertray $(which nosetests)
after_success: coveralls
notifications:
email:
on_success: never
on_failure: always

46
CHANGELOG.md Normal file
View File

@ -0,0 +1,46 @@
This file will only list released and supported versions, usually skipping over very minor updates.
Unreleased
==========
4.0.2
=====
* Adds a --reverse flag for users with bar at the bottom of their screen
* Uses markup to keep points in fixed-width, so titles are more readable
* Removes the buggy hover-out behaviour (non-appindicator). You now need to click elsewhere to close the menu
4.0.1
=====
* Changes "Show Comments" entry to a radio menu item
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
* Removed all MixPanel tracking.
3.0.0
=====
* Oct 3, 2014
* Major release.
* Firefox support behind `--firefox` flag
* Analytics support. Can be disabled using `--dnt` flag
* Hovering now shows url, timestamp, and uploader nick
2.3.2
=====
* Sep 27, 2014
* Adds proxy support
2.2.0
=====
* Adds support for using chrome history behind the `--chrome` flag.

7
LICENSE Normal file
View File

@ -0,0 +1,7 @@
Copyright 2021 Abhay Rana
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

117
README.md
View File

@ -1,25 +1,24 @@
HackerTray
==========
# HackerTray
[![HackerTray on PyPi](https://pypip.in/v/hackertray/badge.png)](https://pypi.python.org/pypi/hackertray/)
[![HackerTray on PyPi](https://pypip.in/d/hackertray/badge.png)](https://pypi.python.org/pypi/hackertray/)
[![Build Status](https://travis-ci.org/captn3m0/hackertray.png)](https://travis-ci.org/captn3m0/hackertray)
[![Build Status](https://travis-ci.org/captn3m0/hackertray.png)](https://travis-ci.org/captn3m0/hackertray) [![Coverage Status](https://coveralls.io/repos/github/captn3m0/hackertray/badge.svg?branch=master)](https://coveralls.io/github/captn3m0/hackertray?branch=master)
HackerTray is a simple [Hacker News](https://news.ycombinator.com/) 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.
that lets you view top HN stories in your System Tray. It uses appindicator where available,
but provides a Gtk StatusIcon fallback in case AppIndicator is not available.
The inspiration for this came from [Hacker Bar](http://hackerbarapp.com), which is Mac-only.
##Screenshot
## Screenshot
![HackerTray Screenshot in elementaryOS](http://i.imgur.com/63l3qXV.png)
##Installation
## Installation
HackerTray is distributed as a python package. Do the following to install:
``` sh
```sh
sudo pip install hackertray
OR
sudo easy_install hackertray
@ -30,78 +29,92 @@ sudo python setup.py install
After that, you can run `hackertray` from anywhere and it will run. You can
now add it to your OS dependent session autostart method. In Ubuntu, you can
access it via:
access it via:
1. System > Preferences > Sessions
(OR)
2. System > Preferences > Startup Applications
1. System > Preferences > Sessions
(OR)
2. System > Preferences > Startup Applications
depending on your Ubuntu Version. Or put it in `~/.config/openbox/autostart`
if you are running OpenBox. [Here](http://imgur.com/mnhIzDK) is how the
configuration should look like in Ubuntu and its derivatives.
depending on your Ubuntu Version. Or put it in `~/.config/openbox/autostart`
if you are running OpenBox. [Here](http://imgur.com/mnhIzDK) is how the
configuration should look like in Ubuntu and its derivatives.
###Upgrade
The latest stable version is always the one [available on pip](https://pypi.python.org/pypi/hackertray/).
You can check which version you have installed with `pip freeze | grep hackertray`.
### Upgrade
The latest stable version is [![the one on PyPi](https://pypip.in/v/hackertray/badge.png)](https://pypi.python.org/pypi/hackertray/)
You can check which version you have installed with `hackertray --version`.
To upgrade, run `pip install -U hackertray`. In some cases (Ubuntu), you might
need to clear the pip cache before upgrading:
`sudo rm -rf /tmp/pip-build-root/hackertray`
##Options
HackerTray will automatically check the latest version on startup, and inform you if there is an update available.
## Options
HackerTray accepts its various options via the command line. Run `hackertray -h` to see all options. Currently the following switches are supported:
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.
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.
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. `--reverse` (or `-r`). Switches the order for the elements in the menu, so Quit is at top. Use this if your system bar is at the bottom of the screen.
###Google Chrome Profile Path
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).
Where your Profile is stored depends on which version of chrome you are using:
### Google Chrome Profile Path
- `google-chrome-stable`: `~/.config/google-chrome/Default/`
- `google-chrome-unstable`: `~/.config/google-chrome-unstable/Default/`
- `chromium`: `~/.config/chromium/Default/`
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):
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/`).
- [Chrome Stable] `~/.config/google-chrome/Default`
- [Chrome Beta] `~/.config/google-chrome-beta/Default`
- [Chrome Dev] `~/.config/google-chrome-unstable/Default`
- [Chromium] `~/.config/chromium/Default`
##Features
1. Minimalist Approach to HN
2. Opens links in your default browser
3. Remembers which links you opened
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)
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/`).
###Troubleshooting
### Firefox Profile Path
If the app indicator fails to show in Ubuntu versions, consider installing
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
1. Minimalist Approach to HN
2. Opens links in your default browser
3. Remembers which links you opened, even if you opened them outside of HackerTray
4. Shows Points/Comment count in a simple format
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
If the app indicator fails to show in Ubuntu versions, consider installing
python-appindicator with
`sudo apt-get install python-appindicator`
###Development
Note that appindicator is no longer supported in non-Ubuntu distros, because it only works on Python2.
### Development
To develop on hackertray, or to test out experimental versions, do the following:
- Clone the project
- Run `(sudo) python setup.py develop` in the hackertray root directory
- Run `hackertray` with the required command line options from anywhere.
- Clone the project
- Run `(sudo) python setup.py develop` in the hackertray root directory
- Run `hackertray` with the required command line options from anywhere.
##Credits
## Analytics
- Mark Rickert for [Hacker Bar](http://hackerbarapp.com/).
- [Giridaran Manivannan](https://github.com/ace03uec) for troubleshooting instructions.
On every launch, a request is made to `https://pypi.python.org/pypi/hackertray/json` to check the latest version.
##Author Information
- Abhay Rana (<me@captnemo.in>)
**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 (Version `< 4.0.0`).
## Donating
Support this project and [others by captn3m0][gittip] via [gittip][].
## Credits
[![Support via Gittip][gittip-badge]][gittip]
- Mark Rickert for [Hacker Bar](http://hackerbarapp.com/) (No longer active)
- [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/)
[gittip-badge]: https://rawgithub.com/twolfson/gittip-badge/master/dist/gittip.png
[gittip]: https://www.gittip.com/captn3m0/
## Licence
##Licence
Licenced under the [MIT Licence](http://nemo.mit-license.org/).
Licenced under the [MIT Licence](https://nemo.mit-license.org/). See the LICENSE file for complete license text.

View File

@ -2,43 +2,39 @@
import os
import requests
import subprocess
if(os.environ.get('TRAVIS')!='true'):
import pygtk
pygtk.require('2.0')
import gtk
if(os.environ.get('TRAVIS') != 'true'):
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk,GLib
import webbrowser
try:
import appindicator
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 argparse
from os.path import expanduser
import signal
##This is to get --version to work
try:
import pkg_resources
from .hackernews import HackerNews
from .chrome import Chrome
from .firefox import Firefox
from .version import Version
__version = pkg_resources.require("hackertray")[0].version
except:
__version = "Can't read version number."
from hackernews import HackerNews
from chrome import Chrome
class HackerNewsApp:
HN_URL_PREFIX = "https://news.ycombinator.com/item?id="
UPDATE_URL = "https://github.com/captn3m0/hackertray#upgrade"
ABOUT_URL = "https://github.com/captn3m0/hackertray"
def __init__(self, args):
#Load the database
# Load the database
home = expanduser("~")
with open(home + '/.hackertray.json', 'a+') as content_file:
content_file.seek(0)
@ -54,72 +50,93 @@ class HackerNewsApp:
self.ind.set_icon(get_icon_filename("hacker-tray.png"))
# 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.reverse = args.reverse
# create items for the menu - refresh, quit and a separator
menuSeparator = gtk.SeparatorMenuItem()
menuSeparator = Gtk.SeparatorMenuItem()
menuSeparator.show()
self.menu.append(menuSeparator)
self.add(menuSeparator)
btnComments = gtk.CheckMenuItem("Show Comments")
btnComments = Gtk.CheckMenuItem("Show Comments")
btnComments.show()
btnComments.set_active(args.comments)
btnComments.set_draw_as_radio(True)
btnComments.connect("activate", self.toggleComments)
self.menu.append(btnComments)
self.add(btnComments)
btnAbout = gtk.MenuItem("About")
btnAbout = Gtk.MenuItem("About")
btnAbout.show()
btnAbout.connect("activate", self.showAbout)
self.menu.append(btnAbout)
self.add(btnAbout)
btnRefresh = gtk.MenuItem("Refresh")
btnRefresh = Gtk.MenuItem("Refresh")
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)
self.menu.append(btnRefresh)
self.add(btnRefresh)
btnQuit = gtk.MenuItem("Quit")
if Version.new_available():
btnUpdate = Gtk.MenuItem("New Update Available")
btnUpdate.show()
btnUpdate.connect('activate', self.showUpdate)
self.add(btnUpdate)
btnQuit = Gtk.MenuItem("Quit")
btnQuit.show()
btnQuit.connect("activate", self.quit)
self.menu.append(btnQuit)
self.add(btnQuit)
self.menu.show()
self.ind.set_menu(self.menu)
self.refresh(chrome_data_directory=args.chrome)
if args.firefox == "auto":
args.firefox = Firefox.default_firefox_profile_path()
self.refresh(chrome_data_directory=args.chrome, firefox_data_directory=args.firefox)
def add(self, item):
if self.reverse:
self.menu.prepend(item)
else:
self.menu.append(item)
def toggleComments(self, widget):
"""Whether comments page is opened or not"""
self.commentState = not self.commentState
def showUpdate(self, widget):
"""Handle the update button"""
webbrowser.open(HackerNewsApp.UPDATE_URL)
# Remove the update button once clicked
self.menu.remove(widget)
def showAbout(self, widget):
"""Handle the about btn"""
webbrowser.open("https://github.com/captn3m0/hackertray/")
webbrowser.open(HackerNewsApp.ABOUT_URL)
#ToDo: Handle keyboard interrupt properly
# ToDo: Handle keyboard interrupt properly
def quit(self, widget, data=None):
""" Handler for the quit button"""
l = list(self.db)
home = expanduser("~")
#truncate the file
# truncate the file
with open(home + '/.hackertray.json', 'w+') as file:
file.write(json.dumps(l))
gtk.main_quit()
Gtk.main_quit()
def run(self):
signal.signal(signal.SIGINT, self.quit)
gtk.main()
Gtk.main()
return 0
def open(self, widget, event=None, data=None):
def open(self, widget, **args):
"""Opens the link in the web browser"""
#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
# 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
if not widget.get_active():
widget.disconnect(widget.signal_id)
widget.set_active(True)
@ -128,65 +145,86 @@ class HackerNewsApp:
self.db.add(widget.item_id)
webbrowser.open(widget.url)
# TODO: Add support for Shift+Click or Right Click
# to do the opposite of the current commentState setting
if self.commentState:
webbrowser.open(self.HN_URL_PREFIX + widget.hn_id)
webbrowser.open(self.HN_URL_PREFIX + str(widget.hn_id))
def addItem(self, item):
"""Adds an item to the menu"""
#This is in the case of YC Job Postings, which we skip
# This is in the case of YC Job Postings, which we skip
if item['points'] == 0 or item['points'] is None:
return
i = gtk.CheckMenuItem(
"(" + str(item['points']).zfill(3) + "/" + str(item['comments_count']).zfill(3) + ") " + item['title'])
points = str(item['points']).zfill(3) + "/" + str(item['comments_count']).zfill(3)
i = Gtk.CheckMenuItem.new_with_label(label="(" + points + ")"+item['title'])
label = i.get_child()
label.set_markup("<tt>" + points + "</tt> <span>"+item['title']+"</span>".format(points=points, title=item['title']))
label.set_selectable(False)
visited = item['history'] or item['id'] in self.db
i.set_active(visited)
i.url = item['url']
tooltip = "{url}\nPosted by {user} {timeago}".format(url=item['url'], user=item['user'], timeago=item['time_ago'])
i.set_tooltip_text(tooltip)
i.signal_id = i.connect('activate', self.open)
i.hn_id = item['id']
i.item_id = item['id']
self.menu.prepend(i)
if self.reverse:
self.menu.append(i)
else:
self.menu.prepend(i)
i.show()
def refresh(self, widget=None, no_timer=False, chrome_data_directory=None):
def refresh(self, widget=None, no_timer=False, chrome_data_directory=None, firefox_data_directory=None):
"""Refreshes the menu """
try:
# Create an array of 20 false to denote matches in History
searchResults = [False]*20
data = list(reversed(HackerNews.getHomePage()[0:20]))
urls = [item['url'] for item in data]
if(chrome_data_directory):
urls = [item['url'] for item in data]
searchResults = Chrome.search(urls, chrome_data_directory)
searchResults = self.mergeBoolArray(searchResults, Chrome.search(urls, chrome_data_directory))
#Remove all the current stories
if(firefox_data_directory):
searchResults = self.mergeBoolArray(searchResults, Firefox.search(urls, firefox_data_directory))
# Remove all the current stories
for i in self.menu.get_children():
if hasattr(i, 'url'):
self.menu.remove(i)
#Add back all the refreshed news
# Add back all the refreshed news
for index, item in enumerate(data):
if(chrome_data_directory):
item['history'] = searchResults[index]
else:
item['history'] = False
item['history'] = searchResults[index]
if item['url'].startswith('item?id='):
item['url'] = "https://news.ycombinator.com/" + item['url']
self.addItem(item)
# Catch network errors
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:
# Call every 10 minutes
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
def mergeBoolArray(self, original, patch):
for index, var in enumerate(original):
original[index] = original[index] or patch[index]
return original
def main():
parser = argparse.ArgumentParser(description='Hacker News in your System Tray')
parser.add_argument('-v','--version', action='version', version=__version)
parser.add_argument('-c','--comments', dest='comments',action='store_true', help="Load the HN comments link for the article as well")
parser.add_argument('-v', '--version', action='version', version=Version.current())
parser.add_argument('-c', '--comments', dest='comments', default=False, 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.set_defaults(comments=False)
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('-r', '--reverse', dest='reverse', default=False, action='store_true', help="Reverse the order of items. Use if your status bar is at the bottom of the screen")
args = parser.parse_args()
indicator = HackerNewsApp(args)
indicator.run()

View File

@ -9,8 +9,7 @@
#=========================
# We require PyGTK
import gtk
import gobject
from gi.repository import Gtk,GLib
# We also need os and sys
import os
@ -35,13 +34,14 @@ def get_icon_filename(icon_name):
# The main class
class Indicator:
# Constructor
def __init__(self, unknown, icon, category):
# Store the settings
self.inactive_icon = get_icon_filename(icon)
self.active_icon = "" # Blank until the user calls set_attention_icon
# Create the status icon
self.icon = gtk.StatusIcon()
self.icon = Gtk.StatusIcon()
# Initialize to the default icon
self.icon.set_from_file(self.inactive_icon)
@ -80,32 +80,7 @@ class Indicator:
def show_menu(self, widget):
# Show the menu
self.menu.popup(None, None, None, 0, 0)
# Get the location and size of the window
mouse_rect = self.menu.get_window().get_frame_extents()
self.x = mouse_rect.x
self.y = mouse_rect.y
self.right = self.x + mouse_rect.width
self.bottom = self.y + mouse_rect.height
# Set a timer to poll the menu
self.timer = gobject.timeout_add(100, self.check_mouse)
def check_mouse(self):
if not self.menu.get_window().is_visible():
return
# Now check the global mouse coords
root = self.menu.get_screen().get_root_window()
x, y, z = root.get_pointer()
if x < self.x or x > self.right or y < self.y or y > self.bottom:
self.hide_menu()
else:
return True
self.menu.popup(None, None, None, 0, 0, Gtk.get_current_event_time())
def hide_menu(self):
self.menu.popdown()

View File

@ -1,28 +1,32 @@
from __future__ import print_function
import sqlite3
import shutil
import os
import sys
class Chrome:
HISTORY_TMP_LOCATION = '/tmp/hackertray.chrome'
@staticmethod
def search(urls, config_folder_path):
Chrome.setup(config_folder_path)
conn = sqlite3.connect('/tmp/hackertray.chrome')
db = conn.cursor()
conn = sqlite3.connect(Chrome.HISTORY_TMP_LOCATION)
db = conn.cursor()
result = []
for url in urls:
db_result = db.execute('SELECT url from urls WHERE url=:url',{"url":url})
db_result = db.execute('SELECT url from urls WHERE url=:url', {"url": url})
if(db.fetchone() == None):
result.append(False)
else:
result.append(True)
os.remove('/tmp/hackertray.chrome')
os.remove(Chrome.HISTORY_TMP_LOCATION)
return result
@staticmethod
def setup(config_folder_path):
file_name = os.path.abspath(config_folder_path+'/History')
if not os.path.isfile(file_name):
print("ERROR: ", "Could not find Chrome history file", file=sys.stderr)
sys.exit(1)
shutil.copyfile(file_name, '/tmp/hackertray.chrome')
shutil.copyfile(file_name, Chrome.HISTORY_TMP_LOCATION)

51
hackertray/firefox.py Normal file
View File

@ -0,0 +1,51 @@
import sqlite3
import shutil
import os
import sys
from pathlib import Path
import configparser
class Firefox:
HISTORY_TMP_LOCATION = '/tmp/hackertray.firefox'
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
def search(urls, config_folder_path):
Firefox.setup(config_folder_path)
conn = sqlite3.connect(Firefox.HISTORY_TMP_LOCATION)
db = conn.cursor()
result = []
for url in urls:
db_result = db.execute('SELECT url from moz_places WHERE url=:url', {"url": url})
if(db.fetchone() == None):
result.append(False)
else:
result.append(True)
os.remove(Firefox.HISTORY_TMP_LOCATION)
return result
@staticmethod
def setup(config_folder_path):
file_name = os.path.abspath(config_folder_path + Firefox.HISTORY_FILE_NAME)
if not os.path.isfile(file_name):
print("ERROR: Could not find Firefox history file, using %s" % file_name)
sys.exit(1)
shutil.copyfile(file_name, Firefox.HISTORY_TMP_LOCATION)

View File

@ -7,6 +7,7 @@ urls = [
class HackerNews:
@staticmethod
def getHomePage():
random.shuffle(urls)
@ -15,4 +16,4 @@ class HackerNews:
try:
return r.json()
except ValueError:
continue
continue

29
hackertray/version.py Normal file
View File

@ -0,0 +1,29 @@
import requests
import pkg_resources
class Version:
PYPI_URL = "https://pypi.python.org/pypi/hackertray/json"
@staticmethod
def latest():
res = requests.get(Version.PYPI_URL).json()
return res['info']['version']
@staticmethod
def current():
return pkg_resources.require("hackertray")[0].version
@staticmethod
def new_available():
latest = Version.latest()
current = Version.current()
try:
if pkg_resources.parse_version(latest) > pkg_resources.parse_version(current):
print("[+] New version " + latest + " is available")
return True
else:
return False
except requests.exceptions.RequestException as e:
print("[+] There was an error in trying to fetch updates")
return False

View File

@ -3,18 +3,15 @@ import sys
from setuptools import setup
from setuptools import find_packages
requirements = ['requests']
if sys.version_info < (2, 7):
requirements.append('argparse')
setup(name='hackertray',
version='2.0.1',
version='4.0.2',
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',
url='http://captnemo.in/hackertray',
author='Abhay Rana',
url='https://captnemo.in/hackertray',
author='Abhay Rana (Nemo)',
author_email='me@captnemo.in',
license='MIT',
packages=find_packages(),
@ -22,7 +19,7 @@ setup(name='hackertray',
'hackertray.data': ['hacker-tray.png']
},
install_requires=[
'requests>=2.0',
'requests>=2.23.0'
],
entry_points={
'console_scripts': ['hackertray = hackertray:main'],

View File

@ -5,11 +5,11 @@ from hackertray import Chrome
class ChromeTest(unittest.TestCase):
def runTest(self):
config_folder_path = os.getcwd()+'/test/'
config_folder_path = os.getcwd()+'/test/'
data = Chrome.search([
"https://github.com/",
"https://news.ycombinator.com/",
"https://github.com/captn3m0/hackertray",
"http://invalid_url/"],
config_folder_path)
self.assertTrue(data == [True,True,True,False])
"https://github.com/",
"https://news.ycombinator.com/",
"https://github.com/captn3m0/hackertray",
"http://invalid_url/"],
config_folder_path)
self.assertTrue(data == [True,True,True,False])

32
test/firefox_test.py Normal file
View File

@ -0,0 +1,32 @@
import unittest
import os
import pathlib
from pathlib import Path
from hackertray import Firefox
class FirefoxTest(unittest.TestCase):
def test_history(self):
config_folder_path = os.getcwd()+'/test/'
data = Firefox.search([
"http://www.hckrnews.com/",
"http://www.google.com/",
"http://wiki.ubuntu.com/",
"http://invalid_url/"],
config_folder_path)
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")))

BIN
test/places.sqlite Normal file

Binary file not shown.

10
test/version_test.py Normal file
View File

@ -0,0 +1,10 @@
import unittest
from hackertray import Version
class VersionTest(unittest.TestCase):
def runTest(self):
version = Version.latest()
assert version
if __name__ == '__main__':
unittest.main()