changeset 14:a62fbff067f8

start bringing this whole thing up to speed
author Jeff Hammel <jhammel@mozilla.com>
date Wed, 01 May 2013 06:56:12 -0700
parents f11ce7b1a349
children 1281d999618c
files README.txt setup.py smartopen/handlers.py smartopen/smartopen.py
diffstat 4 files changed, 230 insertions(+), 32 deletions(-) [+]
line wrap: on
line diff
--- a/README.txt
+++ b/README.txt
@@ -1,13 +1,14 @@
 smartopen
 =========
 
 smartopen is a command line program that will transform text according
-to handlers to URLs and open them 
+to handlers to URLs and open them
+
 
 Usage
 -----
 
 ``smartopen --help`` displays the program usage:
 
 """
 Usage: smartopen [options]
@@ -38,29 +39,72 @@ setuptools entry point ``[smartopen.loca
 for the handlers.  Several handlers come with smartopen, or you can
 write your own.
 
 URL:  a resolvable URL
 Google: a google search
 Wikipedia: a Wikipedia article
 GoogleMaps: an address in google maps
 
+etc.
+
 
 Configuration
 -------------
 
 smartopen uses a configuration file that defines the order of the
 handlers and their behavior.
 
+A simple example is shown here:
+http://k0s.org/hg/smartopen/file/tip/smartopen.ini
+
 
 Interfacing With Your Environment
 ---------------------------------
 
 While smartopen is useful of its own right, its utility may be
 enhanced by using as part of an environment.  For instance, I run the
 fluxbox window manager which allows the use of hotkeys.  By binding
-smartopen to Ctrl+Alt+s and using xclip, I can open a URL that is
+smartopen to ``Ctrl+Alt+s`` and using ``xclip``, I can open a URL that is
 mappable to a smartopen handler with a keyboard shortcut.  The
-relevant line from my ~/.fluxbox/keys file reads:
+relevant line from my ``~/.fluxbox/keys`` file reads:
 
 Control Mod1 s :ExecCommand /home/jhammel/python/smartopen.py "$(xclip -o)" # smartopen
 
 
+----
+
+
+Posterity is gained...
+----------------------
+
+I mostly wrote smartopen to help me.  That it has.
+However...a few things....
+
+- better way of setting up handlers?
+- really, i want to push down to choose what i think the handler is,
+  but only should we I release the hand that Jesus becomes Magic
+  (sorry, I hate the movie too)
+
+....and more?
+
+Was reading about catfish....e.g.
+http://www.addictivetips.com/ubuntu-linux-tips/catfish-file-searching-tool-for-ubuntu-linux/
+...
+
+"""
+file search tool that support several different engines
+ 
+A file search tool using different backends which is configurable via
+the command line.
+
+This program acts as a frontend for different file search engines.
+The interface is intentionally lightweight and simple. But it takes
+configuration options from the command line.
+
+Currently find, locate, tracker, strigi, pinot, and beagle are
+supported as backends
+""".
+
+Should smartopen....be a pluggable thing that takes text and finds
+both *the handler* and *what to do with it*???
+
+I mean, short of it being a neural network anyway....
--- a/setup.py
+++ b/setup.py
@@ -1,17 +1,17 @@
 from setuptools import setup, find_packages
 import sys, os
 
 try:
-    description = file('README.txt').read()
+    description = file(os.path.join(os.path.dirname(__file__), 'README.txt')).read()
 except:
     description = ''
 
-version = '0.1.6'
+version = '0.2'
 
 setup(name='smartopen',
       version=version,
       description="open text in a browser contextually",
       long_description=description,
       classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
       keywords='',
       author='Jeff Hammel',
@@ -34,10 +34,11 @@ setup(name='smartopen',
       GoogleMaps = smartopen.handlers:GoogleMaps
       Wikipedia = smartopen.handlers:Wikipedia
       Wiktionary = smartopen.handlers:Wiktionary
       Google = smartopen.handlers:Google
       Trac = smartopen.handlers:Trac
       Bugzilla = smartopen.handlers:Bugzilla
       FedEx = smartopen.handlers:FedEx
       MercurialRevision = smartopen.handlers:MercurialRevision
+      UbuntuPackage = smartopen.handlers:UbuntuPackage
       """,
       )
--- a/smartopen/handlers.py
+++ b/smartopen/handlers.py
@@ -1,20 +1,22 @@
 import address
 import string
 import urllib
 import urllib2
