Merge branch 'gui'.
authorOlof-Joachim Frahm <olof@macrolet.net>
Thu, 22 Jan 2015 22:06:21 +0000 (22:06 +0000)
committerOlof-Joachim Frahm <olof@macrolet.net>
Thu, 22 Jan 2015 22:06:21 +0000 (22:06 +0000)
Seems good enough for now.

.ropeproject/config.py [new file with mode: 0644]
Makefile
README.md
crypto-install [new file with mode: 0755]
crypto-install.py [deleted file]
git_hooks/pre-commit/10-tests [new file with mode: 0755]
git_hooks/pre-commit/20-flake8 [new file with mode: 0755]
locale/crypto-install.pot [new file with mode: 0644]
locale/de/LC_MESSAGES/crypto-install.po [new file with mode: 0644]
setup.cfg [new file with mode: 0644]
setup.py [new file with mode: 0755]

diff --git a/.ropeproject/config.py b/.ropeproject/config.py
new file mode 100644 (file)
index 0000000..9a6abff
--- /dev/null
@@ -0,0 +1,95 @@
+# The default ``config.py``
+
+
+def set_prefs(prefs):
+    """This function is called before opening the project"""
+
+    # Specify which files and folders to ignore in the project.
+    # Changes to ignored resources are not added to the history and
+    # VCSs.  Also they are not returned in `Project.get_files()`.
+    # Note that ``?`` and ``*`` match all characters but slashes.
+    # '*.pyc': matches 'test.pyc' and 'pkg/test.pyc'
+    # 'mod*.pyc': matches 'test/mod1.pyc' but not 'mod/1.pyc'
+    # '.svn': matches 'pkg/.svn' and all of its children
+    # 'build/*.o': matches 'build/lib.o' but not 'build/sub/lib.o'
+    # 'build//*.o': matches 'build/lib.o' and 'build/sub/lib.o'
+    prefs['ignored_resources'] = ['*.pyc', '*~', '.ropeproject',
+                                  '.hg', '.svn', '_svn', '.git', '.tox']
+
+    # Specifies which files should be considered python files.  It is
+    # useful when you have scripts inside your project.  Only files
+    # ending with ``.py`` are considered to be python files by
+    # default.
+    # prefs['python_files'] = ['*.py']
+
+    # Custom source folders:  By default rope searches the project
+    # for finding source folders (folders that should be searched
+    # for finding modules).  You can add paths to that list.  Note
+    # that rope guesses project source folders correctly most of the
+    # time; use this if you have any problems.
+    # The folders should be relative to project root and use '/' for
+    # separating folders regardless of the platform rope is running on.
+    # 'src/my_source_folder' for instance.
+    # prefs.add('source_folders', 'src')
+
+    # You can extend python path for looking up modules
+    # prefs.add('python_path', '~/python/')
+
+    # Should rope save object information or not.
+    prefs['save_objectdb'] = True
+    prefs['compress_objectdb'] = False
+
+    # If `True`, rope analyzes each module when it is being saved.
+    prefs['automatic_soa'] = True
+    # The depth of calls to follow in static object analysis
+    prefs['soa_followed_calls'] = 0
+
+    # If `False` when running modules or unit tests "dynamic object
+    # analysis" is turned off.  This makes them much faster.
+    prefs['perform_doa'] = True
+
+    # Rope can check the validity of its object DB when running.
+    prefs['validate_objectdb'] = True
+
+    # How many undos to hold?
+    prefs['max_history_items'] = 32
+
+    # Shows whether to save history across sessions.
+    prefs['save_history'] = True
+    prefs['compress_history'] = False
+
+    # Set the number spaces used for indenting.  According to
+    # :PEP:`8`, it is best to use 4 spaces.  Since most of rope's
+    # unit-tests use 4 spaces it is more reliable, too.
+    prefs['indent_size'] = 4
+
+    # Builtin and c-extension modules that are allowed to be imported
+    # and inspected by rope.
+    prefs['extension_modules'] = []
+
+    # Add all standard c-extensions to extension_modules list.
+    prefs['import_dynload_stdmods'] = True
+
+    # If `True` modules with syntax errors are considered to be empty.
+    # The default value is `False`; When `False` syntax errors raise
+    # `rope.base.exceptions.ModuleSyntaxError` exception.
+    prefs['ignore_syntax_errors'] = False
+
+    # If `True`, rope ignores unresolvable imports.  Otherwise, they
+    # appear in the importing namespace.
+    prefs['ignore_bad_imports'] = False
+
+    # If `True`, rope will transform a comma list of imports into
+    # multiple separate import statements when organizing
+    # imports.
+    prefs['split_imports'] = False
+
+    # If `True`, rope will sort imports alphabetically by module name
+    # instead of alphabetically by import statement, with from imports
+    # after normal imports.
+    prefs['sort_imports_alphabetically'] = False
+
+
+def project_opened(project):
+    """This function is called after opening the project"""
+    # Do whatever you like here!
index 676a09d..9d16253 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,9 +1,33 @@
-all: build
-       @sed \
+all: check test build/crypto-install
+
+run: build/crypto-install
+       build/crypto-install
+
+check:
+       flake8 .
+
+test:
+       ./setup.py test
+
+build:
+       mkdir build
+
+build/crypto-install: crypto-install build locale/*/LC_MESSAGES/crypto-install.mo
+       sed \
                -e "s/GIT-TAG/`git describe --abbrev=0 --tags`/g" \
                -e "s/GIT-COMMIT/`git rev-parse --short=7 HEAD`/g" \
                -e "s/GIT-BRANCH/`git rev-parse --abbrev-ref HEAD`/g" \
-               crypto-install.py > build/crypto_install.py
+               crypto-install > build/crypto-install
+       chmod a+rx build/crypto-install
 
-build:
-       mkdir build
+%.mo: %.po
+       msgfmt -o $@ $<
+
+locale/crypto-install.pot: crypto-install
+       xgettext -L Python -o $@ $<
+
+%.po: locale/crypto-install.pot
+       msgmerge -U $@ $<
+
+clean:
+       rm -rf build
index eef6c8a..d9086d1 100644 (file)
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-crypto-install.py
+crypto-install
 
 # USAGE
 
@@ -7,6 +7,24 @@ Run the script to install a baseline setup for both GnuPG and OpenSSH.
 Existing files are detected and not touched in the process, so running
 it is always safe to do.
 
+# OPTIONS
+
+- `--no-gui` disables the GUI, which means text mode will be enabled for
+  everything including the passphrase input
+- `--no-gpg` disables the GnuPG key generation and related setup
+  routines
+- `--no-ssh` does the same for the OpenSSH setup
+- `--gpg-home` sets the directory for the GnuPG files (defaults to the
+  value of `GNUPGHOME` or `~/.gnupg`)
+- `--ssh-home` does the same for OpenSSH files (defaults to `~/.ssh`)
+
+There is also `-h/--help` and `-v/--version` as expected.
+
+# ENVIRONMENT
+
+- `FULLNAME`, `EMAIL`, `USER` are used to pre-fill the corresponding
+  fields
+
 # INSTALLATION
 
 Until I set up a better routine:
@@ -16,8 +34,49 @@ Until I set up a better routine:
     git clone git@github.com:Ferada/crypto-install.git
 
     cd crypto-install
-    make
-    cp build/crypto-install.py ~/bin # or wherever
+    python setup.py install
+
+Using `--prefix` with `install` the path may be changed to just locally
+install it for e.g. the current user.
+
+# DEVELOPMENT
+
+There is a `Makefile` available to run common commands, e.g.:
+
+    make # checks PEP8, runs tests, builds final file
+    make run # run the built program
+    make clean # remove build folder
+
+If you have [`git-hooks`](https://github.com/icefox/git-hooks)
+installed, then the two hooks in `git_hooks` will run the tests and
+check for PEP8 compatibility before committing as well.  Run
+`git hooks --install` in the checked out folder to register the hooks
+initially.
+
+# LOCALISATION
+
+Currently working simultaneously on the English and German version.
+Patches welcome.
+
+To run with a different language set, use:
+
+    TEXTDOMAINDIR=locale LANGUAGE=de_DE ./crypto-install
+
+(I would really like to if that environment variable is okay to use
+here!)
+
+**TODO**: If the application is installed, you should only have to set
+the `LANGUAGE` environment variable instead, as the default locale
+directory will be set during the installation (to
+`prefix/share/locale` probably).
+
+To start off with a new translation, use:
+
+    cd po
+    msginit -l en_US # or whatever language code
+
+You'll have to confirm, or edit the email address and author.
+Afterwards, edit the new `.po` file as usual.
 
-Simply copy the built file into your path and possibly ensure execution
-permissions.
+Please read the `gettext` documentation (`info gettext`) for more
+details.
diff --git a/crypto-install b/crypto-install
new file mode 100755 (executable)
index 0000000..f0a881c
--- /dev/null
@@ -0,0 +1,743 @@
+#!/usr/bin/env python
+# -*- mode: python; coding: utf-8; -*-
+
+
+import argparse, errno, gettext, itertools, locale, os, re, readline, \
+    subprocess, sys, tempfile, textwrap, threading
+
+
+if sys.version_info[0] == 2:
+    from Tkinter import *
+    from tkMessageBox import *
+    from Tix import *
+    from ScrolledText import *
+    from ttk import *
+    from Queue import *
+
+
+    def input_string (prompt=""):
+        return raw_input (prompt)
+
+
+    def gettext_install (*args, **kwargs):
+        gettext.install (*args, unicode = True, **kwargs)
+elif sys.version_info[0] > 2:
+    from tkinter import *
+    from tkinter.messagebox import *
+    from tkinter.tix import *
+    from tkinter.scrolledtext import *
+    from tkinter.ttk import *
+    from queue import *
+
+
+    def input_string (prompt=""):
+        return input (prompt)
+
+
+    gettext_install = gettext.install
+else:
+    raise Exception ("Unsupported Python version {}".format (sys.version_info))
+
+
+def dedented (text):
+    return textwrap.dedent (text).strip ()
+
+
+def ldedented (text):
+    return textwrap.dedent (text).lstrip ()
+
+
+def filled (text):
+    return textwrap.fill (dedented (text), width = 72)
+
+
+def lfilled (text):
+    return textwrap.fill (ldedented (text), width = 72)
+
+
+def read_input_string (prompt = "", default = ""):
+    if default != "":
+        readline.set_startup_hook (lambda: readline.insert_text (default))
+
+    try:
+        return input_string(prompt)
+    finally:
+        readline.set_startup_hook()
+
+
+def parse_arguments ():
+    parser = argparse.ArgumentParser ()
+    parser.add_argument (
+        "-v", "--version",
+        dest = "version",
+        action = "version",
+        version = "crypto-install version GIT-TAG (GIT-COMMIT/GIT-BRANCH)",
+        help = _ ("Display version."))
+    parser.add_argument (
+        "--no-gui",
+        dest = "gui",
+        action = "store_false",
+        help = _ ("Disable GUI, use text interface."))
+    gnupg_group = parser.add_argument_group (
+        _ ("GnuPG"),
+        _ ("Options related to the GnuPG setup."))
+    gnupg_group.add_argument (
+        "--no-gpg",
+        dest = "gnupg",
+        action = "store_false",
+        help = _ ("Disable GnuPG setup."))
+    gnupg_group.add_argument (
+        "--gpg-home",
+        dest = "gnupg_home",
+        default = os.getenv("GNUPGHOME") or "~/.gnupg",
+        metavar = _ ("PATH"),
+        help = _ ("Default directory for GnuPG files."))
+    openssh_group = parser.add_argument_group (
+        _ ("OpenSSH"),
+        _ ("Options related to the OpenSSH setup."))
+    openssh_group.add_argument (
+        "--no-ssh",
+        dest = "openssh",
+        action = "store_false",
+        help = _ ("Disable OpenSSH setup."))
+    openssh_group.add_argument (
+        "--ssh-home",
+        dest = "openssh_home",
+        default = "~/.ssh",
+        metavar = _ ("PATH"),
+        help = _ ("Default directory for OpenSSH files."))
+    return parser.parse_args ()
+
+
+def ensure_directories (path, mode = 0o777):
+    try:
+        os.makedirs (path, mode)
+    except OSError as exception:
+        if exception.errno != errno.EEXIST:
+            raise
+
+
+def default_name ():
+    return os.getenv ("FULLNAME")
+
+
+def default_email ():
+    return os.getenv ("EMAIL")
+
+
+def default_comment ():
+    return ""
+
+
+def default_hostname ():
+    return subprocess.check_output ("hostname").strip ()
+
+
+def default_username ():
+    return os.getenv ("USER")
+
+
+def valid_email (value):
+    return re.match (".+@.+", value)
+
+
+def valid_name (value):
+    return value.strip () != ""
+
+
+def valid_user (value):
+    return value.strip () != ""
+
+
+def valid_host (value):
+    return value.strip () != ""
+
+
+def valid_comment (value):
+    return True
+
+
+def gnupg_exists (arguments):
+    gnupg_home = os.path.expanduser (arguments.gnupg_home)
+    gnupg_secring = os.path.join (gnupg_home, "secring.gpg")
+
+    return os.path.exists (gnupg_secring)
+
+
+def openssh_exists (arguments):
+    openssh_home = os.path.expanduser (arguments.openssh_home)
+    openssh_config = os.path.join (openssh_home, "config")
+    openssh_key = os.path.join (openssh_home, "id_rsa")
+
+    return os.path.exists (openssh_config) and os.path.exists (openssh_key)
+
+
+def quoted (string):
+    return string.replace ("+", "++").replace (" ", "+")
+
+
+def input_passphrase (arguments):
+    batch_passphrase = ldedented ("""
+    RESET
+    OPTION ttyname={}
+    OPTION ttytype={}
+    """).format (subprocess.check_output ("tty").strip (),
+                 os.getenv ("TERM"))
+
+    expected_oks = 3
+
+    batch_env = dict (os.environ)
+    if arguments.gui:
+        batch_passphrase += ldedented ("""
+        OPTION xauthority={}
+        OPTION display={}
+        """).format (os.getenv ("XAUTHORITY"),
+                     os.getenv ("DISPLAY"))
+        expected_oks += 2
+    else:
+        del batch_env["DISPLAY"]
+
+    passphrase_process = subprocess.Popen (["gpg-agent", "--server"],
+                                           stdin = subprocess.PIPE,
+                                           stdout = subprocess.PIPE,
+                                           stderr = subprocess.PIPE,
+                                           env = batch_env)
+
+    try:
+        line = passphrase_process.stdout.readline ().decode ("UTF-8")
+        if line != "OK Pleased to meet you\n":
+            raise Exception ("Couldn't read expected OK.")
+
+        passphrase_process.stdin.write (batch_passphrase.encode ("UTF-8"))
+
+        for i in range (expected_oks):
+            line = passphrase_process.stdout.readline ().decode ("UTF-8")
+            if line != "OK\n":
+                raise Exception ("Couldn't read expected OK.")
+
+        error, prompt, description = "", _ ("Passphrase:"), ""
+
+        while True:
+            batch_passphrase = \
+                "GET_PASSPHRASE --data --repeat=1 --qualitybar X {} {} {}\n" \
+                    .format ((error and quoted (error)) or "X",
+                             (prompt and quoted (prompt)) or "X",
+                             (description and quoted (description)) or "X")
+
+            passphrase_process.stdin.write (batch_passphrase.encode ("UTF-8"))
+
+            line = passphrase_process.stdout.readline ().decode ("UTF-8")
+
+            if line == "OK\n":
+                error = _ ("Empty passphrase")
+                continue
+
+            if line.startswith ("D "):
+                passphrase = line[2:-1]
+
+                if len (passphrase) < 8:
+                    error = _ ("Passphrase too short")
+                    description = _ ("Passphrase has to have at least 8 characters.")
+                    continue
+
+                return passphrase
+
+            if line.startswith ("ERR 83886179"):
+                raise Exception ("Operation cancelled.")
+
+            raise Exception ("Unexpected response.")
+    finally:
+        passphrase_process.stdin.close ()
+        passphrase_process.stdout.close ()
+        passphrase_process.stderr.close ()
+
+        passphrase_process.wait ()
+
+
+def redirect_to_stdout (process):
+    # TODO: argh.  there has to be a better way
+    process.stdin.close ()
+    while process.poll () is None:
+        sys.stdout.write (process.stdout.readline ())
+
+    while True:
+        line = process.stdout.readline ()
+        if len (line) == 0:
+            break
+        sys.stdout.write (line.decode ("UTF-8"))
+
+
+def gnupg_setup (arguments, name = None, email = None, comment = None):
+    gnupg_home = os.path.expanduser (arguments.gnupg_home)
+    gnupg_secring = os.path.join (gnupg_home, "secring.gpg")
+
+    if gnupg_exists (arguments):
+        print (_ ("GnuPG secret keyring already exists at '{}'.")
+               .format (gnupg_secring))
+        return
+
+    if not arguments.gui:
+        print (filled (_ ("""
+        No default GnuPG key available.  Please enter your information to
+        create a new key.""")))
+
+        name = read_input_string (_ ("What is your name (e.g. 'John Doe')? "),
+                                  default_name ())
+
+        email = read_input_string (dedented (_ ("""
+        What is your email address (e.g. 'test@example.com')? """)),
+                                   default_email ())
+
+        comment = read_input_string (dedented (_ ("""
+        What is your comment phrase, if any (e.g. 'key for 2014')? """)),
+                                     default_comment ())
+
+    if not os.path.exists (gnupg_home):
+        print (_ ("Creating GnuPG directory at '{}'.").format (gnupg_home))
+        ensure_directories (gnupg_home, 0o700)
+
+    with tempfile.NamedTemporaryFile () as tmp:
+        batch_key = ldedented ("""
+        %ask-passphrase
+        Key-Type: DSA
+        Key-Length: 2048
+        Subkey-Type: ELG-E
+        Subkey-Length: 2048
+        Name-Real: {}
+        Name-Email: {}
+        Expire-Date: 0
+        """).format (name, email)
+
+        if comment != "":
+            batch_key += "Name-Comment: {}\n".format (comment)
+
+        tmp.write (batch_key.encode ("UTF-8"))
+        tmp.flush ()
+
+        batch_env = dict (os.environ)
+        if not arguments.gui:
+            del batch_env["DISPLAY"]
+
+        gnupg_process = subprocess.Popen (["gpg2",
+                                           "--homedir", gnupg_home,
+                                           "--batch", "--gen-key", tmp.name],
+                                          stdin = subprocess.PIPE,
+                                          stdout = subprocess.PIPE,
+                                          stderr = subprocess.STDOUT,
+                                          env = batch_env)
+
+        redirect_to_stdout (gnupg_process)
+
+        if gnupg_process.returncode != 0:
+            raise Exception ("Couldn't create GnuPG key.")
+
+
+def openssh_setup (arguments, comment = None):
+    openssh_home = os.path.expanduser (arguments.openssh_home)
+    openssh_config = os.path.join (openssh_home, "config")
+
+    if not os.path.exists (openssh_config):
+        print (_ ("Creating OpenSSH directory at '{}'.").format (openssh_home))
+        ensure_directories (openssh_home, 0o700)
+
+        print (_ ("Creating OpenSSH configuration at '{}'.")
+               .format (openssh_config))
+        with open (openssh_config, "w") as config:
+            config.write (ldedented ("""
+            ForwardAgent yes
+            ForwardX11 yes
+            """))
+
+    openssh_key = os.path.join (openssh_home, "id_rsa")
+
+    if os.path.exists (openssh_key):
+        print (_ ("OpenSSH key already exists at '{}'.").format (openssh_key))
+        return
+
+    openssh_key_dsa = os.path.join (openssh_home, "id_dsa")
+
+    if os.path.exists (openssh_key_dsa):
+        print (_ ("OpenSSH key already exists at '{}'.").format (openssh_key_dsa))
+        return
+
+    print (filled (_ ("No OpenSSH key available.  Generating new key.")))
+
+    if not arguments.gui:
+        comment = "{}@{}".format (default_username (), default_hostname ())
+        comment = read_input_string (ldedented (_ ("""
+        What is your comment phrase (e.g. 'user@mycomputer')? """)), comment)
+
+    passphrase = input_passphrase (arguments)
+
+    batch_env = dict (os.environ)
+    if not arguments.gui:
+        del batch_env["DISPLAY"]
+
+    # TODO: is it somehow possible to pass the password on stdin?
+    openssh_process = subprocess.Popen (["ssh-keygen",
+                                         "-P", passphrase,
+                                         "-C", comment,
+                                         "-f", openssh_key],
+                                        stdin = subprocess.PIPE,
+                                        stdout = subprocess.PIPE,
+                                        stderr = subprocess.STDOUT,
+                                        env = batch_env)
+
+    redirect_to_stdout (openssh_process)
+
+    if openssh_process.returncode != 0:
+        raise Exception ("Couldn't create OpenSSH key.")
+
+
+def _state (value):
+    return NORMAL if value else DISABLED
+
+
+# http://www.blog.pythonlibrary.org/2014/07/14/tkinter-redirecting-stdout-stderr/
+# http://www.virtualroadside.com/blog/index.php/2012/11/10/glib-idle_add-for-tkinter-in-python/
+class RedirectText (object):
+    def __init__ (self, root, widget):
+        self.root = root
+        self.widget = widget
+
+        self.queue = Queue ()
+
+    def write (self, string):
+        self.widget.insert (END, string)
+
+    def enqueue (self, value):
+        self.queue.put (value)
+        self.root.event_generate ("<<Idle>>", when = "tail")
+
+
+class CryptoInstallProgress (Toplevel):
+    def __init__ (self, parent):
+        Toplevel.__init__ (self, parent)
+
+        self.parent = parent
+
+        self.create_widgets ()
+
+    def create_widgets (self):
+        self.balloon = Balloon (self, initwait = 250)
+
+        self.text = ScrolledText (self)
+        self.text.pack (fill = BOTH, expand = True)
+
+        self.redirect = RedirectText (self.parent, self.text)
+
+        self._quit = Button (self,
+                             text = _ ("Quit"),
+                             command = self.maybe_quit)
+        self.balloon.bind_widget (self._quit,
+                                  msg = _ ("Quit the program immediately"))
+        self._quit.pack ()
+
+    def update_widgets (self):
+        if self.parent.state () == "normal":
+            self._quit["text"] = _ ("Close")
+            self.balloon.bind_widget (self._quit,
+                                      msg = _ ("Close this window"))
+
+    def maybe_quit (self):
+        (self.quit if self.parent.state () != "normal" else self.destroy) ()
+
+
+class CryptoInstall (Tk):
+    def __init__ (self, arguments):
+        Tk.__init__ (self)
+
+        self.style = Style ()
+        self.style.theme_use ("clam")
+        self.style.configure ("Invalid.TLabel", foreground = "red")
+        self.style.configure ("Invalid.TEntry", foreground = "red")
+
+        self.arguments = arguments
+
+        self.resizable (width = False, height = False)
+        self.title (_ ("Crypto Install Wizard"))
+
+        self.progress = None
+
+        self.create_widgets ()
+
+    def create_widgets (self):
+        self.fields = {}
+
+        self.balloon = Balloon (self, initwait = 250)
+
+        self.info_frame = Frame (self)
+        self.info_frame.pack (fill = X)
+
+        msg = dedented (_ ("""
+        Username on the local machine (e.g. 'user')
+        """))
+        self.user_label = Label (self.info_frame, text = _ ("Username"))
+        self.balloon.bind_widget (self.user_label, msg = msg)
+        self.user_label.grid ()
+
+        self.user_var = StringVar (self, default_username ())
+        self.user_var.trace ("w", self.update_widgets)
+
+        self.user = Entry (self.info_frame, textvariable = self.user_var)
+        self.balloon.bind_widget (self.user, msg = msg)
+        self.user.grid (row = 0, column = 1)
+
+        self.fields["user"] = [self.user_var, valid_user,
+                               self.user, self.user_label]
+
+        msg = dedented (_ ("""
+        Host name of the local machine (e.g. 'mycomputer')
+        """))
+        self.host_label = Label (self.info_frame, text = _ ("Host Name"))
+        self.balloon.bind_widget (self.host_label, msg = msg)
+        self.host_label.grid ()
+
+        self.host_var = StringVar (self, default_hostname ())
+        self.host_var.trace ("w", self.update_widgets)
+
+        self.host = Entry (self.info_frame, textvariable = self.host_var)
+        self.balloon.bind_widget (self.host, msg = msg)
+        self.host.grid (row = 1, column = 1)
+
+        self.fields["host"] = [self.host_var, valid_host,
+                               self.host, self.host_label]
+
+        msg = dedented (_ ("""
+        Full name as it should appear in the key description (e.g. 'John Doe')
+        """))
+        self.name_label = Label (self.info_frame, text = _ ("Full Name"))
+        self.balloon.bind_widget (self.name_label, msg = msg)
+        self.name_label.grid ()
+
+        self.name_var = StringVar (self, default_name ())
+        self.name_var.trace ("w", self.update_widgets)
+
+        self.name = Entry (self.info_frame, textvariable = self.name_var)
+        self.balloon.bind_widget (self.name, msg = msg)
+        self.name.grid (row = 2, column = 1)
+
+        self.fields["name"] = [self.name_var, valid_name,
+                               self.name, self.name_label]
+
+        msg = dedented (_ ("""
+        Email address associated with the name (e.g. '<test@example.com>')
+        """))
+        self.email_label = Label (self.info_frame, text = _ ("Email address"))
+        self.balloon.bind_widget (self.email_label, msg = msg)
+        self.email_label.grid ()
+
+        self.email_var = StringVar (self, default_email ())
+        self.email_var.trace ("w", self.update_widgets)
+
+        self.email = Entry (self.info_frame, textvariable = self.email_var)
+        self.balloon.bind_widget (self.email, msg = msg)
+        self.email.grid (row = 3, column = 1)
+
+        self.fields["email"] = [self.email_var, valid_email,
+                                self.email, self.email_label]
+
+        msg = dedented (_ ("""
+        Comment phrase for the GnuPG key, if any (e.g. 'key for 2014')
+        """))
+        self.comment_label = Label (self.info_frame, text = _ ("Comment phrase"))
+        self.balloon.bind_widget (self.comment_label, msg = msg)
+        self.comment_label.grid ()
+
+        self.comment_var = StringVar (self, default_comment ())
+        self.comment_var.trace ("w", self.update_widgets)
+
+        self.comment = Entry (self.info_frame, textvariable = self.comment_var)
+        self.balloon.bind_widget (self.comment, msg = msg)
+        self.comment.grid (row = 4, column = 1)
+
+        self.fields["comment"] = [self.comment_var, valid_comment,
+                                  self.comment, self.comment_label]
+
+        self.options_frame = Frame (self)
+        self.options_frame.pack (fill = X)
+
+        self.gnupg_label = Label (self.options_frame,
+                                  text = _ ("Generate GnuPG key"))
+        self.gnupg_label.grid ()
+
+        self.gnupg_var = IntVar (self, 1 if self.arguments.gnupg else 0)
+        self.gnupg_var.trace ("w", self.update_widgets)
+
+        self.gnupg = Checkbutton (self.options_frame,
+                                  variable = self.gnupg_var)
+        self.gnupg.grid (row = 0, column = 1)
+
+        self.openssh_label = Label (self.options_frame,
+                                    text = _ ("Generate OpenSSH key"))
+        self.openssh_label.grid ()
+
+        self.openssh_var = IntVar (self, 1 if self.arguments.openssh else 0)
+        self.openssh_var.trace ("w", self.update_widgets)
+
+        self.openssh = Checkbutton (self.options_frame,
+                                    variable = self.openssh_var)
+        self.openssh.grid (row = 1, column = 1)
+
+        self.button_frame = Frame (self)
+        self.button_frame.pack (fill = X)
+
+        self._generate = Button (self.button_frame, text = _ ("Generate Keys"),
+                                 command = self.generate)
+        self.balloon.bind_widget (
+            self._generate,
+            msg = _ ("Generate the keys as configured above"))
+        self._generate.pack (side = LEFT, fill = Y)
+
+        self._quit = Button (self.button_frame, text = _ ("Quit"),
+                             command = self.quit)
+        self.balloon.bind_widget (self._quit,
+                                  msg = _ ("Quit the program immediately"))
+        self._quit.pack (side = LEFT)
+
+        self.update_widgets ()
+
+    def valid_state (self):
+        if not self.openssh_var.get () and not self.gnupg_var.get ():
+            return False
+
+        if gnupg_exists (self.arguments) and openssh_exists (self.arguments):
+            return False
+
+        if not valid_name (self.user_var.get ()):
+            return False
+
+        if not valid_host (self.host_var.get ()):
+            return False
+
+        if not valid_email (self.email_var.get ()):
+            return False
+
+        if not valid_name (self.name_var.get ()):
+            return False
+
+        if not valid_comment (self.comment_var.get ()):
+            return False
+
+        return True
+
+    def update_field (self, name):
+        field = self.fields[name]
+
+        valid = field[1] (field[0].get ())
+
+        field[2]["style"] = "" if valid else "Invalid.TEntry"
+        field[3]["style"] = "" if valid else "Invalid.TLabel"
+
+    def update_widgets (self, *args):
+        self._generate["state"] = _state (self.valid_state ())
+
+        for field in ["user", "host", "name", "email", "comment"]:
+            self.update_field (field)
+
+        self.gnupg["state"] = _state (not gnupg_exists (self.arguments))
+        self.openssh["state"] = _state (not openssh_exists (self.arguments))
+
+        gnupg_key = self.name_var.get ().strip ()
+        comment = self.comment_var.get ().strip ()
+        if comment != "":
+            gnupg_key + " ({}) ".format (comment)
+        gnupg_key += "<{}>".format (self.email_var.get ().strip ())
+
+        user = self.user_var.get ().strip ()
+        host = self.host_var.get ().strip ()
+
+        openssh_key = "{}@{}".format (user, host)
+
+        msg = dedented (_ ("""
+        Generate a GnuPG key for '{}' and configure a default setup for it
+        """)).format (gnupg_key)
+
+        self.balloon.bind_widget (self.gnupg, msg = msg)
+        self.balloon.bind_widget (self.gnupg_label, msg = msg)
+
+        msg = dedented (_ ("""
+        Generate an OpenSSH key for '{}' and configure a default setup for it
+        """)).format (openssh_key)
+
+        self.balloon.bind_widget (self.openssh, msg = msg)
+        self.balloon.bind_widget (self.openssh_label, msg = msg)
+
+    def generate_thread (self, gnupg, openssh, name, email, comment, user,
+                         host):
+        stdout = sys.stdout
+
+        try:
+            sys.stdout = self.progress.redirect
+
+            if gnupg:
+                gnupg_setup (self.arguments, name, email, comment)
+                # TODO: put update into queue
+                self.update_widgets ()
+
+            if openssh:
+                comment = "{}@{}".format (user, host)
+                openssh_setup (self.arguments, comment)
+                # TODO: put update into queue
+                self.update_widgets ()
+        except Exception as exception:
+            self.deiconify ()
+
+            sys.stdout.write (exception)
+            sys.stdout.write ("\n")
+
+            raise
+        finally:
+            # TODO: put update into queue
+            self.progress.update_widgets ()
+            sys.stdout = stdout
+
+    def _on_idle ():
+        try:
+            while True:
+                message = self.progress.queue.get (block = False)
+                self.progress.redirect.write (message)
+        except Empty:
+            pass
+
+    def generate (self):
+        self.withdraw ()
+
+        if not self.progress or self.progress.winfo_exists () == 0:
+            self.progress = CryptoInstallProgress (self)
+        self.progress.text.delete ("0.0", "end")
+
+        self.bind ("<<Idle>>", self._on_idle)
+
+        thread = threading.Thread (target = self.generate_thread,
+                                   args = (self.gnupg_var.get (),
+                                           self.openssh_var.get (),
+                                           self.name_var.get (),
+                                           self.email_var.get (),
+                                           self.comment_var.get (),
+                                           self.user_var.get (),
+                                           self.host_var.get ()))
+        thread.start ()
+
+
+def main ():
+    locale.setlocale (locale.LC_ALL, "")
+
+    gettext_install ("crypto-install", localedir = os.getenv ("TEXTDOMAINDIR"))
+
+    arguments = parse_arguments ()
+
+    if arguments.gui:
+        # TODO: use gtk instead?  would be more consistent with the pinentry style
+        # (assuming it's using gtk)
+        CryptoInstall (arguments).mainloop ()
+    else:
+        if arguments.gnupg:
+            gnupg_setup (arguments)
+
+        if arguments.openssh:
+            openssh_setup (arguments)
+
+
+if __name__ == "__main__":
+    main ()
diff --git a/crypto-install.py b/crypto-install.py
deleted file mode 100755 (executable)
index 9715441..0000000
+++ /dev/null
@@ -1,173 +0,0 @@
-#!/usr/bin/env python
-# -*- mode: python; coding: utf-8-unix; -*-
-
-
-import argparse, errno, os, readline, subprocess, sys, tempfile, textwrap
-
-
-if sys.version_info[0] == 2:
-    def input_string (prompt=""):
-        return raw_input (prompt)
-elif sys.version_info[0] > 2:
-    def input_string (prompt=""):
-        return input (prompt)
-else:
-    raise Exception ("Unsupported Python version {}".format (sys.version_info))
-
-
-def dedented (text):
-    return textwrap.dedent (text).strip ()
-
-
-def filled (text):
-    return textwrap.fill (dedented (text), width = 72)
-
-
-def read_input_string (prompt="", default=""):
-    if default != "":
-        readline.set_startup_hook (lambda: readline.insert_text (default))
-
-    try:
-        return input_string(prompt)
-    finally:
-        readline.set_startup_hook()
-
-
-def parse_arguments ():
-    parser = argparse.ArgumentParser ()
-    parser.add_argument (
-        "-v", "--version",
-        dest = "version",
-        action = "version",
-        version = "crypto-install.py version GIT-TAG (GIT-COMMIT/GIT-BRANCH)",
-        help = "Display version.")
-    gnupg_group = parser.add_argument_group ("GnuPG",
-        "Options related to the GnuPG setup.")
-    gnupg_group.add_argument (
-        "--no-gpg",
-        dest = "gnupg",
-        action = "store_false",
-        help = "Disable GnuPG setup.")
-    gnupg_group.add_argument (
-        "--gpg-home",
-        dest = "gnupg_home",
-        default = "~/.gnupg",
-        metavar = "PATH",
-        help = "Default directory for GnuPG files.")
-    openssh_group = parser.add_argument_group ("OpenSSH",
-        "Options related to the OpenSSH setup.")
-    openssh_group.add_argument (
-        "--no-ssh",
-        dest = "openssh",
-        action = "store_false",
-        help = "Disable OpenSSH setup.")
-    openssh_group.add_argument (
-        "--ssh-home",
-        dest = "openssh_home",
-        default = "~/.ssh",
-        metavar = "PATH",
-        help = "Default directory for OpenSSH files.")
-    return parser.parse_args ()
-
-
-def gnupg_setup (arguments):
-    gnupg_home = os.path.expanduser (arguments.gnupg_home)
-    gnupg_secring = os.path.join (gnupg_home, "secring.gpg")
-
-    if os.path.exists (gnupg_secring):
-        print ("GnuPG secret keyring already exists at {!r}."
-               .format (gnupg_secring))
-        return
-
-    print (filled ("""
-    No default GnuPG key available.  Please enter your information to
-    create a new key."""))
-
-    default_name = os.getenv ("FULLNAME")
-    name = read_input_string ("What is your name? ", default_name)
-
-    default_email = os.getenv ("EMAIL")
-    email = read_input_string ("What is your email address? ", default_email)
-
-    comment = read_input_string ("What is your comment phrase, if any (e.g. 'key for 2014')? ")
-
-    if not os.path.exists (gnupg_home):
-        ensure_directories (gnupg_home, 0o700)
-
-    with tempfile.NamedTemporaryFile () as tmp:
-        batch_key = dedented ("""
-        %ask-passphrase
-        Key-Type: DSA
-        Key-Length: 2048
-        Subkey-Type: ELG-E
-        Subkey-Length: 2048
-        Name-Real: {}
-        Name-Email: {}
-        Expire-Date: 0
-        """).format (name, email)
-
-        if comment != "":
-            batch_key += "\nName-Comment: {}\n".format (comment)
-
-        tmp.write (batch_key)
-        tmp.flush ()
-
-        batch_env = dict(os.environ)
-        del batch_env["DISPLAY"]
-
-        gnupg_process = subprocess.Popen (["gpg2", "--homedir", gnupg_home, "--batch", "--gen-key", tmp.name],
-                                        env = batch_env)
-        gnupg_process.wait ()
-
-        if gnupg_process.returncode != 0:
-            raise Exception ("Couldn't create GnuPG key.")
-
-
-def ensure_directories (path, mode = 0o777):
-    try:
-        os.makedirs (path, mode)
-    except OSError as exception:
-        if exception.errno != errno.EEXIST:
-            raise
-
-
-def openssh_setup (arguments):
-    openssh_home = os.path.expanduser (arguments.openssh_home)
-    openssh_config = os.path.join (openssh_home, "config")
-
-    if not os.path.exists (openssh_config):
-        ensure_directories (openssh_home, 0o700)
-
-        with open (openssh_config, "w") as config:
-            config.write (dedented ("""
-            ForwardAgent yes
-            ForwardX11 yes
-            """))
-
-    openssh_key = os.path.join (openssh_home, "id_rsa")
-
-    if os.path.exists (openssh_key):
-        print("OpenSSH key already exists at {!r}.".format (openssh_key))
-        return
-
-    print (filled ("No OpenSSH key available.  Generating new key."))
-
-    openssh_process = subprocess.Popen (["ssh-keygen", "-f", openssh_key])
-    openssh_process.wait ()
-
-    if openssh_process.returncode != 0:
-        raise Exception ("Couldn't create OpenSSH key.")
-
-
-def main ():
-    arguments = parse_arguments ()
-
-    if arguments.gnupg:
-        gnupg_setup (arguments)
-
-    if arguments.openssh:
-        openssh_setup (arguments)
-
-
-if __name__ == "__main__":
-    main ()
diff --git a/git_hooks/pre-commit/10-tests b/git_hooks/pre-commit/10-tests
new file mode 100755 (executable)
index 0000000..587ab08
--- /dev/null
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+exec ./setup.py test
diff --git a/git_hooks/pre-commit/20-flake8 b/git_hooks/pre-commit/20-flake8
new file mode 100755 (executable)
index 0000000..50037b4
--- /dev/null
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+exec flake8 .
diff --git a/locale/crypto-install.pot b/locale/crypto-install.pot
new file mode 100644 (file)
index 0000000..43bf3dd
--- /dev/null
@@ -0,0 +1,238 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-21 23:50+0000\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: crypto-install:75
+msgid "Display version."
+msgstr ""
+
+#: crypto-install:80
+msgid "Disable GUI, use text interface."
+msgstr ""
+
+#: crypto-install:82
+msgid "GnuPG"
+msgstr ""
+
+#: crypto-install:83
+msgid "Options related to the GnuPG setup."
+msgstr ""
+
+#: crypto-install:88
+msgid "Disable GnuPG setup."
+msgstr ""
+
+#: crypto-install:93 crypto-install:107
+msgid "PATH"
+msgstr ""
+
+#: crypto-install:94
+msgid "Default directory for GnuPG files."
+msgstr ""
+
+#: crypto-install:96
+msgid "OpenSSH"
+msgstr ""
+
+#: crypto-install:97
+msgid "Options related to the OpenSSH setup."
+msgstr ""
+
+#: crypto-install:102
+msgid "Disable OpenSSH setup."
+msgstr ""
+
+#: crypto-install:108
+msgid "Default directory for OpenSSH files."
+msgstr ""
+
+#: crypto-install:218
+msgid "Passphrase:"
+msgstr ""
+
+#: crypto-install:232
+msgid "Empty passphrase"
+msgstr ""
+
+#: crypto-install:239
+msgid "Passphrase too short"
+msgstr ""
+
+#: crypto-install:240
+msgid "Passphrase has to have at least 8 characters."
+msgstr ""
+
+#: crypto-install:275
+msgid "GnuPG secret keyring already exists at '{}'."
+msgstr ""
+
+#: crypto-install:280
+msgid ""
+"\n"
+"        No default GnuPG key available.  Please enter your information to\n"
+"        create a new key."
+msgstr ""
+
+#: crypto-install:284
+msgid "What is your name (e.g. 'John Doe')? "
+msgstr ""
+
+#: crypto-install:287
+msgid ""
+"\n"
+"        What is your email address (e.g. 'test@example.com')? "
+msgstr ""
+
+#: crypto-install:291
+msgid ""
+"\n"
+"        What is your comment phrase, if any (e.g. 'key for 2014')? "
+msgstr ""
+
+#: crypto-install:296
+msgid "Creating GnuPG directory at '{}'."
+msgstr ""
+
+#: crypto-install:340
+msgid "Creating OpenSSH directory at '{}'."
+msgstr ""
+
+#: crypto-install:343
+msgid "Creating OpenSSH configuration at '{}'."
+msgstr ""
+
+#: crypto-install:354 crypto-install:360
+msgid "OpenSSH key already exists at '{}'."
+msgstr ""
+
+#: crypto-install:363
+msgid "No OpenSSH key available.  Generating new key."
+msgstr ""
+
+#: crypto-install:367
+msgid ""
+"\n"
+"        What is your comment phrase (e.g. 'user@mycomputer')? "
+msgstr ""
+
+#: crypto-install:430 crypto-install:592
+msgid "Quit"
+msgstr ""
+
+#: crypto-install:433 crypto-install:595
+msgid "Quit the program immediately"
+msgstr ""
+
+#: crypto-install:438
+msgid "Close"
+msgstr ""
+
+#: crypto-install:440
+msgid "Close this window"
+msgstr ""
+
+#: crypto-install:458
+msgid "Crypto Install Wizard"
+msgstr ""
+
+#: crypto-install:472
+msgid ""
+"\n"
+"        Username on the local machine (e.g. 'user')\n"
+"        "
+msgstr ""
+
+#: crypto-install:475
+msgid "Username"
+msgstr ""
+
+#: crypto-install:489
+msgid ""
+"\n"
+"        Host name of the local machine (e.g. 'mycomputer')\n"
+"        "
+msgstr ""
+
+#: crypto-install:492
+msgid "Host Name"
+msgstr ""
+
+#: crypto-install:506
+msgid ""
+"\n"
+"        Full name as it should appear in the key description (e.g. 'John "
+"Doe')\n"
+"        "
+msgstr ""
+
+#: crypto-install:509
+msgid "Full Name"
+msgstr ""
+
+#: crypto-install:523
+msgid ""
+"\n"
+"        Email address associated with the name (e.g. '<test@example.com>')\n"
+"        "
+msgstr ""
+
+#: crypto-install:526
+msgid "Email address"
+msgstr ""
+
+#: crypto-install:540
+msgid ""
+"\n"
+"        Comment phrase for the GnuPG key, if any (e.g. 'key for 2014')\n"
+"        "
+msgstr ""
+
+#: crypto-install:543
+msgid "Comment phrase"
+msgstr ""
+
+#: crypto-install:561
+msgid "Generate GnuPG key"
+msgstr ""
+
+#: crypto-install:572
+msgid "Generate OpenSSH key"
+msgstr ""
+
+#: crypto-install:585
+msgid "Generate Keys"
+msgstr ""
+
+#: crypto-install:589
+msgid "Generate the keys as configured above"
+msgstr ""
+
+#: crypto-install:652
+msgid ""
+"\n"
+"        Generate a GnuPG key for '{}' and configure a default setup for it\n"
+"        "
+msgstr ""
+
+#: crypto-install:659
+msgid ""
+"\n"
+"        Generate an OpenSSH key for '{}' and configure a default setup for "
+"it\n"
+"        "
+msgstr ""
diff --git a/locale/de/LC_MESSAGES/crypto-install.po b/locale/de/LC_MESSAGES/crypto-install.po
new file mode 100644 (file)
index 0000000..962c77c
--- /dev/null
@@ -0,0 +1,272 @@
+# German translations for PACKAGE package
+# German messages for PACKAGE.
+# Copyright (C) 2015 THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Olof-Joachim Frahm <olof@macrolet.net>, 2015.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-21 23:50+0000\n"
+"PO-Revision-Date: 2015-01-17 12:29+0000\n"
+"Last-Translator: Olof-Joachim Frahm <olof@macrolet.net>\n"
+"Language-Team: German <translation-team-de@lists.sourceforge.net>\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: crypto-install:75
+msgid "Display version."
+msgstr "Version anzeigen."
+
+#: crypto-install:80
+msgid "Disable GUI, use text interface."
+msgstr "Schalte GUI ab, benutze das Textinterface."
+
+#: crypto-install:82
+msgid "GnuPG"
+msgstr "GnuPG"
+
+#: crypto-install:83
+msgid "Options related to the GnuPG setup."
+msgstr "Optionen für das GnuPG-Setup."
+
+#: crypto-install:88
+msgid "Disable GnuPG setup."
+msgstr "GnuPG-Setup deaktivieren."
+
+#: crypto-install:93 crypto-install:107
+msgid "PATH"
+msgstr "PFAD"
+
+#: crypto-install:94
+msgid "Default directory for GnuPG files."
+msgstr "Standardverzeichnis für GnuPG-Dateien."
+
+#: crypto-install:96
+msgid "OpenSSH"
+msgstr "OpenSSH"
+
+#: crypto-install:97
+msgid "Options related to the OpenSSH setup."
+msgstr "Optionen für das OpenSSH-Setup."
+
+#: crypto-install:102
+msgid "Disable OpenSSH setup."
+msgstr "OpenSSH-Setup deaktivieren."
+
+#: crypto-install:108
+msgid "Default directory for OpenSSH files."
+msgstr "Standardverzeichnis für OpenSSH-Dateien."
+
+#: crypto-install:218
+msgid "Passphrase:"
+msgstr "Passwort:"
+
+#: crypto-install:232
+msgid "Empty passphrase"
+msgstr "Leeres Passwort"
+
+#: crypto-install:239
+msgid "Passphrase too short"
+msgstr "Passwort zu kurz"
+
+#: crypto-install:240
+msgid "Passphrase has to have at least 8 characters."
+msgstr "Passwort muß mindestens 8 Zeichen lang sein."
+
+#: crypto-install:275
+msgid "GnuPG secret keyring already exists at '{}'."
+msgstr "Geheimer GnuPG-Schlüssel existiert bereits unter '{}'."
+
+#: crypto-install:280
+msgid ""
+"\n"
+"        No default GnuPG key available.  Please enter your information to\n"
+"        create a new key."
+msgstr ""
+"\n"
+"        Kein Standard GnuPG-Schlüssel vorhanden.  Bitte gib Deine Informationen um\n"
+"        einen neuen Schlüssel zu erstellen."
+
+#: crypto-install:284
+msgid "What is your name (e.g. 'John Doe')? "
+msgstr "Wie heißt Du (z.B. 'Max Mustermann')? "
+
+#: crypto-install:287
+msgid ""
+"\n"
+"        What is your email address (e.g. 'test@example.com')? "
+msgstr ""
+"\n"
+"        Was ist Deine Email-Adresse? (z.B. 'test@example.com')? "
+
+#: crypto-install:291
+msgid ""
+"\n"
+"        What is your comment phrase, if any (e.g. 'key for 2014')? "
+msgstr ""
+"\n"
+"        Was ist Dein Kommentar, falls gewünscht (z.B. 'Schlüssel für 2014')? "
+
+#: crypto-install:296
+msgid "Creating GnuPG directory at '{}'."
+msgstr "Erstelle GnuPG-Verzeichnis unter '{}'."
+
+#: crypto-install:340
+msgid "Creating OpenSSH directory at '{}'."
+msgstr "Erstelle OpenSSH-Verzeichnis unter '{}'."
+
+#: crypto-install:343
+msgid "Creating OpenSSH configuration at '{}'."
+msgstr "Erstelle OpenSSH-Konfiguration unter '{}'."
+
+#: crypto-install:354 crypto-install:360
+msgid "OpenSSH key already exists at '{}'."
+msgstr "OpenSSH-Schlüssel existiert bereits unter '{}'."
+
+#: crypto-install:363
+msgid "No OpenSSH key available.  Generating new key."
+msgstr "Kein OpenSSH-Schlüssel verfügbar.  Erstelle neuen Schlüssel."
+
+#: crypto-install:367
+msgid ""
+"\n"
+"        What is your comment phrase (e.g. 'user@mycomputer')? "
+msgstr ""
+"\n"
+"        Was ist Dein Kommentar (z.B. 'benutzer@meincomputer')? "
+
+#: crypto-install:430 crypto-install:592
+msgid "Quit"
+msgstr "Beenden"
+
+#: crypto-install:433 crypto-install:595
+msgid "Quit the program immediately"
+msgstr "Programm sofort beenden"
+
+#: crypto-install:438
+msgid "Close"
+msgstr "Schließen"
+
+#: crypto-install:440
+msgid "Close this window"
+msgstr "Schließe dieses Fenster"
+
+#: crypto-install:458
+msgid "Crypto Install Wizard"
+msgstr "Crypto Installationsassistent"
+
+#: crypto-install:472
+msgid ""
+"\n"
+"        Username on the local machine (e.g. 'user')\n"
+"        "
+msgstr ""
+"\n"
+"        Benutzername am lokalen Rechner (z.B. 'benutzer')\n"
+"        "
+
+#: crypto-install:475
+msgid "Username"
+msgstr "Benutzername"
+
+#: crypto-install:489
+msgid ""
+"\n"
+"        Host name of the local machine (e.g. 'mycomputer')\n"
+"        "
+msgstr ""
+"\n"
+"        Hostname des lokalen Rechners (z.B. 'meincomputer')\n"
+"        "
+
+#: crypto-install:492
+msgid "Host Name"
+msgstr "Hostname"
+
+#: crypto-install:506
+msgid ""
+"\n"
+"        Full name as it should appear in the key description (e.g. 'John "
+"Doe')\n"
+"        "
+msgstr ""
+"\n"
+"        Voller Name wie er in der Schlüsselbeschreibung erscheinen soll (z."
+"B. 'Max Mustermann')\n"
+"        "
+
+#: crypto-install:509
+msgid "Full Name"
+msgstr "Voller Name"
+
+#: crypto-install:523
+msgid ""
+"\n"
+"        Email address associated with the name (e.g. '<test@example.com>')\n"
+"        "
+msgstr ""
+"\n"
+"        Dem Namen zugeordnete Emailadresse (z.B. '<test@example.com>')\n"
+"        "
+
+#: crypto-install:526
+msgid "Email address"
+msgstr "Emailadresse"
+
+#: crypto-install:540
+msgid ""
+"\n"
+"        Comment phrase for the GnuPG key, if any (e.g. 'key for 2014')\n"
+"        "
+msgstr ""
+"\n"
+"        Kommentar für den GnuPG-Schlüssel, falls gewünscht (z.B. 'Schlüssel "
+"für 2014')\n"
+"        "
+
+#: crypto-install:543
+msgid "Comment phrase"
+msgstr "Kommentar"
+
+#: crypto-install:561
+msgid "Generate GnuPG key"
+msgstr "GnuPG-Schlüssel erstellen"
+
+#: crypto-install:572
+msgid "Generate OpenSSH key"
+msgstr "OpenSSH-Schlüssel erstellen"
+
+#: crypto-install:585
+msgid "Generate Keys"
+msgstr "Schlüssel erstellen"
+
+#: crypto-install:589
+msgid "Generate the keys as configured above"
+msgstr "Schlüssel wie oben konfiguriert erstellen"
+
+#: crypto-install:652
+msgid ""
+"\n"
+"        Generate a GnuPG key for '{}' and configure a default setup for it\n"
+"        "
+msgstr ""
+"\n"
+"        Erstelle einen GnuPG-Schlüssel für '{}' und konfiguriere ein "
+"Standardsetup dafür\n"
+"        "
+
+#: crypto-install:659
+msgid ""
+"\n"
+"        Generate an OpenSSH key for '{}' and configure a default setup for "
+"it\n"
+"        "
+msgstr ""
+"\n"
+"        Erstelle einen OpenSSH-Schlüssel für '{}' und konfiguriere ein "
+"Standardsetup dafür"
diff --git a/setup.cfg b/setup.cfg
new file mode 100644 (file)
index 0000000..328cd3c
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,4 @@
+[flake8]
+exclude = build
+select = E123,E226,E241,E242
+ignore = E211,E251,E401
diff --git a/setup.py b/setup.py
new file mode 100755 (executable)
index 0000000..9a24727
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python
+
+import glob, os, os.path, re, sys
+
+from distutils.command.build import build as _build
+from distutils.command.clean import clean as _clean
+from distutils.command.install import install as _install
+from setuptools.command.test import test as _test
+from distutils.core import setup
+from distutils.dir_util import remove_tree
+from setuptools import find_packages
+from subprocess import check_call, check_output
+
+
+def translations ():
+    return glob.glob("locale/*/*/*.po")
+
+
+def message_catalog (translation):
+    return os.path.splitext (os.path.basename (translation))[0] + ".mo"
+
+
+def message_catalogs ():
+    return [os.path.join (os.path.dirname (translation), message_catalog (translation)) for translation in translations ()]
+
+
+class test (_test):
+    user_options = [("pytest-args=", "a", "Arguments to pass to py.test")]
+
+    def initialize_options (self):
+        _test.initialize_options (self)
+
+        self.pytest_args = []
+
+    def finalize_options (self):
+        _test.finalize_options (self)
+
+        self.test_args = []
+        self.test_suite = True
+
+    def run_tests (self):
+        _test.run_tests (self)
+
+        # import here, cause outside the eggs aren't loaded
+        import pytest
+        sys.exit (pytest.main (self.pytest_args))
+
+
+class build (_build):
+    def run (self):
+        _build.run (self)
+
+        def update_version ():
+            with open (os.path.join (self.build_scripts, "crypto-install"), "r+") as file:
+                data = file.read ()
+                data = re.sub ("GIT-TAG", check_output (["git", "describe", "--abbrev=0", "--tags"]).strip (), data)
+                data = re.sub ("GIT-COMMIT", check_output (["git", "rev-parse", "--short=7", "HEAD"]).strip (), data)
+                data = re.sub ("GIT-BRANCH", check_output (["git", "rev-parse", "--abbrev-ref", "HEAD"]).strip (), data)
+                file.seek (0)
+                file.write (data)
+
+        self.execute (update_version, [], "Updating version")
+
+        def compile_message_catalog (translation, output):
+            check_call (["msgfmt", "-o", os.path.join (output, message_catalog (translation)), translation])
+
+        for translation in translations ():
+            output = os.path.join (self.build_base, os.path.dirname (translation))
+
+            self.mkpath (output)
+
+            self.execute (compile_message_catalog, [translation, output], "Compiling message catalog {}".format (translation))
+
+
+class install (_install):
+    def run (self):
+        _install.run (self)
+
+        self.copy_tree (os.path.join (self.build_base, "locale"),
+                        os.path.join (self.install_data, "share/locale"))
+
+
+class clean (_clean):
+    def run (self):
+        _clean.run (self)
+
+        if os.path.exists (self.build_base):
+            remove_tree (self.build_base, dry_run = self.dry_run)
+
+
+setup (
+    name = "crypto_install",
+    version = "0.0.1",
+    author = "Olof-Joachim Frahm",
+    author_email = "olof@macrolet.net",
+    url = "https://github.com/Ferada/crypto-install",
+    scripts = ["crypto-install"],
+    install_requires = [],
+    tests_require = ["pytest"],
+    cmdclass = {
+        "build": build,
+        "clean": clean,
+        "test": test,
+        "install": install
+    },
+    classifiers = [
+        "Development Status :: 3 - Alpha",
+        "Environment :: Console",
+        "Environment :: X11 Applications",
+        "Intended Audience :: End Users/Desktop",
+        "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
+        "Natural Language :: English",
+        "Natural Language :: German",
+        "Operating System :: POSIX",
+        "Programming Language :: Python :: 2.7",
+        "Programming Language :: Python :: 3.2",
+        "Topic :: Security :: Cryptography",
+        "Topic :: Utilities"
+    ])