opn (empty) → 0.1.0
raw patch · 6 files changed
+354/−0 lines, 6 filesdep +basedep +directorydep +filepathbuild-type:Customsetup-changed
Dependencies added: base, directory, filepath, ini, network-uri, optparse-applicative, process, text, unordered-containers
Files
+ LICENSE view
@@ -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.
+ README.md view
@@ -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>.
+ Setup.hs view
@@ -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
+ opn.1 view
@@ -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>
+ opn.cabal view
@@ -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
+ opn.hs view
@@ -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