+from subprocess import call
 
 class Location(object):
     """
     generic class for locations
     """
 
     def __init__(self, baseurl=""):
         self.baseurl = baseurl
+        # should/could strip here?
 
     def url(self, query):
         return self.baseurl + self.process(query)
 
     def process(self, query):
         return query
 
     def test(self, query):
@@ -75,27 +77,27 @@ class Trac(Location):
             if query[1:].isdigit():
                 return 'changeset/' + str(query[1:])
         if query[0] == '#':
             if query[1:].isdigit():
                 return 'ticket/' + str(query[1:])
 
     def test(self, query):
         return bool(self.process(query))
-        
+
 
 class Wikipedia(Location):
     """try to open the query in wikipedia"""
-    def __init__(self):        
+    def __init__(self):
         wikiurl = 'http://en.wikipedia.org/wiki/'
         Location.__init__(self, wikiurl)
 
     def process(self, query):
         return urllib.quote_plus('_'.join(query.split()))
-        
+
     def test(self, query):
         'test to see if the article exists'
 
         # need a phony user agent so wikipedia won't know we're a bot
         headers = {}
         headers['User-Agent'] = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.8.0.4) Gecko/20060508 Firefox/1.5.0.4'
 
         request = urllib2.Request(self.url(query), None, headers)
@@ -113,45 +115,45 @@ class Wiktionary(Location):
         baseurl = 'http://en.wiktionary.org/wiki/'
         Location.__init__(self, baseurl)
     def test(self, query):
         for c in (' ', '\n', '/'):
             if c in query:
                 return False
         if self.exists(self.url(query)):
             return True
-        
+
 class Bugzilla(Location):
     def __init__(self):
         baseurl = 'https://bugzilla.mozilla.org/show_bug.cgi?id='
         Location.__init__(self, baseurl)
-    
+
     def test(self, query):
         try:
             int(query)
             return True
         except:
             return False
 
 class Google(Location):
-    def __init__(self):        
+    def __init__(self):
         googleurl = 'http://www.google.com/search?hl=en&q='
         Location.__init__(self, googleurl)
-        
+
     def process(self, query):
         return urllib.quote_plus(query)
 
 class FedEx(Location):
     def __init__(self):
         baseurl = 'http://www.fedex.com/Tracking?cntry_code=us&language=english&tracknumber_list='
         Location.__init__(self, baseurl)
 
     def process(self, query):
         if query.count(' ') == 2:
-            query = ''.join(query.split(' '))        
+            query = ''.join(query.split(' '))
         return query
 
     def test(self, query):
         query = self.process(query)
         if len(query) != 12:
             return False
         try:
             int(query)
@@ -165,10 +167,50 @@ class MercurialRevision(Location):
         Location.__init__(self, baseurl)
 
     def test(self, query):
         query = set(query)
         if query.issubset(string.digits + 'abcdef'):
             return True
         return False
 
+class UbuntuPackage(Location):
+    def __init__(self):
+        baseurl = 'http://packages.ubuntu.com/'
+        Location.__init__(self, baseurl)
+
+    def test(self, query):
+        if len(self.baseurl.strip().split()) > 1:
+            return False # no spaces in packages
+
+        # use `apt-cache show` for the package name
+        # the output could be captured:, e.g.
+        #         ...
+        # Filename: pool/main/h/hello/hello_2.8-2_amd64.deb
+        # Size: 28124
+        # MD5sum: 73ad59a7920e2adcff9b84c00e38418a
+        # SHA1: 70ef927cfa40b8e367f9ca7821e0064a946726c5
+        # SHA256: f8341711f6b803e032be9aff2206dfdb4045a0c6c296b0ea0d310d415f10ff4d
+        # Description-en: The classic greeting, and a good example
+        #  The GNU hello program produces a familiar, friendly greeting.  It
+        #  allows non-programmers to use a classic computer science tool which
+        #  would otherwise be unavailable to them.
+        #  .
+        #  Seriously, though: this is an example of how to do a Debian package.
+        #  It is the Debian version of the GNU Project's `hello world' program
+        #  (which is itself an example for the GNU Project).
+        # Homepage: http://www.gnu.org/software/hello/
+        # Description-md5: b7df6fe7ffb325083a3a60819a7df548
+        # Bugs: https://bugs.launchpad.net/ubuntu/+filebug
+        # Origin: Ubuntu
+        # Supported: 18m
+
+        # in theory both the home page and the ubuntu page could be interesting
+        # (different handlers?)
+
+        returncode = call(['apt-cache', 'show', self.baseurl])
+        if returncode:
+            return False
+
 # TODO:
