{-# LANGUAGE GeneralizedNewtypeDeriving #-} module Main where import Control.Applicative import Control.Concurrent import Control.Monad import Data.Char import Data.IORef import Data.List import Numeric (showIntAtBase) import System.IO --import System.Posix.Signals import GHC.Stats (getGCStats) import Control.Monad.Error import Control.Monad.Reader import Control.Monad.State import Control.Monad.Writer import Data.Map (Map) import qualified Data.Map as Map import Buffer import Process import Scanner (scan, runScanner, toChar) data VTConfig = VTConfig { withOutput :: IO () -> IO () } data VTState = VTState { buffer :: Buffer , mode :: Mode , processCount :: Int , count :: Maybe Int , register :: Char , registers :: Map Char String } defaultRegister :: Char defaultRegister = '"' main :: IO () main = do hSetEcho stdin False hSetBuffering stdin NoBuffering -- WINCH -- TODO installHandler 28 (Catch $ ioctl 0 ...) Nothing let st = VTState { mode = InsertMode , buffer = ("!while date; do sleep 1; done", "") , processCount = 0 , count = Nothing , register = defaultRegister , registers = Map.empty } lockRef <- newMVar () qRef <- newIORef st let _putState = writeIORef qRef -- TODO atomicModifyIORef (?) _getState = readIORef qRef _withOutput a = do q <- _getState withMVar lockRef $ \ _ -> do clearLine a renderInputLine (count q) (mode q) (buffer q) hFlush stdout let cf = VTConfig { withOutput = _withOutput } -- render initial input line _withOutput $ return () uiThread cf _putState _getState uiThread :: VTConfig -> (VTState -> IO ()) -> IO VTState -> IO () uiThread cf putState getState = forever $ do q0 <- getState _ <- hLookAhead stdin -- wait for input --t0 <- getCurrentTime -- ((res, s), _) <- runScanner scan ((_, s), _) <- runScanner scan --t1 <- getCurrentTime --putStrLn $ "====> \ESC[32;1m" ++ show s ++ "\ESC[m in " ++ -- (show $ diffUTCTime t1 t0) -- ++ ": \"\ESC[35m" ++ (s >>= colorize . toChar) -- ++ "\ESC[m\"" --case res of -- Left msg -> putStrLn $ " error: " ++ msg -- Right _ -> return () -- TODO don't leak C let cmd = getCommand (mode q0) (map toChar s) --withOutput cf $ do -- putStrLn $ show cmd ((eitCmd, lns), q1) <- runVT cf q0 (execCommand cmd) -- TODO only putState if it has changed (?) putState q1 withOutput cf $ do forM_ lns putStrLn whenLeft eitCmd $ \err -> ringBell >> putStrLn (prettyError err) --when (mode st /= mode st') $ do -- putStrLn $ "change mode: " ++ (show $ mode st') data Command = AlertBadInput String | InsertString String | KillLastWord | KillLastChar | KillNextChar | ExecuteInputBuffer | MotionCommand LeftRightMotion | MotionCommandWarn LeftRightMotion | ChangeMode Mode -- TODO Move Count Motion -- Delete Count Register Motion -- etc. | Combine Command Command | Nop | RingBell | AppendCount Int | SetCount (Maybe Int) | SetRegister Char | DeleteEntireLine instance Monoid Command where mempty = Nop mappend = Combine data ExecError = UnhandledInputError String | OtherError String instance Error ExecError where noMsg = OtherError "something went wrong" prettyError :: ExecError -> String prettyError e = rec e where color cc s = "\x1b[" ++ cc ++ "m" ++ s ++ "\x1b[m" rec (UnhandledInputError s) = color "31" $ "unhandled input: <" ++ (s >>= pp "31;1") ++ "\x1b[;31m>" rec (OtherError s) = color "31" $ "error: " ++ s -- TODO cc is ColorCode pp cc c | isPrint c = [c] | otherwise = specialChar cc $ case c of '\x1b' -> "^[" _ -> charToCode c specialChar cc s = "\x1b[1;35m" ++ s ++ "\x1b[;" ++ cc ++ "m" modifyBuffer :: (Buffer -> Buffer) -> VT () modifyBuffer f = modify $ \st -> st { buffer = f (buffer st) } newtype VT a = VT (ReaderT VTConfig (ErrorT ExecError (WriterT [String] (StateT VTState IO ))) a) deriving ( Applicative , Functor , Monad , MonadError ExecError , MonadIO , MonadReader VTConfig , MonadState VTState , MonadWriter [String] ) runVT :: VTConfig -> VTState -> VT a -> IO ((Either ExecError a, [String]), VTState) runVT cf st (VT a) = runStateT (runWriterT (runErrorT (runReaderT a cf))) st insertString :: String -> Buffer -> Buffer insertString s (ls, rs) = (ls ++ s, rs) execCommand :: Command -> VT () execCommand (MotionCommand x) = do c <- gets count modifyBuffer (move x $ maybe 1 id c) -- TODO apply mode constraints somewhere else q <- get when (mode q == NormalMode) $ when (null $ snd $ buffer q) $ modifyBuffer (gotoLeft 1) execCommand (MotionCommandWarn x) = do b0 <- gets buffer execCommand (MotionCommand x) b1 <- gets buffer -- TODO make this a warning or else ... when (b0 == b1) $ throwError (OtherError $ "your motion has no effect: " ++ show x) execCommand (ChangeMode m) = modify $ \ q -> q { mode = m } execCommand (InsertString s) = modifyBuffer (insertString s) execCommand ExecuteInputBuffer = do ---- XXX hack to replace empty command line --gets (null . showBuffer . buffer) >>= flip when -- (modify $ \q -> q { buffer = ("!","") }) st <- get case showBuffer (buffer st) of ":r" -> do tell [ "--- Registers ---" ] tell $ map (\(r, s) -> ['"', r] ++ " " ++ s) -- TODO pp $ Map.toList (registers st) ":s" -> do s <- liftIO getGCStats tell [ show s ] '!' : cmdline -> do --tell [ "spawn: " ++ cmdline ] -- "input: <" ++ (showBuffer b >>= reform 32) ++ ">" ] -- TODO register process i <- state $ \ q -> let i = processCount q + 1 in (i, q { processCount = i }) cf <- ask liftIO $ forkIO $ spawn i (withOutput cf) cmdline return () "" -> do liftIO ringBell s -> do tell [ "input: <" ++ (s >>= reform 32) ++ ">" ] modifyBuffer (const emptyBuffer) execCommand KillNextChar = do get >>= flip (when . null . snd . buffer) (throwError $ OtherError "nothing to kill right") modifyBuffer $ \(lhs, _:rhs') -> (lhs, rhs') execCommand KillLastChar = do get >>= flip (when . null . fst . buffer) (throwError $ OtherError "nothing to kill left") modifyBuffer $ \(lhs, rhs) -> (init lhs, rhs) execCommand KillLastWord = do get >>= flip (when . null . fst . buffer) (throwError $ OtherError "nothing to kill left") modifyBuffer $ \(lhs, rhs) -> (foldr dropWhileEnd lhs [not . isSpace, isSpace], rhs) execCommand (AlertBadInput s) = throwError (UnhandledInputError s) execCommand (Combine c1 c2) = do cf <- ask q0 <- get ((eSt1, lines1), q1) <- liftIO $ runVT cf q0 (execCommand c1) -- TODO "stack trace" whenLeft eSt1 throwError ((eSt2, lines2), q2) <- liftIO $ runVT cf q1 (execCommand c2) -- TODO "stack trace" whenLeft eSt2 throwError tell lines1 tell lines2 put q2 execCommand Nop = return () execCommand RingBell = liftIO ringBell execCommand (AppendCount i) = modify $ \q -> q { count = f (count q) } where f Nothing = Just i f (Just c) = Just (c * 10 + i) execCommand (SetCount i) = modify $ \q -> q { count = i } execCommand (SetRegister c) = modify $ \q -> q { register = c } execCommand DeleteEntireLine = modify $ \q -> -- TODO Numbered registers "0 to "9 -- Small delete register "- let s = showBuffer $ buffer q r = register q in q { registers = Map.insert r s $ Map.insert defaultRegister s $ registers q , buffer = emptyBuffer , register = defaultRegister } reform :: Int -> Char -> String reform colorCode c = if isPrint c then normal colorCode [c] else special colorCode $ case ord c of 27 -> "^[" _ -> charToCode c normal :: Int -> String -> String normal colorCode s = "\x1b[" ++ show colorCode ++ "m" ++ s ++ "\x1b[m" special :: Int -> String -> String special colorCode s = "\x1b[1;" ++ show colorCode ++ "m" ++ s ++ "\x1b[m" -- XXX assumes that the cursor is already at the (cleared) input line -- TODO renderInputLine looks like it wants to be -> VT () renderInputLine :: Maybe Int -> Mode -> Buffer -> IO () renderInputLine mb_cnt m (lhs, rhs) = do clearLine -- TODO this is required for drawing the mode on the right side saveCursor moveCursorRight 1024 let (infoLen, info) = case mb_cnt of Nothing -> let gaudy = "\x1b[1;30m" ++ show m ++ "\x1b[m" plain = show m in (length plain, gaudy) Just cnt -> let gaudy = "\x1b[1;30m" ++ show m ++ "[\x1b[33m" ++ show cnt ++ "\x1b[30m]\x1b[m" plain = show m ++ "[" ++ show cnt ++ "]" in (length plain, gaudy) moveCursorLeft $ infoLen - 1 putStr info unsaveCursor let promptString = case m of NormalMode -> "\x1b[33;1m@\x1b[m " InsertMode -> "> " VerbatimMode -> "\x1b[34;1m^\x1b[m " SelectRegisterMode -> "\" " DeleteMode -> "\x1b[31;1m>\x1b[m " putStr $ promptString ++ (lhs >>= reform') ++ (rhs >>= reform') moveCursorLeft (length $ rhs >>= reformVis) where -- TODO unify reform and reform' reform' c = if isPrint c then [c] else "\x1b[35m" ++ ( case ord c of 27 -> "^[" _ -> charToCode c ) ++ "\x1b[m" reformVis c = if isPrint c then [c] else case ord c of 27 -> "^[" _ -> charToCode c clearLine :: IO () clearLine = putStr "\x1b[2K" >> moveCursorLeft 1024 ringBell :: IO () ringBell = putStr "\x07" -- BEL '\a' saveCursor :: IO () saveCursor = putStr "\x1b[s" unsaveCursor :: IO () unsaveCursor = putStr "\x1b[u" moveCursorLeft :: Int -> IO () moveCursorLeft 0 = return () moveCursorLeft i = putStr $ "\x1b[" ++ show i ++ "D" moveCursorRight :: Int -> IO () moveCursorRight 0 = return () moveCursorRight i = putStr $ "\x1b[" ++ show i ++ "C" -- TODO? charToCode c = "\\x" ++ showHex (ord c) charToCode :: Char -> String charToCode c = "\\x" ++ showIntAtBase 16 intToDigit (ord c) "" dmap :: Keymap dmap = [ ("\x1b", ChangeMode NormalMode) , ("d", DeleteEntireLine <> ChangeMode NormalMode) ] selectRegisterMap :: Keymap selectRegisterMap = [ ("\x1b", ChangeMode NormalMode) ] ++ (map (\c -> ([c], SetRegister c <> ChangeMode NormalMode)) (['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9'] ++ ".%#:-\"")) nmap :: Keymap nmap = [ ("\x1b", SetCount Nothing) -- ^TODO RingBell if count is already Nothing -- TODO cancel any unfinished commands , ("i", ChangeMode InsertMode <> SetCount Nothing) , ("a", ChangeMode InsertMode <> SetCount Nothing <> MotionCommand GotoRight) , ("I", ChangeMode InsertMode <> MotionCommand GotoFirstChar) , ("A", ChangeMode InsertMode <> MotionCommand GotoEndOfLine) , ("|", MotionCommand GotoColumn <> SetCount Nothing) , ("$", MotionCommand GotoEndOfLine <> SetCount Nothing) , ("h", MotionCommand GotoLeft <> SetCount Nothing) , ("l", MotionCommand GotoRight <> SetCount Nothing) , ("b", MotionCommand WordsBackward <> SetCount Nothing) , ("w", MotionCommand WordsForward <> SetCount Nothing) , ("d", ChangeMode DeleteMode <> SetCount Nothing) , ("\"", ChangeMode SelectRegisterMode <> SetCount Nothing) , ("\x1b[C", MotionCommand GotoRight <> SetCount Nothing) , ("\x1b[D", MotionCommand GotoLeft <> SetCount Nothing) , ("\x0a", ExecuteInputBuffer <> ChangeMode InsertMode <> SetCount Nothing) ] ++ (map (\i -> (show i, AppendCount i)) [0..9]) -- XXX -- if we would want 0 to move the cursor to the first character of the -- line, then we would need ("0", x) -- where -- x :: Command -- x = Embed f -- f :: VT Command -- f = gets (isJust . count) >>= -- return . bool (MotionCommand GotoFirstChar) (AppendCount 0) -- bool :: a -> a -> Bool -> a -- bool _ a True = a -- bool a _ False = a -- and also we would have to extend data Command by Embed (VT Command) -- execCommand (Embed a) = a >>= execCommand -- -- This all looks quite strange, so just use | if you want that movement... -- ^_^ imap :: Keymap imap = [ ("\x1b", ChangeMode NormalMode <> MotionCommand GotoLeft) , ("\x01", MotionCommand GotoFirstChar) , ("\x05", MotionCommand GotoEndOfLine) , ("\x1b[3~", KillNextChar) , ("\x1b[C", MotionCommand GotoRight) , ("\x1b[D", MotionCommand GotoLeft) , ("\x16", ChangeMode VerbatimMode) -- ^V , ("\x17", KillLastWord) -- ^W , ("\x0a", ExecuteInputBuffer) , ("\x7f", KillLastChar) -- Delete , ("\x08", KillLastChar) -- BackSpace , ("\x1bOc", MotionCommand WordsForward) , ("\x1bOd", MotionCommand WordsBackward) ] type Keymap = [(String, Command)] data Mode = InsertMode | NormalMode | VerbatimMode | SelectRegisterMode | DeleteMode deriving (Eq) instance Show Mode where show NormalMode = "normal" show InsertMode = "insert" show VerbatimMode = "verbatim" show SelectRegisterMode = "select register" show DeleteMode = "delete" getCommand :: Mode -> String -> Command getCommand InsertMode s = maybe (InsertString s) id $ lookup s imap getCommand NormalMode s = maybe (AlertBadInput s <> SetCount Nothing) id $ lookup s nmap getCommand VerbatimMode s = InsertString s <> ChangeMode InsertMode getCommand SelectRegisterMode s = maybe (AlertBadInput s) id $ lookup s selectRegisterMap -- ^ TODO clear bad input getCommand DeleteMode s = maybe (AlertBadInput s) id $ lookup s dmap -- TODO Control.Monad.whenLeft whenLeft :: Monad m => Either a b -> (a -> m ()) -> m () whenLeft (Left x) f = f x whenLeft _ _ = return ()