Source

tll / tll

Full commit
#!/usr/bin/env python
'''Simple launcher'''

__author__ = "Miki Tebeka <miki.tebeka@gmail.com>"
__version__ = "0.3.3"

import Tkinter as tk
from tkFont import Font
from tkMessageBox import showerror
from threading import Timer

from os.path import exists, expanduser, join, isfile
from os import environ, popen, stat, system
from stat import S_IEXEC
from string import strip
from time import time

def install_gnome():
    def config(dirname):
        return "%s/.gconf/apps/metacity/%s/%%gconf.xml" % dirname

    with open(config("keybinding_commands"), "w") as fo:
        xml = '''<?xml version="1.0"?>
                    <gconf>
                    <entry name="command_1" mtime="%d" type="string">
                            <stringvalue>%s</stringvalue>
                    </entry>
                    </gconf>''' % (time(), __file__)
        fo.write(xml)


    with open(config("global_keybindings"), "w") as fo:
        xml = '''<?xml version="1.0"?>
            <gconf>
                <entry name="run_command_1" mtime="%d" type="string">
                    <stringvalue>&lt;Control&gt;&lt;Shift&gt;k</stringvalue>
                </entry>
            </gconf>''' % time()
        fo.write(xml)

def unique(items):
    seen = set()
    for item in items:
        if item in seen:
            continue
        seen.add(item)
        yield item

def config_file(base_file, env_key):
    filename = ""
    if env_key in environ:
        return environ[env_key]
    elif "HOME" in environ:
        return join(environ["HOME"], base_file)

    return ""

class History:
    histnames = (".tll_history", "TLL_HISTORY")

    def __init__(self, history=None):
        self.history = list(history) if history else []
        self.index = 0

    def with_config_file(self, func, mode):
        histfile = config_file(*self.histnames)
        if (not histfile) or ((mode[0] == "r") and (not isfile(histfile))):
            return

        try:
            fo = open(histfile, mode)
            return func(fo)
        finally:
            fo.close()

    def load(self):
        def _load(fo):
            history = filter(None, map(strip, fo))
            history = list(unique(history))

            self.history = history

        self.with_config_file(_load, "r")

    def save(self):
        def _save(fo):
            for item in unique(self.history):
                print >> fo, item
            fo.close()

        self.with_config_file(_save, "wt")

    def update(self, newline):
        old = filter(lambda line: line != newline, self.history)
        self.history = [newline] + old

    def next(self, step):
        self.index = (self.index + step) % len(self.history)
        return self.history[self.index]

def load_aliases():
    rcfile = config_file(".tllrc", "TLLRC")

    if (not rcfile) or (not isfile(rcfile)):
        return {}

    aliases = {}
    for line in open(rcfile):
        line = line.strip()
        if (not line) or (line[0] == "#"):
            continue
        alias, target = map(strip, line.split("=", 1))

        aliases[alias] = target

    return aliases

def is_executable(path):
    if not isfile(path):
        return 0

    return S_IEXEC & stat(path).st_mode

def launch(name, args, aliases):
    fullname = aliases.get(name, name)

    if " " in fullname:
        fullname, xargs = fullname.split(" ", 1)
        args = "%s %s" % (xargs, args)

    fullname = expanduser(fullname)
    if not exists(fullname):
        fullname = popen("which %s 2>/dev/null" % fullname).read().strip()
        if not fullname:
            raise ValueError("can't find %s" % name)

    if is_executable(fullname):
        cmd = "\"%s\" %s&" % (fullname, args)
    else:
        cmd = "start \"%s\"" % fullname

    if system(cmd) != 0:
        raise ValueError

class TLL:
    def __init__(self):
        self.aliases = load_aliases()
        self.history = History()
        self.history.load()
        self.root = root = tk.Tk()
        root.title("TLL")
        root.option_add("*font", "Courier -40 bold")
        root.bind("<Escape>", lambda e: self.quit())
        root.protocol("WM_DELETE_WINDOW", self.quit)
        self.command = cmd = tk.Entry(root, width=20)
        cmd.pack(fill=tk.BOTH)
        cmd.bind("<Return>", self.on_enter)
        cmd.bind("<Up>", lambda e: self.on_move(-1))
        cmd.bind("<Down>", lambda e: self.on_move(1))
        # Close automatically after 30 seconds
        self.timer = Timer(30, self.quit)

    def run(self):
        self.center_on_screen()
        self.command.focus()
        self.timer.start()

        self.root.mainloop()

    def on_enter(self, event):
        line = self.command.get().strip()
        if not line:
            showerror("TLL Error", "Please enter *something*")
            return

        if " " in line:
            command, args = line.split(" ", 1)
        else:
            command, args = line, ""

        try:
            launch(command, args, self.aliases)
            self.history.update(line)
        except ValueError:
            showerror("TLL Error", "Can't run %s" % command)
            return
        self.quit()

    def on_move(self, step):
        line = self.history.next(step)
        self.command.delete(0, tk.END)
        self.command.insert(0, line)

    # http://mail.python.org/pipermail/tutor/2006-December/051337.html
    def center_on_screen(self):
        root = self.root
        width = root.winfo_width()
        height = root.winfo_height()
        if width == 1 and height == 1:
            # If the window has not yet been displayed, its size is
            # reported as 1x1, so use requested size.
            width = root.winfo_reqwidth()
            height = root.winfo_reqheight()

        # Place in centre of screen:
        x = (root.winfo_screenwidth() - width) / 2
        y = (root.winfo_screenheight() - height) / 3
        x = max(x, 0)
        y = max(y, 0)
        root.geometry('+%d+%d' % (x, y))

    def quit(self):
        self.history.save()
        self.timer.cancel()
        self.root.quit()

def set_user_path():
    if "HOME" not in environ:
        return

    new_path = "%s:%s" % (join(environ["HOME"], "bin"), environ["PATH"])
    environ["PATH"] = new_path

def main(argv=None):
    if argv is None:
        import sys
        argv = sys.argv

    from optparse import OptionParser

    parser = OptionParser(usage="usage: %prog", 
            version="tll %s" % __version__)
    parser.add_option("--install", help="install key bindings", 
                  dest="install", default="")

    opts, args = parser.parse_args(argv[1:])

    if opts.install:
        if opts.install != "gnome":
            raise SystemExit("error: only gnome install supported")
        install_gnome()
        raise SystemExit


    if len(args) != 0:
        parser.error("wrong number of arguments") # Will exit

    set_user_path()
    ui = TLL()
    ui.run()

if __name__ == "__main__":
    main()