alga-0.2.0: src/Alga/Interaction/Commands.hs
--
-- This module describes all supported ALGA commands. It also provides all
-- the functionality to load source files and patch XML files.
--
-- Copyright © 2015–2016 Mark Karpov
--
-- ALGA is free software: you can redistribute it and/or modify it under the
-- terms of the GNU General Public License as published by the Free Software
-- Foundation, either version 3 of the License, or (at your option) any
-- later version.
--
-- ALGA is distributed in the hope that it will be useful, but WITHOUT ANY
-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-- FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
-- details.
--
-- You should have received a copy of the GNU General Public License along
-- with this program. If not, see <http://www.gnu.org/licenses/>.
{-# LANGUAGE RecordWildCards #-}
module Alga.Interaction.Commands
( processCmd
, completionFunc
, cmdBackend
, cmdLoad
, cmdMake
, cmdPrefix )
where
import Alga.Interaction.Base
import Alga.Language
import Alga.Representation
import Alga.Translation
import Control.Monad.Catch
import Control.Monad.IO.Class
import Control.Monad.Reader
import Control.Monad.State.Class
import Data.Char (isSpace)
import Data.Foldable (find)
import Data.List (elemIndex, isPrefixOf)
import Data.Maybe (fromMaybe, listToMaybe)
import Data.Ratio (approxRational)
import Data.Text.Lazy (Text)
import Formatting
import Numeric.Natural
import Path
import Path.IO
import System.Exit (exitSuccess, ExitCode)
import qualified Data.Text.Lazy as T
import qualified Data.Text.Lazy.IO as T
import qualified System.Console.Haskeline as L
import qualified System.FilePath as FP
-- | Description of REPL command.
data Cmd = Cmd
{ cmdName :: String -- ^ Name of command
, cmdFunc :: String -> Alga () -- ^ Action it performs
, cmdDesc :: Text -- ^ Description of command
, cmdComp :: CompletionScheme -- ^ How to complete arguments of the command
}
-- | Scale (linear or logarithmic) to use with some utilities.
data Scale = Lin | Log deriving (Eq, Show)
-- | How to complete arguments of command.
data CompletionScheme
= None -- ^ Don't complete at all
| Files -- ^ Complete as file names
| Names -- ^ Complete as definition names
deriving (Eq, Show)
-- | All defined commands.
commands :: [Cmd]
commands =
-- name action description completion
[ Cmd "backend" cmdBackend "Set backend (name of DAW)" None
, Cmd "cd" cmdCd "Change working directory" Files
, Cmd "clear" cmdClear "Restore default state of environment" None
, Cmd "def" cmdDef "Print definition of given symbol" Names
, Cmd "help" cmdHelp "Show this help text" None
, Cmd "lin" cmdLin "Linear scale conversion to ratio" None
, Cmd "load" cmdLoad' "Load definitions from given file" Files
, Cmd "log" cmdLog "Logarithmic scale conversion to ratio" None
, Cmd "make" cmdMake' "Patch an XML file" Files
, Cmd "prvlen" cmdLength "Set length of displayed results" None
, Cmd "purge" cmdPurge "Remove redundant definitions" None
, Cmd "pwd" cmdPwd "Print working directory" None
, Cmd "quit" cmdQuit "Quit the interactive environment" None
, Cmd "ratio" cmdRatio "Real number to ratio converter" None
, Cmd "save" cmdSave "Save current environment in file" Files
, Cmd "udef" cmdUdef "Remove definition of given symbol" Names
, Cmd "vol" cmdVol "Convert decibels to ratio" None ]
-- | Process command (with its arguments).
processCmd :: Text -> Alga ()
processCmd txt =
case find g commands of
Just Cmd { cmdFunc = f } -> do
result <- try . f . T.unpack . T.strip $ args
:: Alga (Either SomeException ())
case result of
Left e ->
case fromException e :: Maybe ExitCode of
Just _ -> throwM e
Nothing -> spitExc e
Right _ -> return ()
Nothing -> liftIO $
fprint ("Unknown command, try " % string % "help\n") cmdPrefix
where g Cmd { cmdName = c } = c == dropCmdPrefix (T.unpack cmd)
(cmd, args) = T.break isSpace (T.strip txt)
-- | Completion function to work with Haskeline.
completionFunc
:: (HasEnv m, MonadIO m, MonadReader AlgaCfg m, MonadState AlgaSt m)
=> L.CompletionFunc m
completionFunc = L.completeWordWithPrev Nothing " " getCompletions
-- | Generate completions.
getCompletions
:: (HasEnv m, MonadIO m, MonadReader AlgaCfg m, MonadState AlgaSt m)
=> String -- ^ Contents of line before cursor, reversed
-> String -- ^ Contents of line after cursor
-> m [L.Completion] -- ^ List of completions
getCompletions prev word = do
names <- getRefs
files <- L.listFiles word
let cmds = (cmdPrefix ++) . cmdName <$> commands
f = fmap L.simpleCompletion . filter (word `isPrefixOf`)
g None = []
g Files = files
g Names = f names
return $
case words . reverse $ prev of
[] -> f $ cmds ++ names
(c:_) ->
case c `elemIndex` cmds of
Just i -> g . cmdComp $ commands !! i
Nothing -> f names
-- | Change set backend (name of DAW).
cmdBackend :: MonadState AlgaSt m => String -> m ()
cmdBackend arg = modify $ \st -> st { stBackend = toBackend arg }
-- | Change working directory.
cmdCd :: (MonadIO m, MonadCatch m) => String -> m ()
cmdCd next' = do
mnext <- forgivingAbsence (resolveDir' next')
case mnext of
Nothing -> liftIO $ fprint ("Cannot cd to \"" % string % "\"\n") next'
Just next -> do
setCurrentDir next
liftIO $ fprint ("Changed to \"" % string % "\".\n") (fromAbsDir next)
-- | Restore default state of environment.
cmdClear :: (HasEnv m, MonadIO m) => String -> m ()
cmdClear _ = do
clearDefs
liftIO (T.putStrLn "Environment cleared")
-- | Print definition of given symbol.
cmdDef :: (HasEnv m, MonadIO m) => String -> m ()
cmdDef arg = mapM_ f (words arg)
where f name = getSrc name >>= liftIO . T.putStr
-- | Show help with list of all available commands.
cmdHelp :: MonadIO m => String -> m ()
cmdHelp _ = liftIO $ do
T.putStrLn "Available commands:"
mapM_ f commands
where f Cmd {..} = fprint fmt cmdPrefix cmdName cmdDesc
fmt = " " % string % (right 24 ' ' %. string) % text % "\n"
-- | Linear scale conversion to ratio.
cmdLin :: (MonadIO m, MonadReader AlgaCfg m) => String -> m ()
cmdLin str = pRatio Lin (parseNum a 0) (parseNum b 1) (parseNum s 0)
where (a:b:s:_) = parseArgs str
-- | Load definitions from given file. Note that this version of the command
-- is used in REPL, not 'cmdLoad'.
cmdLoad' :: (HasEnv m, MonadIO m, MonadState AlgaSt m, MonadThrow m)
=> String -> m ()
cmdLoad' = cmdLoad . words
-- | Alternative interface to loading functionality. This one is used in
-- main module.
cmdLoad :: (HasEnv m, MonadIO m, MonadState AlgaSt m, MonadThrow m)
=> [FilePath] -> m ()
cmdLoad = mapM_ loadOne
-- | Load single source file.
loadOne :: (HasEnv m, MonadIO m, MonadState AlgaSt m, MonadThrow m)
=> FilePath -> m ()
loadOne given = do
file <- output given ""
let fpath = fromAbsFile file
b <- doesFileExist file
if b
then do
contents <- liftIO $ T.readFile fpath
case parseAlga fpath contents of
Right x -> do
mapM_ f x
setFileName file
liftIO $ fprint
("\"" % string % "\" loaded successfully\n")
fpath
Left x -> liftIO $ fprint (string % "\n") x
else liftIO $ fprint ("Could not find \"" % string % "\"\n") fpath
where f (Definition n t) = processDef n t
f (Exposition _) = return ()
-- | Logarithmic scale conversion to ratio.
cmdLog :: (MonadIO m, MonadReader AlgaCfg m) => String -> m ()
cmdLog str = pRatio Log (parseNum a 0) (parseNum b e) (parseNum s 0)
where (a:b:s:_) = parseArgs str
e = exp 1
-- | Version of 'cmdMake' used by REPL.
cmdMake' :: (HasEnv m, MonadIO m, MonadState AlgaSt m, MonadThrow m)
=> String -> m ()
cmdMake' str =
let (s:b:f:_) = parseArgs str
in cmdMake (parseNum s defaultSeed)
(parseNum b defaultBeats)
f
-- | Patch an XML file.
cmdMake :: (HasEnv m, MonadIO m, MonadState AlgaSt m, MonadThrow m)
=> Natural -- ^ Seed for random number generator
-> Double -- ^ Desired duration in number of whole notes
-> FilePath -- ^ Where to find file to patch
-> m ()
cmdMake s b f = do
file <- output f "xml"
backend <- gets stBackend
status <- patchAuto s b file backend
let msg = if status == 0
then "File patched successfully \"" % string % "\"\n"
else "Failed to patch file \"" % string % "\"\n"
liftIO $ fprint msg (fromAbsFile file)
-- | Set length of displayed results.
cmdLength :: MonadState AlgaSt m => String -> m ()
cmdLength arg = do
len <- gets stPrevLen
modify $ \st -> st { stPrevLen = parseNum (trim arg) len }
-- | Remove redundant definitions.
cmdPurge :: (HasEnv m, MonadIO m) => String -> m ()
cmdPurge _ = do
topDefs >>= purgeEnv
liftIO $ T.putStrLn "Environment purged"
-- | Print working directory.
cmdPwd :: MonadIO m => String -> m ()
cmdPwd _ = liftIO (getCurrentDir >>= putStrLn . fromAbsDir)
-- | Quit the interactive environment.
cmdQuit :: MonadIO m => String -> m ()
cmdQuit _ = liftIO exitSuccess
-- | Real number to ratio converter.
cmdRatio :: (MonadIO m, MonadReader AlgaCfg m) => String -> m ()
cmdRatio = pRatio Lin 0 1 . (`parseNum` 0)
-- | Save current environment in file.
cmdSave :: (HasEnv m, MonadIO m, MonadState AlgaSt m, MonadThrow m)
=> String -> m ()
cmdSave given = do
file <- output given ""
let fpath = fromAbsFile file
src <- fullSrc
liftIO $ T.writeFile fpath src
setFileName file
liftIO $ fprint ("Environment saved as \"" % string % "\"\n") fpath
-- | Undefine definitions.
cmdUdef :: (HasEnv m, MonadIO m) => String -> m ()
cmdUdef arg = mapM_ f (words arg)
where f name = do
remDef name
liftIO $ fprint ("Definition for ‘" % string % "’ removed\n") name
-- | Convert decibels to ratio.
cmdVol :: (MonadIO m, MonadReader AlgaCfg m) => String -> m ()
cmdVol = pRatio Lin 0 1 . dBToRatio . (`parseNum` 0)
where dBToRatio dB = 10 ** (dB / 10)
-- | Universal conversion to ratios.
pRatio :: (MonadIO m, MonadReader AlgaCfg m)
=> Scale -- ^ Scale to use (linear or logarithmic)
-> Double -- ^ Minimum
-> Double -- ^ Maximum
-> Double -- ^ Value between minimum and maximum
-> m ()
pRatio scale a b s = do
ε <- asks cfgPrecision
let σ = case scale of
Lin -> (s - a) / (b - a)
Log -> (log s - log a) / (log b - log a)
x = approxRational σ ε
liftIO $ fprint ("≈ " % string % "\n") (showRatio x)
parseArgs :: String -> [String]
parseArgs str = words str ++ repeat ""
-- | Parse a number defaulting to given value.
parseNum :: (Num a, Read a)
=> String -- ^ String to parse
-> a -- ^ Default value
-> a -- ^ Result
parseNum s x = fromMaybe x $ fst <$> listToMaybe (reads s)
-- | Generate file name from given base name and extension.
output :: (MonadIO m, MonadThrow m, MonadState AlgaSt m)
=> FilePath -- ^ Given file name
-> String -- ^ Extension
-> m (Path Abs File) -- ^ Absolute path to output file
output given' ext = do
given <- resolveFile' given'
actual <- fromAbsFile <$> gets stSrcFile
a <- parseAbsFile $
if null ext then actual else FP.replaceExtension actual ext
return (if null given' then a else given)
-- | Change current file name.
setFileName :: MonadState AlgaSt m => Path Abs File -> m ()
setFileName fpath = modify $ \st -> st { stSrcFile = fpath }
-- | Drop command prefix if it's present.
dropCmdPrefix :: String -> String
dropCmdPrefix arg
| cmdPrefix `isPrefixOf` arg = drop (length cmdPrefix) arg
| otherwise = arg
-- | All REPL commands are prefixed with this.
cmdPrefix :: String
cmdPrefix = ":"
-- | Print out an exception.
spitExc :: MonadIO m => SomeException -> m ()
spitExc = liftIO . fprint ("× " % string % "\n") . show
-- | Stupid trimming for strings.
trim :: String -> String
trim = let f = reverse . dropWhile isSpace in f . f