{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} module Main where import Control.Lens hiding (Empty, imap) import Control.Applicative import Control.Concurrent import Control.Monad import Data.Char import Data.IORef import Data.List hiding (delete) import Numeric (showIntAtBase) import System.IO --import System.Posix.Signals import GHC.Stats (getRTSStats) 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 Hack.Buffer import Process import Scanner import Blessings import qualified Blessings.Internal as Blessings import Blessings.String () data Mode = InsertMode | NormalMode | VerbatimMode | SelectRegisterMode | DeleteMode | YankMode deriving (Eq) instance Show Mode where show NormalMode = "normal" show InsertMode = "insert" show VerbatimMode = "verbatim" show SelectRegisterMode = "select register" show DeleteMode = "delete" show YankMode = "yank" data VTConfig = VTConfig { withOutput :: IO () -> IO () } data VTState = VTState { _buffer :: Buffer , _mode :: Mode , _processCount :: Int , _count :: Maybe Int , _register :: Char , _registers :: Map Char String } instance Show VTState where show VTState{..} = "" 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 s <- scan stdin --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 ScanKey k = s let cmd = getCommand (_mode q0) k --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 | DebugShowVTState | InsertString String | ExecuteInputBuffer | MoveCursor Motion | MoveCursorLeftIfAtEndOfLine | MoveCursorWarn Motion | ChangeMode Mode -- TODO Move Count Motion -- Delete Count Register Motion -- etc. | Combine Command Command | Nop | RingBell | AppendCount Int | SetCount (Maybe Int) | SetRegister Char | Delete Motion | DeleteEntireLine | Yank Motion instance Semigroup Command where (<>) = Combine instance Monoid Command where mempty = Nop 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 DebugShowVTState = get >>= tell . (:[]) . pp . SGR [35] . Plain . show execCommand (MoveCursor 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 merge with mode constraints in MoveCursor execCommand MoveCursorLeftIfAtEndOfLine = do whenM (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 (MoveCursorWarn x) = do b0 <- use buffer execCommand (MoveCursor 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 getRTSStats 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 (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) -- TODO yank into "- (smallDeleteRegister) when deleting less than one line -- TODO reset register after this command (q & register .~ defaultRegister) execCommand (Delete x) = do b0 <- use buffer c <- uses count (maybe 1 id) buffer %= delete x c b1 <- use buffer when (b0 == b1) $ throwError (OtherError "nothing to delete") -- TODO Yank register motion (after motion has incorporated count) execCommand (Yank x) = modify $ \q@VTState{..} -> do let c = maybe 1 id _count y = select x c _buffer q & registers %~ (at _register .~ Just y) -- 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 :: Blessings String -> IO () renderLeft = putStr . pp renderRight :: Blessings String -> IO () renderRight a = do saveCursor moveCursorRight 1024 -- XXX obviously, this is a hack..^_^ moveCursorLeft $ Blessings.length a - 1 renderLeft a unsaveCursor promptString :: Mode -> Blessings String promptString NormalMode = SGR [33,1] "@ " promptString InsertMode = "> " promptString SelectRegisterMode = "\" " promptString DeleteMode = SGR [31,1] "> " promptString VerbatimMode = SGR [34,1] "^ " promptString YankMode = SGR [31,1] "y " 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 :: Pm -> (Char -> Bool) -> String -> Blessings String gaudySpans c p = mconcat . map (either (SGR c . Plain . lit) Plain) . spans p gaudySpecial :: Pm -> String -> Blessings String gaudySpecial c = gaudySpans c (not . isPrint) lit :: String -> String lit = (>>= f) where f '\ESC' = "^[" f c = showLitChar c "" clearLine :: IO () clearLine = putStr "\ESC[2K" >> moveCursorLeft 1024 ringBell :: IO () ringBell = putStr "\x07" -- BEL '\a' saveCursor :: IO () saveCursor = putStr "\ESC[s" unsaveCursor :: IO () unsaveCursor = putStr "\ESC[u" moveCursorLeft :: Int -> IO () moveCursorLeft 0 = return () moveCursorLeft i = putStr $ "\ESC[" ++ show i ++ "D" moveCursorRight :: Int -> IO () moveCursorRight 0 = return () moveCursorRight i = putStr $ "\ESC[" ++ show i ++ "C" -- TODO? charToCode c = "\\x" ++ showHex (ord c) charToCode :: Char -> String charToCode c = "\\x" ++ showIntAtBase 16 intToDigit (ord c) "" dmap :: Keymap dmap = [ ("\ESC", ChangeMode NormalMode <> SetCount Nothing) , ("\ESC[24~", DebugShowVTState) , ("d", DeleteEntireLine <> ChangeMode NormalMode <> SetCount Nothing) , ("$", Yank ToEndOfLine <> Delete ToEndOfLine <> ChangeMode NormalMode <> SetCount Nothing <> MoveCursorLeftIfAtEndOfLine ) , ("0", Yank ToStartOfLine <> Delete ToStartOfLine <> ChangeMode NormalMode <> SetCount Nothing) , ("h", Yank CharsBackward <> Delete CharsBackward <> ChangeMode NormalMode <> SetCount Nothing) , ("l", Yank CharsForward <> Delete CharsForward <> ChangeMode NormalMode <> SetCount Nothing <> MoveCursorLeftIfAtEndOfLine ) ] selectRegisterMap :: Keymap selectRegisterMap = [ ("\ESC", ChangeMode NormalMode) ] ++ (map (\c -> ([c], SetRegister c <> ChangeMode NormalMode)) (['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9'] ++ ".%#:-\"")) -- TODO maybe in normal mode reset count (SetCount Nothing) after each -- command doesn't alter the count. How would this work together with -- ChangeMode DeleteMode -- TODO 2017-08-06 -- initialize count whenever nmap is entered -- ditch SetCount Nothing -- pass count to commands / modes nmap :: Keymap nmap = [ ("\ESC", SetCount Nothing) -- ^TODO RingBell if count is already Nothing -- TODO cancel any unfinished commands , ("i", ChangeMode InsertMode <> SetCount Nothing) , ("a", ChangeMode InsertMode <> SetCount Nothing <> MoveCursor CharsForward) , ("I", ChangeMode InsertMode <> MoveCursor ToStartOfLine) , ("A", ChangeMode InsertMode <> MoveCursor ToEndOfLine) , ("|", MoveCursorWarn ToColumn <> SetCount Nothing) , ("$", MoveCursorWarn ToEndOfLine <> SetCount Nothing) , ("h", MoveCursorWarn CharsBackward <> SetCount Nothing) , ("l", MoveCursorWarn CharsForward <> SetCount Nothing) , ("b", MoveCursorWarn WordsBackward <> SetCount Nothing) , ("w", MoveCursorWarn WordsForward <> SetCount Nothing) , ("d", ChangeMode DeleteMode) , ("y", ChangeMode YankMode) , ("\"", ChangeMode SelectRegisterMode <> SetCount Nothing) , ("\ESC[24~", DebugShowVTState) , ("\ESC[C", MoveCursorWarn CharsForward <> SetCount Nothing) , ("\ESC[D", MoveCursorWarn CharsBackward <> SetCount Nothing) , ("\n", 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 (MoveCursor ToStartOfLine) (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 = [ ("\ESC", ChangeMode NormalMode <> MoveCursor CharsBackward) , ("\x01", MoveCursorWarn ToStartOfLine) , ("\x05", MoveCursorWarn ToEndOfLine) , ("\ESC[24~", DebugShowVTState) , ("\ESC[3~", Delete CharsForward) , ("\ESC[C", MoveCursorWarn CharsForward) , ("\ESC[D", MoveCursorWarn CharsBackward) , ("\x16", ChangeMode VerbatimMode) -- ^V , ("\x17", Delete WordsBackward) -- ^W , ("\x0a", ExecuteInputBuffer) , ("\x7f", Delete CharsBackward) -- Delete , ("\x08", Delete CharsBackward) -- BackSpace , ("\ESCOc", MoveCursorWarn WordsForward) , ("\ESCOd", MoveCursorWarn WordsBackward) ] ymap :: Keymap ymap = [ ("\ESC", ChangeMode NormalMode <> SetCount Nothing) , ("\ESC[24~", DebugShowVTState) -- TODO , ("y", DeleteEntireLine <> ChangeMode NormalMode <> SetCount Nothing) , ("$", Yank ToEndOfLine <> ChangeMode NormalMode <> SetCount Nothing) , ("0", Yank ToStartOfLine <> ChangeMode NormalMode <> SetCount Nothing) , ("h", Yank CharsBackward <> ChangeMode NormalMode <> SetCount Nothing) , ("l", Yank CharsForward <> ChangeMode NormalMode <> SetCount Nothing) ] type Keymap = [(String, Command)] 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 getCommand YankMode s = maybe (AlertBadInput s) id $ lookup s ymap -- 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