#!/usr/bin/env python
# services.py - select programs to process clipboard content
#
# Requires: PyGTK 2 framework, browser shell script
# Author: Tuukka Hastrup <Tuukka@iki.fi>
# License: GNU General Public License
# URL: http://iki.fi/Tuukka/software/services
#
# You should set your desktop (window-manager) to start this menu program
# when you press a key combination like Window-Mouse2, Window-v, Menu, Mouse4.
# The showup is faster if you use the option --show, which leaves the
# program running in the background, waiting for new requests.
# To make first showup fast as well, run in your startup scripts with
# option --server.

# ChangeLog:
# 2006-04-15: window positioning doesn't need hacks anymore
# 2005-06-06: added support for .desktop search providers
# 2004-08-05: reorganized menu a bit
# 2004-08-05: connect selection signal first, then request
# 2004-08-05: implemented Google Lucky, added phrase, Wikipedia
# 2004-08-04: show which program couldn't be launched
# 2004-07-05: mnemonics and accelerators
# 2003-09-26: fixed bug in reload, added ops
# 2003-09-23: check before unlink
# 2003-09-22: Submenu "This menu", check window frame extents
# 2003-09-20: autostart server, client doesn't import gtk, spawn with NOWAIT
# 2003-09-19: server mode listening on a local socket
# 2003-09-18: menu doesn't hide anymore, window is opened below pointer
# 2003-09-16: initial working version

# TODO:
# * separate ops into config file
# * errors from started programs go to /dev/null instead of .xsession-errors
# * we don't know if an op fails and should be reconfigured
# * started programs inherit our open file descriptors
# (* open menu window under the pointer)
# (* fix menu widget behaviour)
# (* error dialogs)
# * keyboard bindings
# (* faster startup time)
# * starting mozilla fails to load java?

import sys, os
import socket
from copy import copy
from traceback import print_exc

import dircache

def mainimports():
    """Imports that are not needed in client mode."""
    global time, urllib, gtk, gdk

    import time, urllib
    import pygtk            # you'll need Debian package python-gtk2
    pygtk.require('2.0')    # you'll need Debian package python-gtk2
    import gtk
    from gtk import gdk

if __name__ != '__main__': # main imports these later
    mainimports()

socketfile = ".services-menu.socket"


window = None
menu = None
server_mode = 0


def smartbookmark(str):
    """Makes an op from a smart bookmark
    (a URL that contains %s for argument)."""
    return lambda x:browser(url(str%query(x)))

def url(str):
    return str.strip() # XXX use urllib.quote?

def query(str):
    return urllib.quote_plus(str)

def query_word(str):
    return query(str.replace(',','').strip())

def browser(url):
    return launch("x-www-browser",url)

def sivistyssanakirja(str):
    if str.strip() == "":
        return browser("http://www.cs.tut.fi/~jkorpela/siv/")
    return browser("http://www.cs.tut.fi/~jkorpela/siv/sanat%s.html#%s" %
                   (query(str.strip()[0]), query_word(str)))

def wikipediabrowser(page):
    return launch("wikitin", page)

