├── .editorconfig ├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── hackertray ├── __init__.py ├── appindicator_replacement.py ├── chrome.py ├── data │ ├── __init__.py │ └── hacker-tray.png ├── firefox.py ├── hackernews.py └── version.py ├── screenshot.png ├── setup.py └── test ├── History ├── chrome_test.py ├── firefox_test.py ├── hn_test.py ├── places.sqlite └── version_test.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # 4 space indentation 12 | [*.py] 13 | indent_style = space 14 | indent_size = 4 15 | 16 | # Matches the exact files either package.json or .travis.yml 17 | [{package.json,.travis.yml}] 18 | indent_style = space 19 | indent_size = 2 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: captn3m0 2 | liberapay: captn3m0 3 | github: captn3m0 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled python modules. 2 | *.pyc 3 | 4 | # Distribution / packaging 5 | build/ 6 | develop-eggs/ 7 | dist/ 8 | eggs/ 9 | lib/ 10 | lib64/ 11 | parts/ 12 | sdist/ 13 | var/ 14 | *.egg-info/ 15 | .installed.cfg 16 | *.egg 17 | 18 | # Installer logs 19 | pip-log.txt 20 | pip-delete-this-directory.txt 21 | 22 | pyvenv.cfg 23 | bin/ 24 | .coverage 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | # https://endoflife.date/python 4 | # goes away in sep 2020 5 | - "3.5" 6 | # goes away in dec 2021 7 | - "3.6" 8 | # goes away in jun 2023 9 | - "3.7" 10 | # goes away in oct 2024 11 | - "3.8" 12 | # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors 13 | install: 14 | - pip install requests 15 | - pip install nose 16 | - pip install coverage 17 | - pip install coveralls 18 | # command to run tests, e.g. python setup.py test 19 | script: coverage run --source=hackertray $(which nosetests) 20 | after_success: coveralls 21 | notifications: 22 | email: 23 | on_success: never 24 | on_failure: always 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | This file will only list released and supported versions, usually skipping over very minor updates. 2 | 3 | Unreleased 4 | ========== 5 | 6 | 4.0.2 7 | ===== 8 | 9 | * Adds a --reverse flag for users with bar at the bottom of their screen 10 | * Uses markup to keep points in fixed-width, so titles are more readable 11 | * Removes the buggy hover-out behaviour (non-appindicator). You now need to click elsewhere to close the menu 12 | 13 | 4.0.1 14 | ===== 15 | 16 | * Changes "Show Comments" entry to a radio menu item 17 | 18 | 4.0.0 19 | ===== 20 | 21 | * Adds support for --firefox auto, picks the default firefox profile automatically 22 | * Upgrades to Python 3.0. Python 2 is no longer supported 23 | * Switches from PyGtk to PyGObject. 24 | * AppIndicator is no longer supported, because it is Python 2 only 25 | * Removed all MixPanel tracking. 26 | 27 | 3.0.0 28 | ===== 29 | 30 | * Oct 3, 2014 31 | * Major release. 32 | * Firefox support behind `--firefox` flag 33 | * Analytics support. Can be disabled using `--dnt` flag 34 | * Hovering now shows url, timestamp, and uploader nick 35 | 36 | 2.3.2 37 | ===== 38 | 39 | * Sep 27, 2014 40 | * Adds proxy support 41 | 42 | 43 | 2.2.0 44 | ===== 45 | 46 | * Adds support for using chrome history behind the `--chrome` flag. 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Abhay Rana 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include hackertray/data/hacker-tray.png 2 | exclude screenshot.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HackerTray 2 | 3 | [![HackerTray on PyPi](https://pypip.in/v/hackertray/badge.png)](https://pypi.python.org/pypi/hackertray/) 4 | [![HackerTray on PyPi](https://pypip.in/d/hackertray/badge.png)](https://pypi.python.org/pypi/hackertray/) 5 | [![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) 6 | 7 | HackerTray is a simple [Hacker News](https://news.ycombinator.com/) Linux application 8 | that lets you view top HN stories in your System Tray. It uses appindicator where available, 9 | but provides a Gtk StatusIcon fallback in case AppIndicator is not available. 10 | 11 | The inspiration for this came from [Hacker Bar](http://hackerbarapp.com), which is Mac-only. 12 | 13 | ## Screenshot 14 | 15 | ![HackerTray Screenshot in elementaryOS](http://i.imgur.com/63l3qXV.png) 16 | 17 | ## Installation 18 | 19 | HackerTray is distributed as a python package. Do the following to install: 20 | 21 | ```sh 22 | sudo pip install hackertray 23 | OR 24 | sudo easy_install hackertray 25 | OR 26 | #Download Source and cd to it 27 | sudo python setup.py install 28 | ``` 29 | 30 | After that, you can run `hackertray` from anywhere and it will run. You can 31 | now add it to your OS dependent session autostart method. In Ubuntu, you can 32 | access it via: 33 | 34 | 1. System > Preferences > Sessions 35 | (OR) 36 | 2. System > Preferences > Startup Applications 37 | 38 | depending on your Ubuntu Version. Or put it in `~/.config/openbox/autostart` 39 | if you are running OpenBox. [Here](http://imgur.com/mnhIzDK) is how the 40 | configuration should look like in Ubuntu and its derivatives. 41 | 42 | ### Upgrade 43 | 44 | The latest stable version is [![the one on PyPi](https://pypip.in/v/hackertray/badge.png)](https://pypi.python.org/pypi/hackertray/) 45 | 46 | You can check which version you have installed with `hackertray --version`. 47 | 48 | To upgrade, run `pip install -U hackertray`. In some cases (Ubuntu), you might 49 | need to clear the pip cache before upgrading: 50 | 51 | `sudo rm -rf /tmp/pip-build-root/hackertray` 52 | 53 | HackerTray will automatically check the latest version on startup, and inform you if there is an update available. 54 | 55 | ## Options 56 | 57 | HackerTray accepts its various options via the command line. Run `hackertray -h` to see all options. Currently the following switches are supported: 58 | 59 | 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. 60 | 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. 61 | 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. 62 | 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. 63 | 64 | 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). 65 | 66 | ### Google Chrome Profile Path 67 | 68 | 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): 69 | 70 | - [Chrome Stable] `~/.config/google-chrome/Default` 71 | - [Chrome Beta] `~/.config/google-chrome-beta/Default` 72 | - [Chrome Dev] `~/.config/google-chrome-unstable/Default` 73 | - [Chromium] `~/.config/chromium/Default` 74 | 75 | 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/`). 76 | 77 | ### Firefox Profile Path 78 | 79 | 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. 80 | 81 | ## Features 82 | 83 | 1. Minimalist Approach to HN 84 | 2. Opens links in your default browser 85 | 3. Remembers which links you opened, even if you opened them outside of HackerTray 86 | 4. Shows Points/Comment count in a simple format 87 | 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) 88 | 89 | ### Troubleshooting 90 | 91 | If the app indicator fails to show in Ubuntu versions, consider installing 92 | python-appindicator with 93 | 94 | `sudo apt-get install python-appindicator` 95 | 96 | Note that appindicator is no longer supported in non-Ubuntu distros, because it only works on Python2. 97 | 98 | ### Development 99 | 100 | To develop on hackertray, or to test out experimental versions, do the following: 101 | 102 | - Clone the project 103 | - Run `(sudo) python setup.py develop` in the hackertray root directory 104 | - Run `hackertray` with the required command line options from anywhere. 105 | 106 | ## Analytics 107 | 108 | On every launch, a request is made to `https://pypi.python.org/pypi/hackertray/json` to check the latest version. 109 | 110 | **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`). 111 | 112 | ## Credits 113 | 114 | - Mark Rickert for [Hacker Bar](http://hackerbarapp.com/) (No longer active) 115 | - [Giridaran Manivannan](https://github.com/ace03uec) for troubleshooting instructions. 116 | - [@cheeaun](https://github.com/cheeaun) for the [Unofficial Hacker News API](https://github.com/cheeaun/node-hnapi/) 117 | 118 | ## Licence 119 | 120 | Licenced under the [MIT Licence](https://nemo.mit-license.org/). See the LICENSE file for complete license text. 121 | -------------------------------------------------------------------------------- /hackertray/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import requests 5 | import subprocess 6 | 7 | if(os.environ.get('TRAVIS') != 'true'): 8 | import gi 9 | gi.require_version('Gtk', '3.0') 10 | from gi.repository import Gtk,GLib 11 | import webbrowser 12 | 13 | try: 14 | import appindicator 15 | except ImportError: 16 | from . import appindicator_replacement as appindicator 17 | 18 | from .appindicator_replacement import get_icon_filename 19 | 20 | import json 21 | import argparse 22 | from os.path import expanduser 23 | import signal 24 | 25 | from .hackernews import HackerNews 26 | from .chrome import Chrome 27 | from .firefox import Firefox 28 | from .version import Version 29 | 30 | 31 | class HackerNewsApp: 32 | HN_URL_PREFIX = "https://news.ycombinator.com/item?id=" 33 | UPDATE_URL = "https://github.com/captn3m0/hackertray#upgrade" 34 | ABOUT_URL = "https://github.com/captn3m0/hackertray" 35 | 36 | def __init__(self, args): 37 | # Load the database 38 | home = expanduser("~") 39 | with open(home + '/.hackertray.json', 'a+') as content_file: 40 | content_file.seek(0) 41 | content = content_file.read() 42 | try: 43 | self.db = set(json.loads(content)) 44 | except ValueError: 45 | self.db = set() 46 | 47 | # create an indicator applet 48 | self.ind = appindicator.Indicator("Hacker Tray", "hacker-tray", appindicator.CATEGORY_APPLICATION_STATUS) 49 | self.ind.set_status(appindicator.STATUS_ACTIVE) 50 | self.ind.set_icon(get_icon_filename("hacker-tray.png")) 51 | 52 | # create a menu 53 | self.menu = Gtk.Menu() 54 | 55 | # The default state is false, and it toggles when you click on it 56 | self.commentState = args.comments 57 | self.reverse = args.reverse 58 | 59 | # create items for the menu - refresh, quit and a separator 60 | menuSeparator = Gtk.SeparatorMenuItem() 61 | menuSeparator.show() 62 | self.add(menuSeparator) 63 | 64 | btnComments = Gtk.CheckMenuItem("Show Comments") 65 | btnComments.show() 66 | btnComments.set_active(args.comments) 67 | btnComments.set_draw_as_radio(True) 68 | btnComments.connect("activate", self.toggleComments) 69 | self.add(btnComments) 70 | 71 | btnAbout = Gtk.MenuItem("About") 72 | btnAbout.show() 73 | btnAbout.connect("activate", self.showAbout) 74 | self.add(btnAbout) 75 | 76 | btnRefresh = Gtk.MenuItem("Refresh") 77 | btnRefresh.show() 78 | # the last parameter is for not running the timer 79 | btnRefresh.connect("activate", self.refresh, True, args.chrome) 80 | self.add(btnRefresh) 81 | 82 | if Version.new_available(): 83 | btnUpdate = Gtk.MenuItem("New Update Available") 84 | btnUpdate.show() 85 | btnUpdate.connect('activate', self.showUpdate) 86 | self.add(btnUpdate) 87 | 88 | btnQuit = Gtk.MenuItem("Quit") 89 | btnQuit.show() 90 | btnQuit.connect("activate", self.quit) 91 | self.add(btnQuit) 92 | self.menu.show() 93 | self.ind.set_menu(self.menu) 94 | 95 | if args.firefox == "auto": 96 | args.firefox = Firefox.default_firefox_profile_path() 97 | self.refresh(chrome_data_directory=args.chrome, firefox_data_directory=args.firefox) 98 | 99 | def add(self, item): 100 | if self.reverse: 101 | self.menu.prepend(item) 102 | else: 103 | self.menu.append(item) 104 | 105 | def toggleComments(self, widget): 106 | """Whether comments page is opened or not""" 107 | self.commentState = not self.commentState 108 | 109 | def showUpdate(self, widget): 110 | """Handle the update button""" 111 | webbrowser.open(HackerNewsApp.UPDATE_URL) 112 | # Remove the update button once clicked 113 | self.menu.remove(widget) 114 | 115 | def showAbout(self, widget): 116 | """Handle the about btn""" 117 | webbrowser.open(HackerNewsApp.ABOUT_URL) 118 | 119 | # ToDo: Handle keyboard interrupt properly 120 | def quit(self, widget, data=None): 121 | """ Handler for the quit button""" 122 | l = list(self.db) 123 | home = expanduser("~") 124 | 125 | # truncate the file 126 | with open(home + '/.hackertray.json', 'w+') as file: 127 | file.write(json.dumps(l)) 128 | 129 | Gtk.main_quit() 130 | 131 | def run(self): 132 | signal.signal(signal.SIGINT, self.quit) 133 | Gtk.main() 134 | return 0 135 | 136 | def open(self, widget, **args): 137 | """Opens the link in the web browser""" 138 | # We disconnect and reconnect the event in case we have 139 | # to set it to active and we don't want the signal to be processed 140 | if not widget.get_active(): 141 | widget.disconnect(widget.signal_id) 142 | widget.set_active(True) 143 | widget.signal_id = widget.connect('activate', self.open) 144 | 145 | self.db.add(widget.item_id) 146 | webbrowser.open(widget.url) 147 | 148 | # TODO: Add support for Shift+Click or Right Click 149 | # to do the opposite of the current commentState setting 150 | if self.commentState: 151 | webbrowser.open(self.HN_URL_PREFIX + str(widget.hn_id)) 152 | 153 | def addItem(self, item): 154 | """Adds an item to the menu""" 155 | # This is in the case of YC Job Postings, which we skip 156 | if item['points'] == 0 or item['points'] is None: 157 | return 158 | 159 | points = str(item['points']).zfill(3) + "/" + str(item['comments_count']).zfill(3) 160 | 161 | i = Gtk.CheckMenuItem.new_with_label(label="(" + points + ")"+item['title']) 162 | label = i.get_child() 163 | label.set_markup("" + points + " "+item['title']+"".format(points=points, title=item['title'])) 164 | label.set_selectable(False) 165 | 166 | visited = item['history'] or item['id'] in self.db 167 | 168 | i.set_active(visited) 169 | i.url = item['url'] 170 | tooltip = "{url}\nPosted by {user} {timeago}".format(url=item['url'], user=item['user'], timeago=item['time_ago']) 171 | i.set_tooltip_text(tooltip) 172 | i.signal_id = i.connect('activate', self.open) 173 | i.hn_id = item['id'] 174 | i.item_id = item['id'] 175 | if self.reverse: 176 | self.menu.append(i) 177 | else: 178 | self.menu.prepend(i) 179 | i.show() 180 | 181 | def refresh(self, widget=None, no_timer=False, chrome_data_directory=None, firefox_data_directory=None): 182 | """Refreshes the menu """ 183 | try: 184 | # Create an array of 20 false to denote matches in History 185 | searchResults = [False]*20 186 | data = list(reversed(HackerNews.getHomePage()[0:20])) 187 | urls = [item['url'] for item in data] 188 | if(chrome_data_directory): 189 | searchResults = self.mergeBoolArray(searchResults, Chrome.search(urls, chrome_data_directory)) 190 | 191 | if(firefox_data_directory): 192 | searchResults = self.mergeBoolArray(searchResults, Firefox.search(urls, firefox_data_directory)) 193 | 194 | # Remove all the current stories 195 | for i in self.menu.get_children(): 196 | if hasattr(i, 'url'): 197 | self.menu.remove(i) 198 | 199 | # Add back all the refreshed news 200 | for index, item in enumerate(data): 201 | item['history'] = searchResults[index] 202 | if item['url'].startswith('item?id='): 203 | item['url'] = "https://news.ycombinator.com/" + item['url'] 204 | 205 | self.addItem(item) 206 | # Catch network errors 207 | except requests.exceptions.RequestException as e: 208 | print("[+] There was an error in fetching news items") 209 | finally: 210 | # Call every 10 minutes 211 | if not no_timer: 212 | GLib.timeout_add(10 * 30 * 1000, self.refresh, widget, no_timer, chrome_data_directory) 213 | 214 | # Merges two boolean arrays, using OR operation against each pair 215 | def mergeBoolArray(self, original, patch): 216 | for index, var in enumerate(original): 217 | original[index] = original[index] or patch[index] 218 | return original 219 | 220 | 221 | def main(): 222 | parser = argparse.ArgumentParser(description='Hacker News in your System Tray') 223 | parser.add_argument('-v', '--version', action='version', version=Version.current()) 224 | parser.add_argument('-c', '--comments', dest='comments', default=False, action='store_true', help="Load the HN comments link for the article as well") 225 | parser.add_argument('--chrome', dest='chrome', help="Specify a Google Chrome Profile directory to use for matching chrome history") 226 | 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") 227 | 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") 228 | args = parser.parse_args() 229 | indicator = HackerNewsApp(args) 230 | indicator.run() 231 | -------------------------------------------------------------------------------- /hackertray/appindicator_replacement.py: -------------------------------------------------------------------------------- 1 | #========================= 2 | # 3 | # AppIndicator for GTK 4 | # drop-in replacement 5 | # 6 | # Copyright 2010 7 | # Nathan Osman 8 | # 9 | #========================= 10 | 11 | # We require PyGTK 12 | from gi.repository import Gtk,GLib 13 | 14 | # We also need os and sys 15 | import os 16 | 17 | from pkg_resources import resource_filename 18 | 19 | # Types 20 | CATEGORY_APPLICATION_STATUS = 0 21 | 22 | # Status 23 | STATUS_ACTIVE = 0 24 | STATUS_ATTENTION = 1 25 | 26 | # Locations to search for the given icon 27 | 28 | 29 | def get_icon_filename(icon_name): 30 | # Determine where the icon is 31 | return os.path.abspath(resource_filename('hackertray.data', 'hacker-tray.png')) 32 | 33 | 34 | # The main class 35 | class Indicator: 36 | # Constructor 37 | 38 | def __init__(self, unknown, icon, category): 39 | # Store the settings 40 | self.inactive_icon = get_icon_filename(icon) 41 | self.active_icon = "" # Blank until the user calls set_attention_icon 42 | 43 | # Create the status icon 44 | self.icon = Gtk.StatusIcon() 45 | 46 | # Initialize to the default icon 47 | self.icon.set_from_file(self.inactive_icon) 48 | 49 | # Set the rest of the vars 50 | self.menu = None # We have no menu yet 51 | 52 | def set_menu(self, menu): 53 | # Save a copy of the menu 54 | self.menu = menu 55 | 56 | # Now attach the icon's signal 57 | # to the menu so that it becomes displayed 58 | # whenever the user clicks it 59 | self.icon.connect("activate", self.show_menu) 60 | 61 | def set_status(self, status): 62 | # Status defines whether the active or inactive 63 | # icon should be displayed. 64 | if status == STATUS_ACTIVE: 65 | self.icon.set_from_file(self.inactive_icon) 66 | else: 67 | self.icon.set_from_file(self.active_icon) 68 | 69 | def set_label(self, label): 70 | self.icon.set_title(label) 71 | return 72 | 73 | def set_icon(self, icon): 74 | # Set the new icon 75 | self.icon.set_from_file(get_icon_filename(icon)) 76 | 77 | def set_attention_icon(self, icon): 78 | # Set the icon filename as the attention icon 79 | self.active_icon = get_icon_filename(icon) 80 | 81 | def show_menu(self, widget): 82 | # Show the menu 83 | self.menu.popup(None, None, None, 0, 0, Gtk.get_current_event_time()) 84 | 85 | def hide_menu(self): 86 | self.menu.popdown() 87 | -------------------------------------------------------------------------------- /hackertray/chrome.py: -------------------------------------------------------------------------------- 1 | 2 | import sqlite3 3 | import shutil 4 | import os 5 | import sys 6 | 7 | 8 | class Chrome: 9 | HISTORY_TMP_LOCATION = '/tmp/hackertray.chrome' 10 | 11 | @staticmethod 12 | def search(urls, config_folder_path): 13 | Chrome.setup(config_folder_path) 14 | conn = sqlite3.connect(Chrome.HISTORY_TMP_LOCATION) 15 | db = conn.cursor() 16 | result = [] 17 | for url in urls: 18 | db_result = db.execute('SELECT url from urls WHERE url=:url', {"url": url}) 19 | if(db.fetchone() == None): 20 | result.append(False) 21 | else: 22 | result.append(True) 23 | os.remove(Chrome.HISTORY_TMP_LOCATION) 24 | return result 25 | 26 | @staticmethod 27 | def setup(config_folder_path): 28 | file_name = os.path.abspath(config_folder_path+'/History') 29 | if not os.path.isfile(file_name): 30 | print("ERROR: ", "Could not find Chrome history file", file=sys.stderr) 31 | sys.exit(1) 32 | shutil.copyfile(file_name, Chrome.HISTORY_TMP_LOCATION) 33 | -------------------------------------------------------------------------------- /hackertray/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/captn3m0/hackertray/ef43ce59424cc76e62405882557908f81d6b83e3/hackertray/data/__init__.py -------------------------------------------------------------------------------- /hackertray/data/hacker-tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/captn3m0/hackertray/ef43ce59424cc76e62405882557908f81d6b83e3/hackertray/data/hacker-tray.png -------------------------------------------------------------------------------- /hackertray/firefox.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import shutil 3 | import os 4 | import sys 5 | from pathlib import Path 6 | import configparser 7 | 8 | class Firefox: 9 | HISTORY_TMP_LOCATION = '/tmp/hackertray.firefox' 10 | HISTORY_FILE_NAME = '/places.sqlite' 11 | 12 | @staticmethod 13 | def default_firefox_profile_path(): 14 | profile_file_path = str(Path.home().joinpath(".mozilla/firefox/profiles.ini")) 15 | profile_path = None 16 | if (os.path.exists(profile_file_path)): 17 | parser = configparser.ConfigParser() 18 | parser.read(profile_file_path) 19 | for section in parser.sections(): 20 | if parser.has_option(section,"Default") and parser[section]["Default"] == "1": 21 | if parser.has_option(section,"IsRelative") and parser[section]["IsRelative"] == "1": 22 | profile_path = str(Path.home().joinpath(".mozilla/firefox/").joinpath(parser[section]["Path"])) 23 | else: 24 | profile_path = parser[section]["Path"] 25 | if profile_path and Path.is_dir(Path(profile_path)): 26 | return profile_path 27 | else: 28 | raise RuntimeError("Couldn't find default Firefox profile") 29 | 30 | @staticmethod 31 | def search(urls, config_folder_path): 32 | Firefox.setup(config_folder_path) 33 | conn = sqlite3.connect(Firefox.HISTORY_TMP_LOCATION) 34 | db = conn.cursor() 35 | result = [] 36 | for url in urls: 37 | db_result = db.execute('SELECT url from moz_places WHERE url=:url', {"url": url}) 38 | if(db.fetchone() == None): 39 | result.append(False) 40 | else: 41 | result.append(True) 42 | os.remove(Firefox.HISTORY_TMP_LOCATION) 43 | return result 44 | 45 | @staticmethod 46 | def setup(config_folder_path): 47 | file_name = os.path.abspath(config_folder_path + Firefox.HISTORY_FILE_NAME) 48 | if not os.path.isfile(file_name): 49 | print("ERROR: Could not find Firefox history file, using %s" % file_name) 50 | sys.exit(1) 51 | shutil.copyfile(file_name, Firefox.HISTORY_TMP_LOCATION) 52 | -------------------------------------------------------------------------------- /hackertray/hackernews.py: -------------------------------------------------------------------------------- 1 | import random 2 | import requests 3 | 4 | urls = [ 5 | 'https://node-hnapi.herokuapp.com/' 6 | ] 7 | 8 | 9 | class HackerNews: 10 | 11 | @staticmethod 12 | def getHomePage(): 13 | random.shuffle(urls) 14 | for i in urls: 15 | r = requests.get(i + "news") 16 | try: 17 | return r.json() 18 | except ValueError: 19 | continue 20 | -------------------------------------------------------------------------------- /hackertray/version.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import pkg_resources 3 | 4 | 5 | class Version: 6 | PYPI_URL = "https://pypi.python.org/pypi/hackertray/json" 7 | 8 | @staticmethod 9 | def latest(): 10 | res = requests.get(Version.PYPI_URL).json() 11 | return res['info']['version'] 12 | 13 | @staticmethod 14 | def current(): 15 | return pkg_resources.require("hackertray")[0].version 16 | 17 | @staticmethod 18 | def new_available(): 19 | latest = Version.latest() 20 | current = Version.current() 21 | try: 22 | if pkg_resources.parse_version(latest) > pkg_resources.parse_version(current): 23 | print("[+] New version " + latest + " is available") 24 | return True 25 | else: 26 | return False 27 | except requests.exceptions.RequestException as e: 28 | print("[+] There was an error in trying to fetch updates") 29 | return False 30 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/captn3m0/hackertray/ef43ce59424cc76e62405882557908f81d6b83e3/screenshot.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from setuptools import setup 4 | from setuptools import find_packages 5 | 6 | requirements = ['requests'] 7 | 8 | setup(name='hackertray', 9 | version='4.0.2', 10 | description='Hacker News app that sits in your System Tray', 11 | 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.', 12 | keywords='hacker news hn tray system tray icon hackertray', 13 | url='https://captnemo.in/hackertray', 14 | author='Abhay Rana (Nemo)', 15 | author_email='me@captnemo.in', 16 | license='MIT', 17 | packages=find_packages(), 18 | package_data={ 19 | 'hackertray.data': ['hacker-tray.png'] 20 | }, 21 | install_requires=[ 22 | 'requests>=2.23.0' 23 | ], 24 | entry_points={ 25 | 'console_scripts': ['hackertray = hackertray:main'], 26 | }, 27 | zip_safe=False) 28 | -------------------------------------------------------------------------------- /test/History: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/captn3m0/hackertray/ef43ce59424cc76e62405882557908f81d6b83e3/test/History -------------------------------------------------------------------------------- /test/chrome_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | 4 | from hackertray import Chrome 5 | 6 | class ChromeTest(unittest.TestCase): 7 | def runTest(self): 8 | config_folder_path = os.getcwd()+'/test/' 9 | data = Chrome.search([ 10 | "https://github.com/", 11 | "https://news.ycombinator.com/", 12 | "https://github.com/captn3m0/hackertray", 13 | "http://invalid_url/"], 14 | config_folder_path) 15 | self.assertTrue(data == [True,True,True,False]) -------------------------------------------------------------------------------- /test/firefox_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import pathlib 4 | from pathlib import Path 5 | 6 | from hackertray import Firefox 7 | 8 | class FirefoxTest(unittest.TestCase): 9 | def test_history(self): 10 | config_folder_path = os.getcwd()+'/test/' 11 | data = Firefox.search([ 12 | "http://www.hckrnews.com/", 13 | "http://www.google.com/", 14 | "http://wiki.ubuntu.com/", 15 | "http://invalid_url/"], 16 | config_folder_path) 17 | self.assertTrue(data == [True,True,True,False]) 18 | 19 | def test_default(self): 20 | test_default_path = Path.home().joinpath(".mozilla/firefox/x0ran0o9.default") 21 | if(os.environ.get('TRAVIS') == 'true'): 22 | if not os.path.exists(str(test_default_path)): 23 | os.makedirs(str(test_default_path)) 24 | with open(str(Path.home().joinpath('.mozilla/firefox/profiles.ini')), 'w') as f: 25 | f.write(""" 26 | [Profile1] 27 | Name=default 28 | IsRelative=1 29 | Path=x0ran0o9.default 30 | Default=1 31 | """) 32 | self.assertTrue(Firefox.default_firefox_profile_path()==str(Path.home().joinpath(".mozilla/firefox/x0ran0o9.default"))) 33 | -------------------------------------------------------------------------------- /test/hn_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from hackertray import HackerNews 3 | 4 | class HNTest(unittest.TestCase): 5 | def runTest(self): 6 | data = HackerNews.getHomePage() 7 | self.assertTrue(len(data) > 0) 8 | 9 | 10 | if __name__ == '__main__': 11 | unittest.main() -------------------------------------------------------------------------------- /test/places.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/captn3m0/hackertray/ef43ce59424cc76e62405882557908f81d6b83e3/test/places.sqlite -------------------------------------------------------------------------------- /test/version_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from hackertray import Version 3 | 4 | class VersionTest(unittest.TestCase): 5 | def runTest(self): 6 | version = Version.latest() 7 | assert version 8 | 9 | if __name__ == '__main__': 10 | unittest.main() --------------------------------------------------------------------------------