├── .gitignore ├── AUTHORS ├── LICENSE ├── Makefile.am ├── README.md ├── Screenshot.png ├── autogen.sh ├── configure.ac ├── data ├── Makefile.am ├── tarpon.desktop.in └── ui │ ├── Makefile.am │ ├── about_dialog.ui.in │ ├── about_dialog_hb.ui.in │ ├── menu.ui │ └── menubar.ui ├── requirements.txt ├── setup.py ├── src ├── Makefile.am ├── tarpon.in └── tarpon_app │ ├── Makefile.am │ ├── __init__.py │ ├── application.py │ ├── docsets.py │ ├── gtk │ ├── Makefile.am │ ├── __init__.py │ └── components.py │ └── info.py └── tarpon.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | AUTHORS 2 | ======= 3 | 4 | Kunal Sarkhel 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2015 Kunal Sarkhel 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /Makefile.am: -------------------------------------------------------------------------------- 1 | SUBDIRS = data src 2 | 3 | EXTRA_DIST = LICENSE 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Tarpon 2 | ====== 3 | 4 | Tarpon is an offline documentation browser written using Python and Gtk+3. 5 | 6 | ![Screenshot of Tarpon](https://raw.githubusercontent.com/techwizrd/tarpon/master/Screenshot.png) 7 | 8 | Installing and Running 9 | ---------------------- 10 | 11 | To run Tarpon, execute it directly for now: 12 | 13 | $ cd tarpon 14 | $ python tarpon.py 15 | 16 | In order to install Tarpon, execute the following autotools commands: 17 | 18 | $ ./autogen.sh 19 | $ make 20 | $ make install 21 | $ pip install -r requirements.txt 22 | 23 | Uninstall Tarpon using ``make uninstall`` or install Tarpon locally 24 | (rather than system-wide) by executing ``./configure --prefix=$HOME/.local`` 25 | before executing ``make``. A distributable package can be built using ``make dist``. 26 | 27 | FAQ 28 | --- 29 | 30 | 1. Why did you create your UI in code instead of ``Gtk.Builder``? 31 | 32 | Moving to ``Gtk.Builder`` and XML files is a future goal. Originally, I could not figure out how to create a ``Gtk.HeaderBar`` and have it set as the titlebar of a ``Gtk.Window`` automatically using ``Gtk.Builder``. 33 | -------------------------------------------------------------------------------- /Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techwizrd/tarpon/382f798716e91e6e3afd2098877cdfcba7d79d51/Screenshot.png -------------------------------------------------------------------------------- /autogen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | autoreconf --force --install --verbose || exit 1 4 | ./configure 5 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | AC_INIT([Tarpon], [0.1]) 2 | 3 | AM_INIT_AUTOMAKE([1.10 no-define foreign dist-xz no-dist-gzip]) 4 | AM_PATH_PYTHON([2.7]) 5 | 6 | AC_CONFIG_FILES([ 7 | Makefile 8 | data/Makefile 9 | data/tarpon.desktop 10 | data/ui/Makefile 11 | data/ui/about_dialog.ui 12 | data/ui/about_dialog_hb.ui 13 | src/Makefile 14 | src/tarpon_app/Makefile 15 | src/tarpon_app/gtk/Makefile 16 | ]) 17 | 18 | AC_OUTPUT 19 | -------------------------------------------------------------------------------- /data/Makefile.am: -------------------------------------------------------------------------------- 1 | SUBDIRS = ui 2 | 3 | desktopdir = $(datadir)/applications 4 | desktop_DATA = tarpon.desktop 5 | 6 | UPDATE_DESKTOP = update-desktop-database $(datadir)/applications || : 7 | 8 | install-data-hook: 9 | $(UPDATE_DESKTOP) 10 | uninstall-hook: 11 | $(UPDATE_DESKTOP) 12 | -------------------------------------------------------------------------------- /data/tarpon.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=@VERSION@ 3 | Encoding=UTF-8 4 | Name=@PACKAGE_NAME@ 5 | GenericName=@PACKAGE_NAME@ 6 | Comment=Offline documentation browser 7 | Icon=@PACKAGE@ 8 | Exec=@PACKAGE@ 9 | Terminal=false 10 | Type=Application 11 | Categories=GNOME;GTK;Development; 12 | StartupNotify=true 13 | -------------------------------------------------------------------------------- /data/ui/Makefile.am: -------------------------------------------------------------------------------- 1 | uidir = $(pkgdatadir)/ui 2 | ui_DATA = menu.ui menubar.ui about_dialog.ui about_dialog_hb.ui 3 | 4 | do_substitution = sed -e 's,[@]pythondir[@],$(pythondir),g' \ 5 | -e 's,[@]pkgdatadir[@],$(pkgdatadir),g' \ 6 | -e 's,[@]PACKAGE[@],$(PACKAGE),g' \ 7 | -e 's,[@]VERSION[@],$(VERSION),g' 8 | 9 | about_dialog.ui: about_dialog.ui.in Makefile 10 | $(do_substitution) < about_dialog.ui.in > about_dialog.ui 11 | 12 | about_dialog_hb.ui: about_dialog_hb.ui.in Makefile 13 | $(do_substitution) < about_dialog_hb.ui.in > about_dialog_hb.ui 14 | 15 | EXTRA_DIST = $(ui_DATA) 16 | -------------------------------------------------------------------------------- /data/ui/about_dialog.ui.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | False 7 | center-on-parent 8 | dialog 9 | @PACKAGE_NAME@ 10 | @VERSION@ 11 | Copyright © 2015 Kunal Sarkhel 12 | https://github.com/techwizrd/tarpon 13 | Visit the @PACKAGE_NAME@ website 14 | Apache 2.0 License 15 | @PACKAGE@ 16 | 17 | 18 | False 19 | vertical 20 | 2 21 | 22 | 23 | False 24 | end 25 | 26 | 27 | False 28 | False 29 | 0 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /data/ui/about_dialog_hb.ui.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | False 7 | center-on-parent 8 | dialog 9 | @PACKAGE_NAME@ 10 | @VERSION@ 11 | Copyright © 2015 Kunal Sarkhel 12 | https://github.com/techwizrd/tarpon 13 | Visit the @PACKAGE_NAME@ website 14 | Apache 2.0 License 15 | @PACKAGE@ 16 | 17 | 18 | False 19 | vertical 20 | 2 21 | 22 | 23 | False 24 | end 25 | 26 | 27 | False 28 | False 29 | 0 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | True 40 | False 41 | About @PACKAGE_NAME@ 42 | False 43 | True 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /data/ui/menu.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | _New Window 7 | <Primary>n 8 | win.new_window 9 | 10 | 11 | New _Tab 12 | <Primary>n 13 | win.new_tab 14 | 15 |
16 |
17 | 18 | Preferences 19 | app.preferences 20 | 21 |
22 |
23 | 24 | Larger Text 25 | <Primary>plus 26 | win.larger_text 27 | 28 | 29 | Smaller Text 30 | <Primary>minus 31 | win.smaller_text 32 | 33 | 34 | Normal Size 35 | <Primary>0 36 | win.normal_text 37 | 38 | 39 | Search Bar 40 | <Primary>S 41 | win.toggle_searchbar 42 | 43 |
44 |
45 | 46 | About 47 | win.about 48 | 49 | 50 | _Quit 51 | <Primary>q 52 | win.quit 53 | 54 |
55 |
56 |
57 | -------------------------------------------------------------------------------- /data/ui/menubar.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _File 6 |
7 | 8 | New _Window 9 | <Primary>n 10 | app.new_window 11 | 12 | 13 | New _Tab 14 | <Primary>t 15 | win.new_tab 16 | 17 |
18 |
19 | 20 | Close Window 21 | win.quit 22 | 23 | 24 | _Quit 25 | <Primary>q 26 | app.quit 27 | 28 |
29 |
30 | 31 | _Edit 32 | 33 | Preferences 34 | app.preferences 35 | 36 | 37 | 38 | _View 39 | 40 | Side Panel 41 | F9 42 | win.toggle_panel 43 | 44 | 45 | Search Bar 46 | <Primary>S 47 | win.toggle_searchbar 48 | 49 |
50 | 51 | Larger Text 52 | <Primary>plus 53 | win.larger_text 54 | 55 | 56 | Smaller Text 57 | <Primary>minus 58 | win.smaller_text 59 | 60 | 61 | Normal Size 62 | <Primary>0 63 | win.normal_text 64 | 65 |
66 |
67 | 68 | _Help 69 | 70 | About 71 | win.about 72 | 73 | 74 |
75 |
76 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | peewee 2 | appdirs 3 | fuzzywuzzy 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /src/Makefile.am: -------------------------------------------------------------------------------- 1 | SUBDIRS = tarpon_app 2 | 3 | bin_SCRIPTS = tarpon 4 | CLEANFILES = $(bin_SCRIPTS) 5 | EXTRA_DIST = tarpon.in 6 | 7 | do_substitution = sed -e 's,[@]pythondir[@],$(pythondir),g' \ 8 | -e 's,[@]pkgdatadir[@],$(pkgdatadir),g' \ 9 | -e 's,[@]PACKAGE[@],$(PACKAGE),g' \ 10 | -e 's,[@]VERSION[@],$(VERSION),g' 11 | 12 | tarpon: tarpon.in Makefile 13 | $(do_substitution) < $(srcdir)/tarpon.in > tarpon 14 | chmod +x tarpon 15 | 16 | -------------------------------------------------------------------------------- /src/tarpon.in: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | 6 | sys.path.insert(1, '@pythondir@') 7 | 8 | from tarpon_app.application import Application 9 | 10 | 11 | def main(): 12 | """Starts the Tarpon application""" 13 | app = Application(package="@PACKAGE@", version="@VERSION@", 14 | pkgdatadir="@pkgdatadir@") 15 | exit_status = app.run(sys.argv) 16 | sys.exit(exit_status) 17 | 18 | 19 | if __name__ == '__main__': 20 | main() 21 | -------------------------------------------------------------------------------- /src/tarpon_app/Makefile.am: -------------------------------------------------------------------------------- 1 | SUBDIRS = gtk 2 | 3 | tarpon_PYTHON = \ 4 | application.py \ 5 | docsets.py \ 6 | info.py \ 7 | __init__.py 8 | 9 | tarpondir = $(pythondir)/tarpon_app 10 | -------------------------------------------------------------------------------- /src/tarpon_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techwizrd/tarpon/382f798716e91e6e3afd2098877cdfcba7d79d51/src/tarpon_app/__init__.py -------------------------------------------------------------------------------- /src/tarpon_app/application.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import glob 5 | import json 6 | import os 7 | from gi.repository import Gtk, Gio 8 | 9 | import appdirs 10 | 11 | from tarpon_app.docsets import Docset 12 | from tarpon_app.gtk.components import TarponWindow, views 13 | import tarpon_app.info as info 14 | 15 | 16 | APP_MENU = """ 17 | 18 | 19 |
20 | 21 | _New Window 22 | <Primary>n 23 | app.new_window 24 | 25 |
26 |
27 | 28 | Preferences 29 | app.preferences 30 | 31 |
32 |
33 | 34 | About 35 | app.about 36 | 37 | 38 | _Quit 39 | <Primary>q 40 | app.quit 41 | 42 |
43 |
44 |
45 | """ 46 | 47 | 48 | def ensure(path): 49 | """ 50 | Ensures that a path exists by creating it if necessary. 51 | 52 | Note: ``os.makedirs()`` in Python 3 has the option ``exist_ok`` which will 53 | not throw an error if the directory already exist (making this function 54 | unnecessary). However, Python 2 has no such option. 55 | 56 | :type path: str 57 | :param path: path to be created if necessary 58 | :rtype: str 59 | :returns: input path (allowing functions like os.path.join to be wrapped) 60 | """ 61 | if not os.path.exists(path): 62 | os.makedirs(path) 63 | return path 64 | 65 | 66 | class Application(Gtk.Application): 67 | data_dir = ensure(appdirs.user_data_dir(appname=info.SHORT_NAME)) 68 | cache_dir = ensure(appdirs.user_cache_dir(appname=info.SHORT_NAME)) 69 | # log_dir = ensure(appdirs.user_log_dir(appname=info.SHORT_NAME)) 70 | docsets = {} 71 | 72 | def __init__(self, package, version, pkgdatadir): 73 | Gtk.Application.__init__(self, application_id="com.sarkhelk.tarpon", 74 | flags=Gio.ApplicationFlags.FLAGS_NONE) 75 | self.package = package 76 | self.version = version 77 | self.pkgdatadir = pkgdatadir 78 | search_paths = glob.glob(self.data_dir + "/*.docset") 79 | search_paths.extend(glob.glob(self.cache_dir + "/*.json")) 80 | self.__choices = [] 81 | self.load_docsets(search_paths) 82 | 83 | @property 84 | def choices(self): 85 | return self.__choices 86 | 87 | def __new_window(self): 88 | window = TarponWindow(self) 89 | window.show_all() 90 | self.add_window(window) 91 | 92 | def do_activate(self): 93 | self.__new_window() 94 | 95 | def do_startup(self): 96 | Gtk.Application.do_startup(self) 97 | 98 | if self.prefers_app_menu(): 99 | print("prefers app_menu") 100 | builder = Gtk.Builder() 101 | builder.add_from_string(APP_MENU) 102 | self.set_app_menu(builder.get_object("menu")) 103 | else: 104 | builder = Gtk.Builder() 105 | print(views(self.pkgdatadir, "menubar.ui")) 106 | builder.add_from_file(views(self.pkgdatadir, "menubar.ui")) 107 | self.set_menubar(builder.get_object("menu")) 108 | 109 | new_window_action = Gio.SimpleAction.new("new_window", None) 110 | new_window_action.connect("activate", self.on_new_window) 111 | self.add_action(new_window_action) 112 | 113 | quit_action = Gio.SimpleAction.new("quit", None) 114 | quit_action.connect("activate", self.on_quit) 115 | self.add_action(quit_action) 116 | 117 | preferences_action = Gio.SimpleAction.new("preferences", None) 118 | preferences_action.connect("activate", self.on_preferences) 119 | self.add_action(preferences_action) 120 | 121 | about_action = Gio.SimpleAction.new("about", None) 122 | about_action.connect("activate", self.on_about) 123 | self.add_action(about_action) 124 | 125 | def load_docsets(self, paths): 126 | for path in paths: 127 | if path.endswith(".docset"): # load from disk 128 | docset = Docset.frompath(path) 129 | self.docsets[docset.name] = docset 130 | self.__choices.extend(docset.items) 131 | elif path.endswith(".json"): # load from cache files 132 | with open(path) as cache_file: 133 | for name, url in json.load(cache_file).iteritems(): 134 | if name in self.docsets: 135 | self.docsets[name].url = url 136 | else: 137 | self.docsets[name] = Docset(name, url=url) 138 | 139 | @property 140 | def docsets_on_disk(self): 141 | return filter(lambda x: x[1].on_disk, self.docsets.iteritems()) 142 | 143 | def on_new_window(self, action, parameter): 144 | self.__new_window() 145 | 146 | def on_quit(self, action, parameter): 147 | self.quit() 148 | 149 | def on_about(self, action, parameter, transient_for=None): 150 | builder = Gtk.Builder() 151 | if self.prefers_app_menu(): 152 | builder.add_from_file(views(self.pkgdatadir, "about_dialog_hb.ui")) 153 | else: 154 | builder.add_from_file(views(self.pkgdatadir, "about_dialog.ui")) 155 | 156 | about_dialog = builder.get_object("about_dialog") 157 | if transient_for: 158 | about_dialog.set_transient_for(transient_for) 159 | about_dialog.run() 160 | about_dialog.destroy() 161 | 162 | def on_preferences(self, action, parameter): 163 | pass 164 | -------------------------------------------------------------------------------- /src/tarpon_app/docsets.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from collections import namedtuple 5 | import os 6 | import plistlib 7 | from unicodedata import normalize 8 | 9 | import peewee 10 | 11 | 12 | class InvalidDocsetException(Exception): 13 | pass 14 | 15 | 16 | DocItem = namedtuple("DocItem", ["name", "data_type", "path"]) 17 | 18 | 19 | def index_model(db): 20 | class SearchIndex(peewee.Model): 21 | name = peewee.TextField() 22 | data_type = peewee.TextField(db_column="type") 23 | path = peewee.TextField() 24 | 25 | @property 26 | def item(self): 27 | return DocItem(normalize("NFKD", unicode(self.name)), 28 | str(self.data_type), str(self.path)) 29 | 30 | def __str__(self): 31 | return str(self.item) 32 | 33 | class Meta: 34 | db_table = "searchIndex" 35 | database = db 36 | 37 | return SearchIndex 38 | 39 | 40 | class Docset: 41 | def __init__(self, name, url=None, path=None): 42 | self.name = name 43 | self.url = url 44 | self.path = path 45 | self.identifier = None 46 | self.index_path = None 47 | self.icon_url = None 48 | self.icon_path = None 49 | self._items = None 50 | self._doc_path = None 51 | if path and self.on_disk: 52 | self.read_docset() 53 | 54 | @property 55 | def on_disk(self): 56 | return self.path and os.path.exists(self.path) 57 | 58 | @property 59 | def db_path(self): 60 | if self.on_disk: 61 | return os.path.join(self.path, "Contents/Resources/docSet.dsidx") 62 | 63 | @property 64 | def doc_path(self): 65 | if self.on_disk: 66 | self._doc_path = os.path.join(self.path, 67 | "Contents/Resources/Documents/") 68 | return self._doc_path 69 | 70 | @property 71 | def items(self): 72 | if not self._items: 73 | db = peewee.SqliteDatabase(self.db_path, threadlocals=True) 74 | db.connect() 75 | self._items = [a.item for a in index_model(db).select()] 76 | db.close() 77 | return self._items 78 | 79 | def read_docset(self): 80 | if self.on_disk: 81 | plist_path = os.path.join(self.path, "Contents", "Info.plist") 82 | if os.path.exists(plist_path): 83 | pl = plistlib.readPlist(plist_path) 84 | if pl["isDashDocset"]: 85 | self.name = pl["CFBundleName"] 86 | self.identifier = pl["CFBundleIdentifier"] 87 | self.index_path = os.path.join(self.doc_path, 88 | pl["dashIndexFilePath"]) 89 | else: 90 | InvalidDocsetException( 91 | "isDashDocset is not True in {0}".format(plist_path)) 92 | else: 93 | raise InvalidDocsetException("{0} not found".format(plist_path)) 94 | else: 95 | raise InvalidDocsetException("Docset is not on disk") 96 | 97 | def __str__(self): 98 | return "".format(self.name) 99 | 100 | @classmethod 101 | def frompath(cls, path): 102 | plist_path = os.path.join(path, "Contents", "Info.plist") 103 | if os.path.exists(plist_path): 104 | pl = plistlib.readPlist(plist_path) 105 | if pl["isDashDocset"]: 106 | new_docset = cls(pl["CFBundleName"], path=path) 107 | return new_docset 108 | else: 109 | InvalidDocsetException( 110 | "isDashDocset is not True in {0}".format(plist_path)) 111 | else: 112 | raise InvalidDocsetException("{0} not found".format(plist_path)) 113 | -------------------------------------------------------------------------------- /src/tarpon_app/gtk/Makefile.am: -------------------------------------------------------------------------------- 1 | tarpon_gtk_PYTHON = \ 2 | components.py \ 3 | __init__.py 4 | 5 | tarpon_gtkdir = $(pythondir)/tarpon_app/gtk 6 | -------------------------------------------------------------------------------- /src/tarpon_app/gtk/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /src/tarpon_app/gtk/components.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | 6 | from gi.repository import Gdk, Gio, Gtk, WebKit 7 | from fuzzywuzzy.process import extractBests as search 8 | 9 | 10 | def views(pkgdatadir, path): 11 | """ 12 | Gets absolute path of view from pkgdatadir. 13 | 14 | :type pkgdatadir: str 15 | :param pkgdatadir: package data directory 16 | :type path: str 17 | :param path: relative path 18 | :rtype: str 19 | :returns: absolute representation of relative path 20 | """ 21 | return os.path.abspath(os.path.join(pkgdatadir, "ui", path)) 22 | 23 | 24 | def toolbar_button(themed_icon, button_class): 25 | """ 26 | Create new Gtk.Button or Gtk.ToogleButton from a stock Gtk icon name 27 | 28 | :param themed_icon: stock Gtk icon name 29 | :type themed_icon: str 30 | :param button_class: ``Gtk.Button`` or ``Gtk.ToggleButton`` 31 | :type button_class: classobj 32 | :return: ``Gtk.Button`` or ``Gtk.ToggleButton`` according to button_class 33 | """ 34 | image = Gtk.Image() 35 | image.set_from_gicon( 36 | Gio.ThemedIcon.new_with_default_fallbacks(themed_icon), 37 | Gtk.IconSize.SMALL_TOOLBAR 38 | ) 39 | return button_class(None, image=image) 40 | 41 | 42 | class Titlebar(Gtk.HeaderBar): 43 | def __init__(self, title=None, subtitle=None, show_close_button=True): 44 | super(Gtk.HeaderBar, self).__init__() 45 | if title: 46 | self.set_title(title) 47 | if subtitle: 48 | self.set_subtitle(title) 49 | self.set_show_close_button(True) 50 | 51 | def __add_buttons(self, box, buttons, linked, spacing): 52 | """ 53 | Add linked or non-linked buttons to Titlebar 54 | 55 | :type box: Gtk.Box 56 | """ 57 | box.set_spacing(spacing) 58 | if linked: 59 | Gtk.StyleContext.add_class(box.get_style_context(), "linked") 60 | for button in buttons: 61 | box.add(button) 62 | 63 | def add_buttons_to_left(self, buttons, linked=False, spacing=6): 64 | """ 65 | Add button(s) to left of title. If there is only one button, the button 66 | will be added on its own. If more than one button is provided, the 67 | buttons will be added to a Gtk.Box container before being add left of 68 | the title. 69 | 70 | :param buttons: a list of Gtk.Button elements to be added 71 | :param linked: True if the buttons should be linked together 72 | :param spacing: spacing in between buttons in pixels 73 | """ 74 | if len(buttons) > 1: 75 | box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) 76 | self.pack_start(box) 77 | self.__add_buttons(box, buttons, linked, spacing) 78 | elif buttons: 79 | self.pack_start(buttons) 80 | 81 | def add_buttons_to_right(self, buttons, linked=False, spacing=6): 82 | """ 83 | Add button(s) to right of title. If there is only one button, the 84 | button will be added on its own. If more than one button is provided, 85 | the buttons will be added to a Gtk.Box container before being add right 86 | of the title. 87 | 88 | :param buttons: a list of Gtk.Button elements to be added 89 | :param linked: True if the buttons should be linked together 90 | :param spacing: spacing in between buttons in pixels 91 | """ 92 | if len(buttons) > 1: 93 | box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) 94 | self.pack_end(box) 95 | self.__add_buttons(box, buttons, linked, spacing) 96 | elif buttons: 97 | self.pack_end(buttons) 98 | 99 | 100 | class WebNotebook(Gtk.Notebook): 101 | def __init__(self, new_tab_page=None): 102 | super(Gtk.Notebook, self).__init__() 103 | self.new_tab_page = new_tab_page 104 | 105 | def new_tab(self, widget, data=None, uri=None): 106 | """Create a new tab.""" 107 | # TODO: Hiding tab bar if only one tab is present should be an option. 108 | if self.get_n_pages() < 1: 109 | self.set_show_tabs(False) 110 | else: 111 | self.set_show_tabs(True) 112 | 113 | sw = Gtk.ScrolledWindow() 114 | wv = WebKit.WebView() 115 | sw.add(wv) 116 | 117 | if uri: 118 | wv.load_uri(uri) 119 | elif self.new_tab_page: 120 | wv.load_uri(self.new_tab_page) 121 | 122 | tab_label = Gtk.Label("Tab {0}".format(self.get_n_pages() + 1)) 123 | self.append_page(sw, tab_label) 124 | self.show_all() 125 | 126 | @property 127 | def browser(self): 128 | """ 129 | Gets the ``WebKit.WebView`` for the current tab. 130 | 131 | :type self: WebKit.WebView 132 | :return: ``WebKit.WebView`` form current tab 133 | """ 134 | return self.get_nth_page(self.get_current_page()).get_child() 135 | 136 | def go_back(self, widget, data=None): 137 | """Return to the previous page in the current tab.""" 138 | self.browser.go_back() 139 | 140 | def go_forward(self, widget, data=None): 141 | """Return to the previous page in the next tab.""" 142 | self.browser.go_forward() 143 | 144 | def zoom_in(self, widget, data=None): 145 | """Increase the font size of the text on the page.""" 146 | self.browser.zoom_in() 147 | 148 | def zoom_out(self, widget, data=None): 149 | """Decrease the font size of the text on the page.""" 150 | self.browser.zoom_out() 151 | 152 | def reset_zoom(self, widget, data=None): 153 | """Reset the font size of the text on the page.""" 154 | self.browser.set_zoom_level(1.0) 155 | 156 | 157 | class TarponWindow(Gtk.ApplicationWindow): 158 | def __init__(self, application): 159 | self.__application = application 160 | Gtk.Window.__init__(self, title="Tarpon", application=application) 161 | self.set_default_size(800, 600) 162 | self.set_gravity(Gdk.Gravity.CENTER) 163 | self.set_position(Gtk.WindowPosition.CENTER) 164 | 165 | self.build_bars() 166 | if self.__application.prefers_app_menu(): 167 | self.set_titlebar(self.__header) 168 | else: 169 | print("We should show a toolbar since app menus are not preferred") 170 | 171 | self.build_sidebar() 172 | self.__web_notebook = WebNotebook() 173 | self.__web_notebook.new_tab(None) 174 | 175 | self.__wrapper = Gtk.Box(Gtk.Orientation.VERTICAL) 176 | self.__content = Gtk.Paned() 177 | self.__content.add1(self.__sidebar) 178 | self.__content.add2(self.__web_notebook) 179 | self.__content.set_position(200) 180 | self.__wrapper.add(self.__content) 181 | self.add(self.__wrapper) 182 | 183 | self.connect_signals() 184 | self.show_all() 185 | 186 | def build_bars(self): 187 | self.__header = Titlebar(title="Tarpon", show_close_button=True) 188 | if hasattr(Gtk.HeaderBar, 'set_decoration_layout'): 189 | self.__header.set_decoration_layout(":close") 190 | 191 | self.__back = Gtk.Button() 192 | self.__back.add(Gtk.Arrow(Gtk.ArrowType.LEFT, Gtk.ShadowType.NONE)) 193 | self.__forward = Gtk.Button() 194 | self.__forward.add(Gtk.Arrow(Gtk.ArrowType.RIGHT, Gtk.ShadowType.NONE)) 195 | self.__new_tab = toolbar_button("tab-new-symbolic", Gtk.Button) 196 | self.__search = toolbar_button("edit-find-symbolic", Gtk.ToggleButton) 197 | self.__menu = toolbar_button("open-menu-symbolic", Gtk.MenuButton) 198 | 199 | builder = Gtk.Builder() 200 | builder.add_from_file(views(self.__application.pkgdatadir, "menu.ui")) 201 | if hasattr(Gtk, 'Popover'): 202 | popover = Gtk.Popover.new_from_model(self.__menu, 203 | builder.get_object("menu")) 204 | self.__menu.set_popover(popover) 205 | else: 206 | print("Gtk.Popover not supported. Using menu model.") 207 | self.__menu.set_menu_model(builder.get_object("menu")) 208 | 209 | # Add buttons to header 210 | # TODO: Add buttons to a toolbar instead of Titlebar if using Unity. 211 | # Unity attaches the menu to the top of the screen, so Gtk.HeaderBar 212 | # looks out of place and a button for opening the menu is unnecessary. 213 | self.__header.add_buttons_to_left((self.__back, self.__forward), 214 | linked=True, spacing=0) 215 | self.__header.add_buttons_to_right((self.__new_tab, self.__menu)) 216 | 217 | def build_sidebar(self): 218 | # TODO: Refactor build_sidebar() into its own "Sidebar" component 219 | self.__sidebar = Gtk.Box.new(Gtk.Orientation.VERTICAL, 6) 220 | self.__sidebar.set_homogeneous(False) 221 | self.__results = None 222 | self.__sidescroll = Gtk.ScrolledWindow() 223 | self.__sidebar_store = Gtk.TreeStore(str) 224 | self.__sidebar_filter = self.__sidebar_store.filter_new() 225 | self.__sidebar_filter.set_visible_func(self.filter_func) 226 | for name, docset in self.__application.docsets_on_disk: 227 | treeiter = self.__sidebar_store.append(None, [name]) 228 | type_rows = {} 229 | for item in docset.items: 230 | if item.data_type not in type_rows: 231 | type_rows[item.data_type] = self.__sidebar_store.append(treeiter, [item.data_type]) 232 | self.__sidebar_store.append(type_rows[item.data_type], 233 | [item.name]) 234 | self.__treeview = Gtk.TreeView.new_with_model(self.__sidebar_filter) 235 | renderer = Gtk.CellRendererText() 236 | column = Gtk.TreeViewColumn(None, renderer, text=0) 237 | self.__treeview.append_column(column) 238 | self.__treeview.set_headers_visible(False) 239 | self.__treeview.set_activate_on_single_click(True) 240 | 241 | self.__search = Gtk.SearchEntry() 242 | self.__sidescroll.add(self.__treeview) 243 | self.__sidescroll.set_vexpand(True) 244 | 245 | self.__sidebar.pack_start(self.__search, False, False, 0) 246 | self.__sidebar.pack_end(self.__sidescroll, True, True, 0) 247 | 248 | def connect_signals(self): 249 | self.connect("destroy", self.on_quit) 250 | self.__back.connect("clicked", self.__web_notebook.go_back) 251 | self.__forward.connect("clicked", self.__web_notebook.go_forward) 252 | self.__new_tab.connect("clicked", self.__web_notebook.new_tab) 253 | self.__treeview.connect("row-activated", self.docitem_selected) 254 | self.__search.connect("search-changed", self.search_docsets) 255 | 256 | new_tab_action = Gio.SimpleAction.new("new_tab") 257 | new_tab_action.connect("activate", self.on_new_tab) 258 | self.add_action(new_tab_action) 259 | 260 | new_window_action = Gio.SimpleAction.new("new_window") 261 | new_window_action.connect("activate", self.on_new_window) 262 | self.add_action(new_window_action) 263 | 264 | about_action = Gio.SimpleAction.new("about") 265 | about_action.connect("activate", self.on_about) 266 | self.add_action(about_action) 267 | 268 | quit_action = Gio.SimpleAction.new("quit") 269 | quit_action.connect("activate", self.on_quit) 270 | self.add_action(quit_action) 271 | 272 | toggle_panel_action = Gio.SimpleAction.new("toggle_panel") 273 | toggle_panel_action.connect("activate", self.toggle_panel) 274 | self.add_action(toggle_panel_action) 275 | 276 | toggle_searchbar_action = Gio.SimpleAction.new("toggle_searchbar") 277 | toggle_searchbar_action.connect("activate", self.toggle_searchbar) 278 | self.add_action(toggle_searchbar_action) 279 | 280 | larger_text_action = Gio.SimpleAction.new("larger_text") 281 | larger_text_action.connect("activate", self.larger_text) 282 | self.add_action(larger_text_action) 283 | 284 | smaller_text_action = Gio.SimpleAction.new("smaller_text") 285 | smaller_text_action.connect("activate", self.smaller_text) 286 | self.add_action(smaller_text_action) 287 | 288 | normal_text_action = Gio.SimpleAction.new("normal_text") 289 | normal_text_action.connect("activate", self.normal_text) 290 | self.add_action(normal_text_action) 291 | 292 | def search_docsets(self, widget): 293 | # TODO: We should move this off of the main thread for performance 294 | query = widget.get_text().strip() 295 | if query: 296 | self.__results = search(query, self.__application.choices, 297 | processor=lambda x: x.name) 298 | else: 299 | self.__results = None 300 | self.__sidebar_filter.refilter() 301 | 302 | 303 | def docitem_selected(self, widget, path, column): 304 | """Change the browser page when an item is selected from the sidebar.""" 305 | # The tree has 3 levels: docset, data type (function, class, etc.), and 306 | # document item. If we select a docset (top-level or path length 1), we 307 | # would like to browse to the index for that docset. If we select a data 308 | # type such as function or class (path length 2), we should do nothing. 309 | # If we select a document item (path length 3), we should browse to the 310 | # path on disk associated with that item. 311 | # TODO: Refactor docitem_selected to be more understandable. 312 | if len(path) == 2: 313 | return None 314 | treeiter = self.__sidebar_filter.get_iter(path) 315 | value = self.__sidebar_filter.get_value(treeiter, 0) 316 | if len(path) == 1: 317 | docset = self.__application.docsets[value] 318 | self.__web_notebook.browser.load_uri("file://" + docset.index_path) 319 | elif len(path) == 3: 320 | type_iter = self.__sidebar_filter.iter_parent(treeiter) 321 | data_type = self.__sidebar_filter.get_value(type_iter, 0) 322 | parent_iter = self.__sidebar_filter.iter_parent(type_iter) 323 | parent = self.__sidebar_filter.get_value(parent_iter, 0) 324 | docset = self.__application.docsets[parent] 325 | print(parent, data_type, value) 326 | for item in docset.items: 327 | if item.name == value and item.data_type == data_type: 328 | page = os.path.join(docset.doc_path, item.path) 329 | self.__web_notebook.browser.load_uri("file://" + page) 330 | return None 331 | 332 | def filter_func(self, model, treeiter, data): 333 | if self.__results: 334 | if model.iter_has_child(treeiter): 335 | return True 336 | row = model[treeiter][0] 337 | for result, score in self.__results: 338 | # TODO: This comparison can result in a UnicodeWarning that 339 | # automatically resolves to False no matter what because we are 340 | # not making sure both result.name and row can both be decoded 341 | # to Unicode. We need to make utf-8 a strong guarantee. 342 | if result.name == row: 343 | self.__treeview.expand_to_path(model.get_path(treeiter)) 344 | return True 345 | return False 346 | return True 347 | 348 | 349 | def on_new_window(self, action, parameter): 350 | self.__application.on_new_window(action, parameter) 351 | 352 | def on_new_tab(self, action, parameter): 353 | self.__web_notebook.new_tab(None) 354 | 355 | def on_about(self, action, parameter): 356 | self.__application.on_about(action, parameter, transient_for=self) 357 | 358 | def on_quit(self, widget, data=None): 359 | self.destroy() 360 | 361 | def toggle_panel(self, widget, data=None): 362 | if self.__sidebar.is_visible(): 363 | self.__sidebar.hide() 364 | else: 365 | self.__sidebar.show() 366 | 367 | def toggle_searchbar(self, widget, data=None): 368 | if self.__search.is_visible(): 369 | self.__search.hide() 370 | else: 371 | self.__search.show() 372 | 373 | def larger_text(self, widget, data=None): 374 | self.__web_notebook.zoom_in(widget, data) 375 | 376 | def smaller_text(self, widget, data=None): 377 | self.__web_notebook.zoom_out(widget, data) 378 | 379 | def normal_text(self, widget, data=None): 380 | self.__web_notebook.reset_zoom(widget, data) 381 | -------------------------------------------------------------------------------- /src/tarpon_app/info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """Various information about Tarpon. Should be updated for every release. 5 | Contains the information displayed in tarpon's credits.""" 6 | 7 | SHORT_NAME = "tarpon" 8 | NAME = "Tarpon" 9 | URL = "https://github.com/techwizrd/tarpon" 10 | EMAIL = "kksarkhel@bluedevs.net" 11 | VERSION = "0.1-development" 12 | 13 | SHORT_DESCRIPTION = "Offline documentation browser" 14 | 15 | # CREDITS 16 | AUTHORS = ["Main developers:", 17 | "\tKunal Sarkhel ", 18 | ] 19 | 20 | ARTISTS = [] 21 | ARTISTS.sort() 22 | TRANSLATORS = """""" 23 | -------------------------------------------------------------------------------- /tarpon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | import os 6 | 7 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) 8 | 9 | from tarpon_app.application import Application 10 | 11 | 12 | def main(): 13 | """Starts the Tarpon application""" 14 | app = Application(package="tarpon", version="0 (debug)", 15 | pkgdatadir=os.path.join(os.path.dirname(__file__), 16 | 'data')) 17 | exit_status = app.run(sys.argv) 18 | sys.exit(exit_status) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | --------------------------------------------------------------------------------