# operations that process text from clipboard
ops = {"View URL": lambda x:browser(url(x)),
       "Google": lambda x:browser("http://www.google.com/search?q="
                                  +query(x)),

       "Look up in/Webster's": lambda x:browser(
    "http://www.m-w.com/cgi-bin/dictionary?"+query_word(x)),
       "Look up in/NetMOT": lambda x:browser(
    "http://mot.kielikone.fi/mot/jyu/netmot.exe?UI=file&dic=all&SearchWord="
    +query_word(x)),
       "URL/Add to NP3": lambda x:launch("np3cmd", ["add",url(x)]),
       "URL/Stream in MPlayer": lambda x:launch("gmplayer", url(x)),
       "Execute in shell": lambda x:launch("bash",["-c",x]),
       "Execute in terminal": lambda x:launch("gnome-terminal",["-e",x]),
       "Look up in/Debian bugs": lambda x:browser("http://bugs.debian.org/"
                                               +query_word(x)),
       "Run program": lambda x:launch(x,[]),
       "Look up in/Sivistyssanakirja": sivistyssanakirja,
       "URL/View from Google cache": lambda x:browser(
    "http://www.google.com/search?q=cache:"+query(x)),
       "URL/Translate/German to English": lambda x:browser(
    "http://translate.google.com/translate?hl=en&sl=de&u="+query(x)),
       "URL/Translate/French to English": lambda x:browser(
    "http://translate.google.com/translate?hl=en&sl=fr&u="+query(x)),
       "URL/Translate/Italian to English": lambda x:browser(
    "http://translate.google.com/translate?hl=en&sl=it&u="+query(x)),
       "Look up in/ATK-sanakirja": lambda x:launch("atk-sanakirja",[x.strip()]),
       "Look up in/RFCs": smartbookmark('http://www.ietf.org/rfc/rfc%s.txt'),
       "I'm feeling lucky": smartbookmark("http://www.google.com/search?q=%s&btnI=I'm+Feeling+Lucky"),
       "Phrase Google": smartbookmark("http://www.google.com/search?q=%%22%s%%22"),
       "Look up in/Wikipedia": smartbookmark("http://www.wikipedia.org/wiki/Special:Search?search=%s&go=Go"),
#       "Look up in/Wikipedia": lambda x:wikipediabrowser(x.strip()),
       "Look up in/Hoogle": smartbookmark("http://haskell.org/hoogle/?q=%s"),
}

def searchprovider(s):
    return lambda x:browser(url(s.replace("\\\\{@}", query(x))))


def searchproviders():
    directory = "/usr/share/services/searchproviders/"
    try:
        files = dircache.listdir(directory)
    except OSError:
        files = []
    providers = []
    for f in files:
        try:
            if not f.endswith(".desktop"): continue
            content = file(directory + f).read()
            keys = {}
            for line in content.split("\n"):
                try:
                    key, value = line.split("=", 1)
                    keys[key] = value
                except ValueError:
                    pass

            try:
                providers.append(keys["Name"])
                ops["Search providers/"+keys["Name"]] = searchprovider(keys["Query"])
            except: pass
        except:
            print_exc()

    #    if providers == []:
    return providers
        

# menu structure
urltranslate_ops = ["German to English", "French to English",
                    "Italian to English"]
url_ops = ["Download", "View from Google cache", "Add to NP3",
           "Stream in MPlayer", "Stream in XMMS", "Stream in RealPlayer",
           ("Translate",urltranslate_ops)]
query_ops = ["NetMOT","Wikipedia","Webster's","Pronounce","ATK-sanakirja",
             "Sivistyssanakirja","Debian bugs","RFCs","Everything 2","Hoogle"]
host_ops = ["host -a","ping","mtr"]
this_ops = ["Close","Reload","Add operation","Add smart bookmark URL",
            "Quit server"]
main_ops = ["Remove linebreaks", "View URL", "Google", "Phrase Google",
            "I'm feeling lucky",
            ("URL", url_ops), ("Look up in", query_ops),
            ("Bookmarks                  ", []), # XXX for padding
            ("Host", host_ops),
            ("Search providers", searchproviders()), 
            None,
            "Save to file...","Run program","Execute in shell",
            "Execute in terminal", None,
            ("This menu", this_ops)]


def launch(cmd, args):
    """Executes an external command with given arguments."""
    if type(args) == type(''): args = [args]
    print cmd+": ",args
    try: 
        if server_mode:
            os.spawnvp(os.P_NOWAIT, cmd, [cmd]+args) # XXX Trace errors *somehow*
            return 0
        else:
            # XXX Our caller should report any error
            return os.execvp(cmd, [cmd]+args) # XXX doesn't tell filename if fails
    except OSError:
        (_,value,traceback) = sys.exc_info()
        #value.errorstr = "Command not found"
        if value.filename == None:
            value.filename = cmd
        raise value

