diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2014, Anders Claesson
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Anders Claesson nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,73 @@
+```
+  ____  ____  ____
+ / __ \/ __ \/ __ \
+/ /_/ / /_/ / / / /
+\____/ .___/_/ /_/
+    /_/            
+```
+
+The `opn` command lets you open files and URLs with associated
+programs. It's intended to "just work" and to be trivial to
+configure. Its configuration resides in `~/.opnconfig`; here's an
+example:
+
+```
+[browser]
+browser: chromium
+
+[associations]
+mupdf:    .pdf
+emacs:    .lhs .hs .py .c
+chromium: .png .jpg .html .txt
+mpv:      .avi .mpg .mp4
+djview:   .djvu
+```
+
+With this configuration, PDFs would open in `mupdf`; Haskell, Python and
+C source files in `emacs`, etc.  If asked to open a file with no
+matching extension, or a file without an extension, `opn` tries to be a
+bit smart. If, for example, one runs
+
+```
+opn foo.h
+```
+
+then the header file `foo.h` would be opened in `emacs`. The reason is
+that such files have mime type `text/x-c`, and, as files with the
+extension `'.c'` shares this mime type, `opn` "guesses" that `'.h'` files
+also should be opened with `emacs`.
+
+Both `[browser]` and `[associations]` sections must be present and
+nonempty in `~/.opnconfig`. The `[browser]` section should in fact
+always have exactly one key, namely `browser`. So a (close to) minimal
+configuration looks something like this:
+
+```
+[browser]
+browser: chromium
+
+[associations]
+chromium: html
+```
+
+The `browser` is used for URLs and as a fallback, so with this
+configuration all files and URLs would open in `chromium`.
+
+Talking of `chromium`: on Linux `chromium` opens downloads using
+`xdg-open`. This works pretty well if you are using a full desktop
+environment like GNOME, KDE or Xfce, but less so if you are running a
+light window manager such as i3 or xmonad.  We can, however, exploit a
+behavior of `xdg-open` to effectively replace it with `opn`. As a
+fallback, when no supported desktop session is running, `xdg-open` uses
+`$BROWSER`. Thus putting
+
+```
+export BROWSER=opn
+```
+
+in your `.bashrc`, or something similar for your favorite shell, will
+"trick" `chromium` and `xdg-open` into using `opn`. This is my own main
+use for `opn`.
+
+For further info see the man page:
+<https://github.com/akc/opn/blob/master/opn.md>.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,8 @@
+import Distribution.Simple
+import System.Process
+import System.Exit
+
+main = defaultMainWithHooks $ simpleUserHooks { postBuild = makeManPage }
+
+makeManPage _ _ _ _ =
+    runCommand "make opn.1" >>= waitForProcess >>= exitWith
diff --git a/opn.1 b/opn.1
new file mode 100644
--- /dev/null
+++ b/opn.1
@@ -0,0 +1,95 @@
+.TH "OPN" "1" "24 Aug 2014" "Opn User Manual" "Version 0.1.0"
+.SH NAME
+.PP
+opn \- open files and URLs with associated programs
+.SH SYNOPSIS
+.PP
+opn (\-\-version | [\-\-dry\-run] PATHS...)
+.SH DESCRIPTION
+.PP
+The opn command lets you open files and URLs with associated programs.
+It\[aq]s intended to "just work" and to be trivial to configure.
+.PP
+The configuration resides in \f[C]~/.opnconfig\f[]; here\[aq]s an
+example:
+.IP
+.nf
+\f[C]
+[browser]
+browser:\ chromium
+
+[associations]
+mupdf:\ \ \ \ .pdf
+emacs:\ \ \ \ .lhs\ .hs\ .py\ .c
+chromium:\ .png\ .jpg\ .html\ .txt
+mpv:\ \ \ \ \ \ .avi\ .mpg\ .mp4
+djview:\ \ \ .djvu
+\f[]
+.fi
+.PP
+Both \f[C][browser]\f[] and \f[C][associations]\f[] sections must be
+present and nonempty.
+The \f[C][browser]\f[] section should in fact always have exactly one
+key, namely \f[C]browser\f[].
+Comments must start at the beginning of the line and start with
+\f[C]\[aq];\[aq]\f[] or \f[C]\[aq]#\[aq]\f[].
+.PP
+If asked to open a file with no matching extension, or a file without an
+extension, \f[C]opn\f[] tries to be a bit smart.
+For example, a header file, say \f[C]foo.h\f[], would be opened in
+\f[C]emacs\f[].
+The reason is that header files have mime type \f[C]text/x\-c\f[], the
+same type as \f[C]\[aq].c\[aq]\f[] files, and thus \f[C]opn\f[]
+"guesses" that \f[C]\[aq].h\[aq]\f[] files also should be opened with
+\f[C]emacs\f[].
+.SH OPTIONS
+.TP
+.B \-\-dry\-run
+Display the command, or commands, that would be executed, then exit.
+.RS
+.RE
+.TP
+.B \-\-version
+Display version information.
+.RS
+.RE
+.TP
+.B \-\-help
+Display a short help message.
+.RS
+.RE
+.SH EXAMPLES
+.PP
+Open \f[C]opn.hs\f[] in the application associated with the extension
+\f[C]\[aq].hs\[aq]\f[]:
+.IP
+.nf
+\f[C]
+opn\ opn.hs
+\f[]
+.fi
+.PP
+Open \f[C]http://akc.is/\f[] in the browser specified under the
+\f[C][browser]\f[] section:
+.IP
+.nf
+\f[C]
+opn\ http://akc.is/
+\f[]
+.fi
+.PP
+For each file in the current directory display the manner in which it
+would be opened:
+.IP
+.nf
+\f[C]
+opn\ \-\-dry\-run\ *
+\f[]
+.fi
+.SH SEE ALSO
+.PP
+The source code to opn can be found at <https://github.com/akc/opn>.
+See also the \f[C]README\f[] on that page.
+.SH AUTHOR
+.PP
+Anders Claesson <http://akc.is>
diff --git a/opn.cabal b/opn.cabal
new file mode 100644
--- /dev/null
+++ b/opn.cabal
@@ -0,0 +1,28 @@
+name:                opn
+version:             0.1.0
+synopsis:            Open files or URLs using associated programs.
+
+description:         The `opn` command lets you open files and URLs with
+                     associated programs. It's intended to "just work" and to
+                     be trivial to configure.
+
+homepage:            http://github.com/akc/opn
+license:             BSD3
+license-file:        LICENSE
+author:              Anders Claesson
+maintainer:          anders.claesson@gmail.com
+Category:            Application, Console
+Build-type:          Custom
+Extra-Source-Files:  README.md opn.1
+cabal-version:       >=1.8
+
+source-repository head
+  type:                git
+  location:            git://github.com/akc/opn.git
+
+executable opn
+  main-is:             opn.hs
+  ghc-options:         -Wall
+  build-depends:       base ==4.6.*, ini, text, process,
+                       unordered-containers, optparse-applicative,
+                       network-uri, directory, filepath
diff --git a/opn.hs b/opn.hs
new file mode 100644
--- /dev/null
+++ b/opn.hs
@@ -0,0 +1,120 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- |
+-- Copyright   : Anders Claesson 2014
+-- Maintainer  : Anders Claesson <anders.claesson@gmail.com>
+-- License     : BSD-3
+--
+
+import           Data.Maybe (mapMaybe, fromMaybe)
+import           Data.Text (Text)
+import qualified Data.Text as T
+import qualified Data.Text.IO as T
+import           Data.HashMap.Strict (HashMap)
+import qualified Data.HashMap.Strict as M
+import           Data.Ini (Ini(..), parseIni)
+import           Options.Applicative
+import           Control.Monad (forM_, void, unless)
+import           System.IO (hPutStrLn, stderr)
+import           System.Process (runProcess, readProcess)
+import           System.FilePath (takeExtension, (</>))
+import           System.Directory (doesFileExist, getHomeDirectory)
+import           Network.URI (isAbsoluteURI)
+import           Opn.Guess (guess)
+
+type Command   = Text
+type Browser   = Command
+type PathOrURL = String
+type Homedir   = String
+type Extension = Text
+type ECMap     = HashMap Extension Command
+type Config    = (Browser, ECMap)
+
+data Opts = Opts
+    { dryrun  :: Bool
+    , paths   :: [String]
+    }
+
+name :: String
+name = "opn 0.1.0"
+
+optsParser :: Parser (Maybe Opts)
+optsParser = hiddenHelp <*> versionOpt <|> (Just <$> (Opts
+    <$> switch (long "dry-run" <> dryrunHelp)
+    <*> some (argument str (metavar "PATHS..."))))
+  where
+    dryrunHelp = help "Display command(s) that would be executed, then exit."
+    hiddenHelp = abortOption ShowHelpText $ hidden <> short 'h' <> long "help"
+    versionOpt = flag' Nothing (long "version")
+
+lookupCommand :: ECMap -> Extension -> Maybe Command
+lookupCommand m ext = T.strip <$> M.lookup ext m
+
+getExtensions :: FilePath -> IO [Extension]
+getExtensions fpath =
+    (ext ++) <$> guess <$> mimeType
+  where
+    ext = case takeExtension fpath of { [] -> []; e -> [T.pack e]}
+    mimeType = getValue <$> runFileCmd
+    getValue = T.pack . drop 1 . reverse . takeWhile (/= ':') . drop 1 . reverse
+    runFileCmd = readProcess "file" ["--mime-type", "-L", fpath] ""
+
+getCommand :: Config -> PathOrURL -> IO (Maybe Command)
+getCommand (browser, m) s = doesFileExist s >>= getCmd
+  where
+    getCmd True = do
+        exts <- getExtensions s
+        case mapMaybe (lookupCommand m) exts of
+            []      -> return (Just browser)
+            (cmd:_) -> return (Just cmd)
+    getCmd False =
+        if isAbsoluteURI s
+            then return (Just browser)
+            else do
+                hPutStrLn stderr (show s ++
+                    " is neither a file-path nor an absolute URL; skipping.")
+                return Nothing
+
+readIni :: Homedir -> IO Ini
+readIni home = do
+    opnconfigExists <- doesFileExist opnconfig
+    unless opnconfigExists $ do
+        hPutStrLn stderr (opnconfig ++ " does not exist; creating a stub.")
+        T.writeFile opnconfig
+            "[browser]\nbrowser: chromium\n\n[associations]\nchromium: html\n"
+    (either error id . parseIni . adjust) <$> T.readFile opnconfig
+  where
+    opnconfig = home </> ".opnconfig"
+    adjust = T.unlines . map T.stripStart . T.lines
+
+mkConfig :: Ini -> Config
+mkConfig (Ini ini) = (browser, d)
+  where
+    d = M.fromList $ M.toList cmdMap >>= \(c,es) -> [(e,c) | e <- T.words es]
+    browser  = let s = "browser" in fromMaybe (errKey s) $ M.lookup s (getSec s)
+    cmdMap   = getSec "associations"
+    getSec s = fromMaybe (errSec s) $ M.lookup s ini
+    errSec s = error $ "Couldn't find required section " ++ show s
+    errKey s = error $ "Couldn't find required key "     ++ show s
+
+readConfig :: Homedir -> IO Config
+readConfig = fmap mkConfig . readIni
+
+run :: Command -> [String] -> IO ()
+run cmd args = void $
+    runProcess (T.unpack cmd) args Nothing Nothing Nothing Nothing Nothing
+
+opn :: Maybe Opts -> IO ()
+opn Nothing     = putStrLn name
+opn (Just opts) = do
+    conf <- readConfig =<< getHomeDirectory
+    forM_ (paths opts) $ \path ->
+        getCommand conf path >>= \c ->
+            case c of
+                Just cmd -> run' cmd [path]
+                Nothing  -> return ()
+  where
+    run' = if dryrun opts then \c [s] -> T.putStr c >> putStrLn (' ':s) else run
+
+main :: IO ()
+main =
+    customExecParser (prefs showHelpOnError) (info optsParser fullDesc) >>= opn