+# - https://mozillians.org/en-US/u/jhammel/ handler
 # - https://mozillians.org/en-US/u/williamr/ handler
+# ... but no 404???
--- a/smartopen/smartopen.py
+++ b/smartopen/smartopen.py
@@ -1,137 +1,248 @@
 #!/usr/bin/env python
 
-""" smart open the data passed in """
+"""
+open what you give in your browser
+"""
 
 import os
+import subprocess
 import sys
 
 from optparse import OptionParser
 from pkg_resources import iter_entry_points
 from ConfigParser import ConfigParser
 
-def locations(names=None, config=None):
+
+### method to open a browser
+
+### It turns out finding the default browser is very hard :(
+# webbrowser is standard...just...what does that mean?  for gnome-terminal,
+# I get firefox;  however, despite what
+# http://stackoverflow.com/questions/4216985/python-call-to-operating-system-to-open-url
+# says (and also because of it), I actually get `links` from xdg-open(!)
+# (which of course it doesn't even tell me)
+
+# xdg-settings --list
+# Known properties:
+#   default-web-browser           Default web browser
+# xdg-settings get default-web-browser
+# xdg-settings: unknown desktop environment
+
+# The solution....is here:
+# http://askubuntu.com/questions/96080/how-to-set-google-chrome-as-the-default-browser/96081#96081
+# and indeed in ~/.local/share/applications/mimeapps.list
+# I do have::
+
+#  text/html=firefox.desktop;thunderbird.desktop;
+
+# in `[Added Associations]`
+# (fwiw, ``pprint(mailcap.getcaps())`` was interesting but not very useful, 
+# referencing /usr/bin/sensible-browser which is in fact links (!)
+
+# TODO:
+
+# probably some fancy module should be written that *actually* does what
+# its supposed to here; maybe https://pypi.python.org/pypi/desktop is that
+# fancy pants module; I don't know! pypi doesn't let me browse tarballs!
+# in any case, this is mostly for me and it must be portable;
+
+# So what we'll actually do:
+# (Not surprisingly, explicit wins)
+# - if specified on the command line (which it can't be yet) that wins
+# - use browser/BROWSER if set in config
+# - if not...you're back to guessing
+# - if `which` was stdlib I'd probably guess on that since I know I can't rely
+#   on `xdg-open`...
+# - ...and there are other platforms; again, I don't care....
+# - so we're back to Firefox being #1
+
+# Which despite my love for Firefox, I'd love to help with autoselection if
+# anyone would throw their hat in
+
+# * let's not forget http://freedesktop.org/wiki/Software/pyxdg if we're
+# talking about non-stdlib solutions for linux only
+
+def open_in_browser(url, browser=None):
+    """yeah, that's right..."""
+
+    if browser is None:
+        browser = 'firefox' # we'll cheat because of course its easy
+
+    # control via env variable; might as well keep it set o_O
+    os.environ.setdefault('BROWSER', browser)
+    # see e.g. http://hg.python.org/cpython/file/2.7/Lib/webbrowser.py
+    # sadly these are different::
+    # xdg-open http://k0s.org/
+    # BROWSER=firefox xdg-open http://k0s.org/
+
+    # now invoke the damn thing
+    import webbrowser
+    return webbrowser.open(url) # XXX lord almighty
+    # why bother returning it even?
+
+
+### methods for inspecting the locations
+
+def locations(names=None, config=None, verbose=False):
     """
     list of 2-tuples of location handlers;
     * names: order names of handlers
     * config: nested dictionary of configuration from names
     """
 
+    # setup
     _handlers = {}
     _names = []
     if config is None:
         config = {}
-    
+
+    # load the handlers
+    # (really, these should come of a dict that is shared with an entry point)
     for i in iter_entry_points('smartopen.locations'):
         try:
             handler = i.load()
