{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} module Main where import Control.Lens hiding (imap) 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) import Trammel data Mode = InsertMode | NormalMode | VerbatimMode | SelectRegisterMode | DeleteMode deriving (Eq) data VTConfig = VTConfig { withOutput :: IO () -> IO () } data VTState = VTState { _buffer :: Buffer , _mode :: Mode , _processCount :: Int , _count :: Maybe Int , _register :: Char , _registers :: Map Char String } makeLenses ''VTState 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 (UnhandledInputError s) = pp $ SGR [31] $ "unhandled input: <" <> SGR [1] (gaudySpecial [35,1] s) <> ">" prettyError (OtherError s) = pp $ SGR [31] $ gaudySpecial [35] s 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 <- uses count (maybe 1 id) buffer %= move x c -- TODO apply mode constraints somewhere else whenM (uses mode (==NormalMode) >>&& uses (buffer . _2) null) $ buffer %= gotoLeft 1 -- TODO Make this "real" warnings, i.e. don't throwError but tell. This -- is required in order to perform any Combine-d commands regardless of -- failed moves. Currently this is only used to SetCount Nothing (which -- is defunct atm) Alternatively we could simply reset the state when an -- error happens Discus! execCommand (MotionCommandWarn x) = do b0 <- use buffer execCommand (MotionCommand x) b1 <- use buffer -- TODO make this a warning or else ... when (b0 == b1) $ throwError (OtherError $ "your motion has no effect: " ++ show x) execCommand (ChangeMode m) = mode .= m execCommand (InsertString s) = buffer %= 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 ":c" -> do let f i = pp $ SGR [38,5,i] $ Plain $ padl 3 '0' $ show i tell [ intercalate " " $ map f [0..255] ] ":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 <- processCount <<+= 1 cf <- ask liftIO $ forkIO $ spawn i (withOutput cf) cmdline return () "" -> do liftIO ringBell s -> do let s' = SGR [32] $ gaudySpecial [1] s tell [ pp $ "input: " <> s' , pp $ SGR [35] $ gaudySpecial [1] $ pp s' ] buffer .= emptyBuffer execCommand KillNextChar = do whenM (uses (buffer . _2) null) $ throwError (OtherError "nothing to kill right") buffer . _2 %= tail execCommand KillLastChar = do whenM (uses (buffer . _1) null) $ throwError (OtherError "nothing to kill left") buffer . _1 %= init execCommand KillLastWord = do whenM (uses (buffer . _1) null) $ throwError (OtherError "nothing to kill left") buffer . _1 %= foldr dropWhileEnd `flip` [not . isSpace, isSpace] 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) = count %= Just . (i+) . maybe 0 (10*) execCommand (SetCount i) = count .= i execCommand (SetRegister c) = register .= c execCommand DeleteEntireLine = -- TODO Numbered registers "0 to "9 -- Small delete _register "- modify $ \q -> do let v = Just $ showBuffer $ _buffer q r = _register q q & buffer .~ emptyBuffer & register .~ defaultRegister & registers %~ (at r .~ v) . (at defaultRegister .~ v) -- 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 renderRight $ SGR [30,1] $ Plain (show m) <> maybe Empty (("["<>) . (<>"]") . SGR [33,1] . Plain . show) mb_cnt renderLeft $ promptString m <> gaudySpecial [35] (lhs ++ rhs) moveCursorLeft $ length $ lit rhs renderLeft :: Trammel String -> IO () renderLeft = putStr . pp renderRight :: Trammel String -> IO () renderRight a = do saveCursor moveCursorRight 1024 -- XXX obviously, this is a hack..^_^ moveCursorLeft $ len a - 1 renderLeft a unsaveCursor promptString :: Mode -> Trammel String promptString NormalMode = SGR [33,1] "@ " promptString InsertMode = "> " promptString SelectRegisterMode = "\" " promptString DeleteMode = SGR [31,1] "> " promptString VerbatimMode = SGR [34,1] "^ " spans :: (a -> Bool) -> [a] -> [Either [a] [a]] spans p xs = f_r (span p_r xs) where p_r = not . p p_l = p f_r (as, bs) = Right as : if null bs then [] else f_l (span p_l bs) f_l (as, bs) = Left as : if null bs then [] else f_r (span p_r bs) gaudySpans :: [Int] -> (Char -> Bool) -> String -> Trammel String gaudySpans c p = mconcat . map (either (SGR c . Plain . lit) Plain) . spans p gaudySpecial :: [Int] -> String -> Trammel String gaudySpecial c = gaudySpans c (not . isPrint) lit :: String -> String lit = (>>= f) where f '\ESC' = "^[" f c = showLitChar 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) , ("|", MotionCommandWarn GotoColumn <> SetCount Nothing) , ("$", MotionCommandWarn GotoEndOfLine <> SetCount Nothing) , ("h", MotionCommandWarn GotoLeft <> SetCount Nothing) , ("l", MotionCommandWarn GotoRight <> SetCount Nothing) , ("b", MotionCommandWarn WordsBackward <> SetCount Nothing) , ("w", MotionCommandWarn WordsForward <> SetCount Nothing) , ("d", ChangeMode DeleteMode) , ("\"", ChangeMode SelectRegisterMode <> SetCount Nothing) , ("\x1b[C", MotionCommandWarn GotoRight <> SetCount Nothing) , ("\x1b[D", MotionCommandWarn 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", MotionCommandWarn GotoFirstChar) , ("\x05", MotionCommandWarn GotoEndOfLine) , ("\x1b[3~", KillNextChar) , ("\x1b[C", MotionCommandWarn GotoRight) , ("\x1b[D", MotionCommandWarn GotoLeft) , ("\x16", ChangeMode VerbatimMode) -- ^V , ("\x17", KillLastWord) -- ^W , ("\x0a", ExecuteInputBuffer) , ("\x7f", KillLastChar) -- Delete , ("\x08", KillLastChar) -- BackSpace , ("\x1bOc", MotionCommandWarn WordsForward) , ("\x1bOd", MotionCommandWarn WordsBackward) ] type Keymap = [(String, Command)] 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 () whenM :: Monad m => m Bool -> m () -> m () whenM a b = a >>= flip when b infixl 1 >>&& (>>&&) :: Monad m => m Bool -> m Bool -> m Bool a >>&& b = do ra <- a rb <- b return $ ra && rb padl :: Int -> a -> [a] -> [a] padl n c s | length s < n = padl n c (c : s) | otherwise = s