packages feed

terminfo-hs (empty) → 0.1.0.0

raw patch · 8 files changed

+494/−0 lines, 8 filesdep +QuickCheckdep +attoparsecdep +basesetup-changed

Dependencies added: QuickCheck, attoparsec, base, bytestring, containers, directory, errors, filepath

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2013, Bryan Richter++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 Bryan Richter 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.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ System/Terminfo.hs view
@@ -0,0 +1,162 @@+-- |+-- Module      :  System.Terminfo+-- Copyright   :  (c) Bryan Richter (2013)+-- License     :  BSD-style+-- Maintainer  :  bryan.richter@gmail.com+--+-- This is a pure-Haskell (no FFI) module for accessing terminfo databases,+-- which contain characteristics, or capabilities, for the various+-- terminals such as screen, vt100, or xterm. Among other things, the+-- capabilities include the idiosyncratic character sequences needed to+-- send commands to the terminal. These commands include things like cursor+-- movement.+--+-- For a deeper understanding of terminfo, consult the man pages for+-- term(5) and terminfo(5).+--+-- There are three parts to this module: acquiring a terminfo database,+-- querying the database, and defining the capabilities.+--+-- This module is dead simple, so a single example will hopefully suffice+-- to demonstrate its usage.+--+-- @+-- import System.Terminfo+-- import System.Terminfo.Caps as C+-- uglyExample :: IO (Maybe Int)+-- uglyExample = do+--     term \<- fromJust \<$> lookupEnv \"TERM\"+--     db \<- 'acquireDatabase' term+--     let maxColors (Right d) = 'queryNumTermCap' d C.'MaxColors'+--     return $ maxColors db+-- @+--+-- >>> uglyExample+-- Just 256+--++module System.Terminfo (+    -- * Acquiring a Database+      acquireDatabase+    -- * Querying Capabilities+    -- $queryFuncs+    , queryBoolTermCap+    , queryNumTermCap+    , queryStrTermCap+    -- * The Capabilities+    -- $capabilities++    -- * The Database Type+    , TIDatabase++    ) where++import Control.Applicative ((<$>), (<|>))+import Control.Error+import Control.Monad ((<=<), filterM)+import qualified Data.ByteString as B+import qualified Data.Map.Lazy as M+import System.Directory+import System.FilePath+import System.IO++import System.Terminfo.Types+import System.Terminfo.DBParse+import System.Terminfo.Internal (terminfoDBLocs)+import System.Terminfo.Caps++data DBType = BerkeleyDB | DirTreeDB+    deriving(Show)++-- Old MacDonald had a farm...+type EIO = EitherT String IO++acquireDatabase+    :: String -- ^ System name+    -> IO (Either String TIDatabase)+       -- ^ A database object for the terminal, if it exists.+acquireDatabase = runEitherT . (parseDBFile <=< findDBFile)++findDBFile :: String -> EIO (DBType, FilePath)+findDBFile term = case term of+    (c:_) -> dbFileM c term `orLeft` "No terminfo db found"+    _     -> hoistEither $ Left "User specified null terminal name"+  where+    orLeft = flip noteT+    dbFileM c t = dirTreeDB c t <|> berkeleyDB++-- | Not implemented+berkeleyDB :: MaybeT IO (DBType, FilePath)+berkeleyDB = nothing++dirTreeDB :: Char -> String -> MaybeT IO (DBType, FilePath)+dirTreeDB c term = MaybeT $ do+    path <- findFirst =<< map (</> [c] </> term) <$> terminfoDBLocs+    return $ (,) DirTreeDB <$> path++findFirst :: [FilePath] -> IO (Maybe FilePath)+findFirst = fmap headMay . filterM doesFileExist++parseDBFile :: (DBType, FilePath) -> EIO TIDatabase+parseDBFile (db, f) = case db of+    DirTreeDB -> extractDirTreeDB f+    BerkeleyDB -> hoistEither+        $ Left "BerkeleyDB support not yet implemented"++-- | Extract a 'TIDatabase' from the specified file. IO exceptions are+-- left to their own devices.+extractDirTreeDB :: FilePath+                 -> EIO TIDatabase+extractDirTreeDB =+    hoistEither . parseDB+    <=< rightT . B.hGetContents+    <=< rightT . flip openBinaryFile ReadMode+  where+    rightT = EitherT . fmap Right++queryBoolTermCap :: TIDatabase+                 -> BoolTermCap+                 -> Bool+queryBoolTermCap (TIDatabase (TCBMap vals) _ _) cap =+    fromMaybe False $ M.lookup cap vals++queryNumTermCap :: TIDatabase+                -> NumTermCap+                -> Maybe Int+queryNumTermCap (TIDatabase _ (TCNMap vals) _) cap = M.lookup cap vals++-- | As this is a dead simple module, no \'smart\' handling of the+-- returned string is implemented. In particular, placeholders for+-- buffer characters and command arguments are left as-is. This will be+-- rectified eventually, probably in a separate module.+queryStrTermCap :: TIDatabase+                -> StrTermCap+                -> Maybe String+queryStrTermCap (TIDatabase _ _ (TCSMap vals)) cap = M.lookup cap vals++--+-- DOCUMENTATION+--++++-- $queryFuncs+--+-- For each of these three actions, the first argument is the database to+-- query, and the second argument is the capability to look up.+--+-- I'm not super proud of this interface, but it's the best I can manage at+-- present without requiring lots of mostly-empty case expressions. Perhaps+-- someone will suggest a more interesting solution.++-- $capabilities+--+-- /see/ "System.Terminfo.Caps"+--+-- There are no less than 497 capabilities specified in term.h on my+-- Intel-based Ubuntu 12.04 notebook (slightly fewer in the terminfo(5) man+-- page). The naive way of making these available to the user is as data+-- constructors, and that is what I have done here.+--+-- The number of constructors absolutely crushes the namespace. I have+-- sequestered them into their own module to try to alleviate the pain.
+ System/Terminfo/DBParse.hs view
@@ -0,0 +1,126 @@+{-# LANGUAGE RecordWildCards #-}++-- |+-- Module      :  System.Terminfo.DBParse+-- Copyright   :  (c) Bryan Richter (2013)+-- License     :  BSD-style+-- +-- Maintainer  :  bryan.richter@gmail.com+--+-- An internal module encapsulating methods for parsing a terminfo file as+-- generated by tic(1). The primary reference is the term(5) manpage.+--++module System.Terminfo.DBParse+    ( parseDB+    ) where++import Control.Applicative ((<$>), (<*>))+import Control.Arrow ((***))+import qualified Control.Arrow as Arr+import Control.Monad ((<=<), when, void)+import Data.Attoparsec as A+import Data.ByteString (ByteString)+import qualified Data.ByteString as B+import Data.Char (chr)+import qualified Data.Map.Lazy as M+import Data.Word (Word16)++import System.Terminfo.Types++-- | term(5) defines a short integer as two 8-bit bytes, so:+type ShortInt = Word16++-- | short ints are stored little-endian.+shortInt :: Integral a => ShortInt -> Parser a+shortInt i = word8 first >> word8 second >> return (fromIntegral i)+  where+    (second, first) = (fromIntegral *** fromIntegral) $ i `divMod` 256++-- | short ints are stored little-endian.+--+-- (-1) is represented by the two bytes 0o377 0o377.+--+-- Return type is Int so I can include (-1) in the possible outputs. I+-- wonder if I will regret this.+anyShortInt :: Parser Int+anyShortInt = do+    first <- fromIntegral <$> anyWord8+    second <- fromIntegral <$> anyWord8+    return $ if first == 0o377 && second == 0o377+       then (-1)+       else 256*second + first++parseDB :: ByteString -> Either String TIDatabase+parseDB = parseOnly tiDatabase++tiDatabase :: Parser TIDatabase+tiDatabase = do+    Header{..} <- header+    -- Ignore names+    _ <- A.take namesSize+    bools <- boolCaps boolSize+    -- Align on an even byte+    when (odd boolSize) (void $ A.take 1)+    nums <- numCaps numIntegers+    strs <- stringCaps numOffsets stringSize+    -- TODO: extended info+    return $ TIDatabase bools nums strs++boolCaps :: Int -- ^ Number of caps+         -> Parser TCBMap+boolCaps =+    return . TCBMap . M.fromList . zip keys . map (== 1) . B.unpack+        <=< A.take+  where+    {-keys :: [BoolTermCap]-}+    keys = [minBound ..]++-- Negative values indicate missing capability.+numCaps :: Int -- ^ Number of caps+        -> Parser TCNMap+numCaps = return . TCNMap . M.fromList . filter notNeg . zip keys+    <=< flip A.count anyShortInt+  where+    notNeg = ((/= -1) . snd)+    {-keys :: [BoolTermCap]-}+    keys = [minBound ..]++stringCaps :: Int -- ^ Number of caps+           -> Int -- ^ Size of table+           -> Parser TCSMap+stringCaps numOffsets stringSize = do+    offs <- A.count numOffsets anyShortInt+    stringTable <- A.take stringSize+    return+        $ TCSMap $ M.fromList $ map (parseValue stringTable)+        $ filter notNeg $ zip keys offs+  where+    notNeg = ((/= -1) . snd)+    keys = [minBound ..]+    parseValue tbl = Arr.second $ parseString tbl+    parseString table offset =+        asString+        $ B.takeWhile (/= 0)  -- null-terminated+        $ B.drop offset table -- starts at offset+    asString = map (chr . fromIntegral) . B.unpack++-- | the magic number for term files+magic :: Parser Int+magic = shortInt 0o432 <?> "Not a terminfo file (bad magic)"++data Header = Header+     { namesSize :: Int+     , boolSize :: Int+     , numIntegers :: Int+     , numOffsets :: Int+     , stringSize :: Int+     }+     deriving (Show)++header :: Parser Header+header = magic >> Header <$> anyShortInt+                         <*> anyShortInt+                         <*> anyShortInt+                         <*> anyShortInt+                         <*> anyShortInt
+ System/Terminfo/Internal.hs view
@@ -0,0 +1,66 @@+module System.Terminfo.Internal+    ( terminfoDBLocs+    , locationsPure+    ) where++import Control.Applicative ((<$>), (<*>), pure)+import Control.Error+import System.Environment (lookupEnv)+import System.FilePath++terminfoDBLocs :: IO [FilePath]+terminfoDBLocs = locationsPure+    <$> lookupEnv "TERMINFO"+    <*> (lookupEnv "HOME" <$$/> ".terminfo")+    <*> lookupEnv "TERMINFO_DIRS"+    <*> pure ["/lib/terminfo", "/usr/share/terminfo"]+++(<$/>) :: Functor f => f FilePath -> FilePath -> f FilePath+fa <$/> b = (</> b) <$> fa+infixr 4 <$/>++(<$$/>) :: (Functor f, Functor g)+        => f (g FilePath)+        -> FilePath+        -> f (g FilePath)+ffa <$$/> b = fmap (<$/> b) ffa+infixr 4 <$$/>++-- | I hate this name. There is undoubtedly a better way of structuring all+-- of this, starting way up at 'dirTreeDB'.+locationsPure :: Maybe FilePath -- ^ Override directory+              -> Maybe FilePath -- ^ $HOME+              -> Maybe String   -- ^ $TERMINFO_DIRS+              -> [FilePath]     -- ^ Defaults+              -> [FilePath]+locationsPure ovr usr termdirs defs = case ovr of+    Just override -> [override]+    Nothing       -> catMaybes [usr] ++ system++  where+    system = case termdirs of+        Just list -> parseTDVar defs list+        Nothing   -> defs++parseTDVar :: [String] -> String -> [String]+parseTDVar defs = replace "" defs . split ':'++-- | Replace an element with multiple replacements+replace :: Eq a => a -> [a] -> [a] -> [a]+replace old news = foldr (\x acc -> if x == old+                                       then news ++ acc+                                       else x : acc)+                         []++-- | split, as seen in ByteString and Text, but for Strings.+split :: Char -> String -> [String]+split s = foldr go [[]]+  where+    go c acc = if c /= s+                  then unshift c acc+                  else "" : acc++    -- | /perldoc -f unshift/+    unshift c (xs:xss) = (c:xs) : xss+    unshift c []       = [[c]]
+ System/Terminfo/Types.hs view
@@ -0,0 +1,20 @@+module System.Terminfo.Types+    ( TIDatabase(..)+    , TCBMap(..)+    , TCNMap(..)+    , TCSMap(..)+    ) where++import Data.Map.Lazy (Map)++import System.Terminfo.Caps++data TCBMap = TCBMap (Map BoolTermCap Bool)+    deriving (Show)+data TCNMap = TCNMap (Map NumTermCap Int)+    deriving (Show)+data TCSMap = TCSMap (Map StrTermCap String)+    deriving (Show)++data TIDatabase = TIDatabase TCBMap TCNMap TCSMap+    deriving (Show)
+ terminfo-hs.cabal view
@@ -0,0 +1,46 @@+name:                terminfo-hs+version:             0.1.0.0+synopsis:            A pure-Haskell (no FFI) module for accessing terminfo databases+description:         This module can acquire terminfo databases and query+                     them for terminal capabilities. For details of+                     terminfo, consult the man pages for term(5) and+                     terminfo(5).++                     This package is dead simple, and doesn't do anything+                     fancy with the capabilities themselves. It merely+                     provides a means for accessing them.++license:             BSD3+license-file:        LICENSE+author:              Bryan Richter+maintainer:          bryan.richter@gmail.com+copyright:           Bryan Richter, 2013+category:            System, Terminal+build-type:          Simple+cabal-version:       >=1.8++source-repository head+  type: git+  location: https://github.com/chreekat/terminfo-hs++source-repository this+  type: git+  tag: 0.1.0.0+  location: https://github.com/chreekat/terminfo-hs/tree/0.1.0.0++library+  ghc-options:         -Wall+  exposed-modules:     System.Terminfo+  other-modules:       System.Terminfo.DBParse,+                       System.Terminfo.Types+                       System.Terminfo.Internal+  build-depends:       base ==4.*, errors ==1.*,+                       bytestring ==0.*, directory ==1.*,+                       filepath ==1.*, attoparsec ==0.*,+                       containers ==0.5.*++test-suite System.Terminfo.Internal+  type:                exitcode-stdio-1.0+  main-is:             test.hs+  build-depends:       base ==4.*, filepath ==1.*, directory ==1.*,+                       errors ==1.*, QuickCheck
+ test.hs view
@@ -0,0 +1,42 @@+{-# LANGUAGE TemplateHaskell #-}++import Test.QuickCheck+import Test.QuickCheck.All+import System.Terminfo.Internal++import Data.Maybe++prop_useOverride ovr =+    forAll notLikely $ \usr ->+    forAll notLikely $ \tds ->+    forAll oftenNull $ \defs ->+    classify (isNothing usr && isNothing tds && null defs)+        "Nothing else specified"$+    classify (isJust usr && isJust tds && not (null defs))+        "Everything else specified"$+    locationsPure (Just ovr) usr tds defs == [ovr]++notLikely = frequency [(2, return Nothing), (1, fmap Just arbitrary)]+oftenNull = frequency [(2, return []), (1, arbitrary)]++prop_includeHome usr =+    forAll notLikely $ \tds ->+    forAll oftenNull $ \defs ->+    let+        withUsr    = locationsPure Nothing (Just usr) tds defs+        withoutUsr = locationsPure Nothing Nothing    tds defs+    in+    classify (isJust tds || not (null defs))+        "Something besides HOME specified"$+    withUsr == usr : withoutUsr++prop_terminfoDirsOverridesDefaults tds =+    forAll (oneof [return [], listOf1 arbitrary]) $ \defs ->+    classify (not $ null defs) "Defaults specified"$+    locationsPure Nothing Nothing (Just tds) defs+        == parseTDVar defs tds++prop_useDefaults defs =+    locationsPure Nothing Nothing Nothing defs == defs++main = $quickCheckAll