-        except:
-            continue # TODO: warn/debug with --verbose flag
+        except Exception, e:
+            if verbose:
+                print >> sys.stderr,  "Error loading handler:\n%s" % e
+            continue
         _handlers[i.name] = handler
         if not names:
             _names.append(i.name)
 
     if not names:
         names = _names
     handlers = []
     for name in names:
         if ':' in name:
             _name, section = name.split(':', 1)
         else:
             _name = name
         if _name in _handlers:
             try:
                 handler = _handlers[_name](**config.get(name, {}))
-            except:
+            except Exception, e:
+                if verbose:
+                    print >> sys.stderr, "blah  blah blah"
                 continue
             handlers.append((name, handler))
     return handlers
 
+
 def urls(query, handlers=None):
+    """returns available urls in order of preference for a query"""
+
     if handlers is None:
         handlers = locations()
     urls = []
     for name, handler in handlers:
         if handler.test(query):
             urls.append((name, handler.url(query)))
     return urls
 
+
 def url(query, handlers=None):
+    """return the top url for a query"""
+
     if handlers is None:
         handlers = locations()
     for name, handler in handlers:
         if handler.test(query):
             return handler.url(query)
 
+### command line entry point
+
 def main(args=sys.argv[1:]):
 
     # parse command line optioins
-    parser = OptionParser()
+    default_browser = os.environ.get('BROWSER', 'firefox') # ^ see LONG note above
+    usage = '%prog [options] query'
+    parser = OptionParser(usage=usage)
+    parser.add_option('-b', '--browser', dest='browser',
+                      default=None,
+                      help="browser to use; can also be set in config and BROWSER [default: %default]")
     parser.add_option('-c', '--config', dest="config",
                       help="config file to read")
-    parser.add_option('-u', '--url', dest="url", 
+    parser.add_option('-u', '--url', dest="url",
                       action='store_true', default=False,
                       help="print the first url handled")
-    parser.add_option('-a', '--all', dest="all", 
+    parser.add_option('-a', '--all', dest="all",
                       action='store_true', default=False,
                       help="print all handlers that match the query")
     parser.add_option('-H', '--handler', dest="handlers",
                       action='append',
                       help="name of the handler to use, in order")
+    parser.add_option('-v', '--verbose', dest='verbose',
+                      action='store_true', default=True,
+                      help="be verbose with output")
     parser.add_option('--print-handlers', dest="print_handlers",
                       action='store_true',
                       help="print all handlers in order they would be tried")
     options, args = parser.parse_args(args)
 
     # sanity check
     assert not (options.url and options.all)
     if not options.handlers:
         options.handlers = None
 
-    # config
+    # read config, if available
     config = ConfigParser()
     if not options.config:
         options.config = os.path.join(os.environ.get('HOME', ''), '.smartopen.ini')
     if os.path.exists(options.config):
         config.read(options.config)
+
+        # select handlers
         if not options.handlers and config.has_option('DEFAULTS', 'handlers'):
-            options.handlers = [ i.strip() for i in config.get('DEFAULTS', 'handlers').split(',') ]
+            options.handlers = [i.strip() for i in config.get('DEFAULTS', 'handlers').split(',')]
+
+        # select browser
+        if not options.browser:
+            for key in 'BROWSER', 'browser':
+                if config.has_option('DEFAULTS', key):
+                    options.browser = config.get('DEFAULTS', key)
+                    break
+
+    # the remaining config is arguments to the handlers
     _config = {}
     for section in config.sections():
         _config[section] = dict(config.items(section))
 
     # get the handlers
-    _locations = locations(options.handlers, _config)
+    _locations = locations(options.handlers, _config, verbose=options.verbose)
 
-    # print the handlers
     if options.print_handlers:
+        # print the handlers
         for name, loc in _locations:
             print name
         sys.exit(0)
 
     # get data to be operated on
     if args:
         data = ' '.join(args)
     else:
+        # read from stdin
         data = sys.stdin.read()
 
-    # print the URLs
     if options.all:
+        # print the URLs
         _urls = urls(data, _locations)
         for name, _url in _urls:
             print '%s: %s' % (name, _url)
         sys.exit(0)
 
+    # select the url
     _url = url(data, _locations)
 
-    # print a URL
     if options.url:
-        print _url
+        # print a URL
+        print _url or 'No handler found for your query'
         sys.exit(0)
 
     # open the URL in a browser
-    os.system("firefox '%s'" % _url)
-    sys.exit(0)
-
+    if _url:
+        open_in_browser(_url)
+    else:
+        parser.error("No handler found")
 
 if __name__ == '__main__':
     main()