mida-0.4.5: src/Mida/Interaction/Commands.hs
-- -*- Mode: Haskell; -*-
--
-- This module describes all supported MIDA commands. It also provides all
-- the functionality to load source files and generate / save MIDI files.
--
-- Copyright © 2014, 2015 Mark Karpov
--
-- MIDA 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.
--
-- MIDA 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/>.
module Mida.Interaction.Commands
( processCmd
, completionFunc
, cmdLoad
, cmdMake
, cmdPrefix )
where
import Control.Exception (SomeException, try)
import Control.Monad (void)
import Control.Monad.IO.Class
import Data.Char (isSpace)
import Data.Foldable (find)
import Data.List (elemIndex, isPrefixOf)
import Data.Maybe (fromMaybe, listToMaybe)
import System.Directory
( canonicalizePath
, doesDirectoryExist
, doesFileExist
, getCurrentDirectory
, getHomeDirectory
, getTemporaryDirectory
, setCurrentDirectory )
import System.Exit (exitSuccess)
import System.FilePath
( addTrailingPathSeparator
, joinPath
, replaceExtension
, splitDirectories
, takeFileName
, (</>) )
import System.Process
( shell
, createProcess
, waitForProcess
, delegate_ctlc )
import qualified Data.Text.Lazy as T
import qualified Data.Text.Lazy.IO as T
import qualified Codec.Midi as Midi
import qualified Data.Text.Format as F
import qualified System.Console.Haskeline as L
import Mida.Interaction.Base
import Mida.Language
import Mida.Midi
import Mida.Representation
data Cmd = Cmd
{ cmdName :: String
, cmdFunc :: String -> MidaIO ()
, cmdDesc :: T.Text
, cmdComp :: CompletionScheme }
data CompletionScheme = None | Files | Names deriving (Eq, Show)
commands :: [Cmd]
commands =
[ 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 "load" cmdLoad' "Load definitions from given file" Files
, Cmd "make" cmdMake' "Generate and save MIDI file" Files
, Cmd "prog" cmdProg "Set program for preview" None
, Cmd "prv" cmdPrv "Play the score with external program" None
, 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 "save" cmdSave "Save current environment in file" Files
, Cmd "tempo" cmdTempo "Set tempo for preview" None
, Cmd "udef" cmdUdef "Remove definition of given symbol" Names ]
processCmd :: T.Text -> MidaIO ()
processCmd txt =
case find g commands of
Just Cmd { cmdFunc = f } -> f . T.unpack . T.strip $ args
Nothing -> liftIO $ F.print "Unknown command, try {}help.\n"
(F.Only cmdPrefix)
where g Cmd { cmdName = c } = c == dropCmdPrefix (T.unpack cmd)
(cmd, args) = T.break isSpace (T.strip txt)
completionFunc :: L.CompletionFunc MidaIO
completionFunc = L.completeWordWithPrev Nothing " " getCompletions
getCompletions :: String -> String -> MidaIO [L.Completion]
getCompletions prev word = do
names <- liftEnv getRefs
files <- L.listFiles word
let cmds = (cmdPrefix ++) . cmdName <$> commands
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
where f = fmap L.simpleCompletion . filter (word `isPrefixOf`)
cmdCd :: String -> MidaIO ()
cmdCd path = liftIO $ do
new <- addTrailingPathSeparator . (</> path) <$> getCurrentDirectory
present <- doesDirectoryExist new
if present
then do corrected <- canonicalizePath new
setCurrentDirectory corrected
F.print "Changed to \"{}\".\n" (F.Only corrected)
else F.print "Cannot cd to \"{}\".\n" (F.Only new)
cmdClear :: String -> MidaIO ()
cmdClear _ = liftEnv clearDefs >> liftIO (T.putStrLn "Environment cleared.")
cmdDef :: String -> MidaIO ()
cmdDef arg = mapM_ f (words arg)
where f name = liftEnv (getSrc name) >>= liftIO . T.putStr
cmdHelp :: String -> MidaIO ()
cmdHelp _ = liftIO (T.putStrLn "Available commands:") >> mapM_ f commands
where f Cmd { cmdName = c, cmdDesc = d } =
liftIO $ F.print " {}{}{}\n" (cmdPrefix, F.right 24 ' ' c, d)
cmdLoad' :: String -> MidaIO ()
cmdLoad' = cmdLoad . words
cmdLoad :: [String] -> MidaIO()
cmdLoad = mapM_ loadOne
loadOne :: String -> MidaIO ()
loadOne given = do
file <- output given ""
b <- liftIO $ doesFileExist file
if b
then do contents <- liftIO $ T.readFile file
case parseMida (takeFileName file) contents of
Right x -> do mapM_ f x
setFileName file
liftIO $ F.print "\"{}\" loaded successfully.\n"
(F.Only file)
Left x -> liftIO $ F.print "Parse error in {}.\n" (F.Only x)
else liftIO $ F.print "Could not find \"{}\".\n" (F.Only file)
where f (Definition n t) = processDef n t
f (Exposition _) = return ()
cmdMake' :: String -> MidaIO ()
cmdMake' arg =
let (s:q:b:f:_) = words arg ++ repeat ""
in cmdMake (parseNum s dfltSeed)
(parseNum q dfltQuarter)
(parseNum b dfltBeats)
f
cmdMake :: Int -> Int -> Int -> String -> MidaIO ()
cmdMake s q b f = do
file <- output f "mid"
midi <- liftEnv $ genMidi s q b
result <- liftIO $ try (Midi.exportFile file midi)
case result of
Right _ -> liftIO $ F.print "MIDI file saved as \"{}\".\n" (F.Only file)
Left e -> spitExc e
cmdProg :: String -> MidaIO ()
cmdProg arg = do
prog <- getProg
setProg $ parseNum (trim arg) prog
cmdPrv :: String -> MidaIO ()
cmdPrv arg = do
prvcmd <- getPrvCmd
progOp <- getProgOp
prog <- show <$> getProg
tempoOp <- getTempoOp
tempo <- show <$> getTempo
temp <- liftIO getTemporaryDirectory
f <- output "" "mid"
let (s:q:b:_) = words arg ++ repeat ""
file = temp </> takeFileName f
cmd = unwords [prvcmd, progOp, prog, tempoOp, tempo, file]
cmdMake (parseNum s dfltSeed)
(parseNum q dfltQuarter)
(parseNum b dfltBeats)
file
(_, _, _, ph) <- liftIO $ createProcess (shell cmd) { delegate_ctlc = True }
liftIO . void $ waitForProcess ph
cmdLength :: String -> MidaIO ()
cmdLength x = getPrevLen >>= setPrevLen . parseNum x
cmdPurge :: String -> MidaIO ()
cmdPurge _ = do
liftEnv $ purgeEnv topDefs
liftIO $ T.putStrLn "Environment purged."
cmdPwd :: String -> MidaIO ()
cmdPwd _ = liftIO (getCurrentDirectory >>= putStrLn)
cmdQuit :: String -> MidaIO ()
cmdQuit _ = liftIO exitSuccess
cmdSave :: String -> MidaIO ()
cmdSave given = do
file <- output given ""
src <- liftEnv fullSrc
result <- liftIO (try (T.writeFile file src) :: IO (Either SomeException ()))
case result of
Right _ -> setFileName file >>
liftIO (F.print "Environment saved as \"{}\".\n" (F.Only file))
Left e -> spitExc e
cmdTempo :: String -> MidaIO ()
cmdTempo arg = do
tempo <- getTempo
setTempo $ parseNum (trim arg) tempo
cmdUdef :: String -> MidaIO ()
cmdUdef arg = mapM_ f (words arg)
where f name = do
liftEnv (remDef name)
liftIO (F.print "Definition for '{}' removed.\n" (F.Only name))
parseNum :: (Num a, Read a) => String -> a -> a
parseNum s x = fromMaybe x $ fst <$> listToMaybe (reads s)
output :: String -> String -> MidaIO String
output given ext = do
actual <- getSrcFile
home <- liftIO getHomeDirectory
let a = if null ext then actual else replaceExtension actual ext
g = joinPath . fmap f . splitDirectories $ given
f x = if x == "~" then home else x
return $ if null given then a else g
setFileName :: FilePath -> MidaIO ()
setFileName path = (</> path) <$> liftIO getCurrentDirectory >>= setSrcFile
dropCmdPrefix :: String -> String
dropCmdPrefix arg
| cmdPrefix `isPrefixOf` arg = drop (length cmdPrefix) arg
| otherwise = arg
cmdPrefix :: String
cmdPrefix = ":"
spitExc :: SomeException -> MidaIO ()
spitExc = liftIO . F.print "× {}.\n" . F.Only . show
trim :: String -> String
trim = f . f
where f = reverse . dropWhile isSpace