def on_optionmenu_activate(entry, widget, command):
    """Called after a service is selected, runs the associated operation."""
    text = entry.get_text()
    window = entry.get_toplevel()
    #print '%s("%s")' % (command,text)
    try:
        ret = ops[command](text) # call the op with text
        if ret != 0:
            raise Exception("The service returned error number "+str(ret))
        else:
            # We're done
            if server_mode:
                window.hide()
            else:
                gtk.main_quit()
    except SystemExit: raise 
    except:
        (type,value,traceback) = sys.exc_info()
        print_exc()
        msg = str(value) # or value.__doc__ or str(type).split('.')[-1]
        dialog = gtk.MessageDialog(window, gtk.DIALOG_DESTROY_WITH_PARENT,
                                   gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, msg)
        dialog.run() # XXX maybe modeless would be better?
        dialog.destroy()

def build_menu(options, handler, accelg, root=""):
    """Constructs a menu tree from given options. Handler is the action
    callback, accelerators are added to accelg, and root gives current
    submenu path in recursive calls. XXX each time, new accels are added?"""
    menu = gtk.Menu()                
    for opt in options:              
        if type(opt) == type((1,2)): # if submenu
            (name, suboptions) = opt
            menu_item = gtk.MenuItem("_"+name)
            menu_item.set_submenu(build_menu(suboptions, handler, accelg,
                                             root+name+"/"))
        else: # if simple menu item
            if opt == None: # None denotes a separator line
                menu_item = gtk.MenuItem()
            else:
                #menu_item= gtk.menu_item_new_with_mnemonic(opt) XXX what's this?
                menu_item = gtk.MenuItem("_"+opt) # XXX find unique
                menu_item.add_accelerator('activate', accelg,
                                          gdk.keyval_from_name(opt[0]),
                                          gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
                menu_item.connect("activate",
                                  handler, root+opt)
            if opt == None or not ops.has_key(root+opt):
                menu_item.set_sensitive(False)
        menu_item.show()
        menu.append(menu_item)
    if options == []:
        menu_item = gtk.MenuItem("(None)")
        menu_item.set_sensitive(False)
        menu_item.show()
        menu.append(menu_item)

    return menu

def got_selection(entry, widget, selection, data):
    """Called after selection data is requested and received, sets text."""
    text = selection.get_text()
    if text == None: text = ''
    if not '\n' in text:
        entry.set_text(text)
    else:
        # XXX switch to text area
        entry.set_text(text)

    entry.select_region(0, len(text))

#def insert_text_handler(entry, text, length, position, data):
#    if '\n' in text:

def menu_show(menu, vbox):
    """Called when main menu closes, opens it again."""
    menu.reparent(vbox)
    menu.show()

def insert_text(entry, text):
    """Called when textbox is activated, inserts a literal newline."""
    pos = entry.get_position()
    entry.insert_text(text, position=entry.get_position())
    entry.set_position(pos+len(text))

def server_window_delete_event(window, event):
    window.hide()
    return True

def init_gui():
    """Creates a window to pop up and a menu attached to it."""
    window = gtk.Window() # XXX something more dialog-like?
    window.set_position(gtk.WIN_POS_MOUSE)
    if server_mode:
        window.connect('delete-event', server_window_delete_event)
    else:
        window.connect('hide', lambda x:window.destroy())
    window.connect('destroy', lambda x:gtk.main_quit())
    window.set_title('Services')

    # add vertical box
    vbox = gtk.VBox()

    # add empty container
    box = gtk.EventBox()
    vbox.pack_start(box, expand=False)
    
    # add text entry
    entry = gtk.Entry()
    #entry.connect('insert_text',lambda v,w,x,y,z:insert_text_handler(box,v,w,x,y,z))
    entry.connect('activate', lambda x:insert_text(entry, '\n'))
    box.add(entry)

    # request X selection to entry content
    window.connect('selection_received', lambda x,y,z:got_selection
                   (entry, x,y,z))

    accel_group = gtk.AccelGroup()
    window.add_accel_group(accel_group)

    # add menu
    menu = build_menu(main_ops, lambda x,y:on_optionmenu_activate(entry,x,y), accel_group)
    # XXX the following stops menu from hiding, but flickers
    menu.connect('hide', lambda x:menu_show(menu,vbox))
    menu_show(menu, vbox)
    #menu.show()
    #menu.attach_to_widget(vbox, None)

    vbox.show_all()
    window.add(vbox)

    menu.set_accel_path("<main>/")

    if 0:
        # add button for closing, dialog style
        close = gtk.Button('Close')
        close.connect('clicked', lambda x: window.hide())
        vbox.pack_start(close, expand=False)

    return window, menu

def window_show(window, menu):
    """Displays the popup window under the mouse pointer."""
    window.hide() # ensure pop up in new position
    window.selection_convert(gdk.atom_intern("PRIMARY"),
                             gdk.atom_intern("STRING"),0L)
    window.show()

def handle_new_connection(window, menu, socket, condition):
    s, addr = socket.accept()
    msg = s.makefile().read() # XXX can block on unexpected long input
    print msg
    if msg == 'SHOW\n':
        window_show(window, menu)
    s.close()
    return True

def restart_server(argv, ss):
    ss.close()
    os.execvp(argv[0], argv+["--show"])

def main():
    # print sys.argv
    global server_mode #, window, menu
    address = os.environ['HOME'] + os.sep + socketfile
    argv = sys.argv
    ss = None

    # parse command line
    option_server = 0
    option_show = 0
    show_failed = 0
    for arg in argv[1:]:
        if arg in ['server','--server']:
            option_server = 1
        elif arg in ['show','--show']:
            option_show = 1
        else:
            print "Unknown option: %s" % arg

    if option_server or option_show:
        # socket code, first try to connect
        try:
            s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
            s.connect(address)

            if(option_show):
                s.send("SHOW\n")
            else:
                s.send("TRIAL\n")
        except:
            connect_failed = 1
            # print_exc()
            if option_show:
                print "Couldn't connect to a running process."
                show_failed = 1
        else:
            if option_show:
                sys.exit(0) # done
            else:
                print "Server was already listening."
                sys.exit(5)
        # more socket code, now try to start listening
        try:
            ss = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
            try:
                os.unlink(address)
            except:
                pass
            ss.bind(address)
            
            ss.listen(5)
            server_mode = 1
        except:
            if option_server: # the error was fatal
                print_exc()
                sys.exit(10)
            else:
                print "Couldn't start listening for connects."

    mainimports()
    ops['This menu/Close'] = lambda x:0 # XXX works because no-op closes window
    if server_mode:
        ops['This menu/Quit server'] = lambda x:sys.exit(0)
        ops['This menu/Reload'] = lambda x:restart_server(argv, ss)
    else:
        ops['This menu/Reload'] = lambda x:os.execvp(argv[0], argv)

    window, menu = init_gui()


    if server_mode:
        gtk.input_add(ss, gtk.gdk.INPUT_READ,
                      lambda x,y:handle_new_connection(window, menu, x,y))

    if not server_mode or option_show:
        window_show(window, menu)

    if server_mode and not option_server: # we shouldn't hang the caller
        if os.fork() != 0:
            sys.exit(0) # return the original to the caller
        else: # detach from terminal
            fd = os.open('/dev/null',os.O_RDWR)
            os.dup2(fd, 0)
            os.dup2(fd, 1)
            os.dup2(fd, 2)
            os.close(fd)
                
    gtk.main()


if __name__ == '__main__':
    # have to fix path here because Gnome 2 breaks it; PATH=$HOME/bin:$PATH
    os.environ['PATH'] = (os.environ['HOME']+os.sep+"bin"+os.pathsep
                          +os.environ['PATH'])

    main()
