summaryrefslogtreecommitdiff
path: root/main
diff options
context:
space:
mode:
Diffstat (limited to 'main')
-rw-r--r--main/Main.hs37
-rw-r--r--main/Terminal.hs241
2 files changed, 260 insertions, 18 deletions
diff --git a/main/Main.hs b/main/Main.hs
index fa2b4c1..528b8c2 100644
--- a/main/Main.hs
+++ b/main/Main.hs
@@ -31,7 +31,6 @@ import Data.Typeable
import Network.Socket
import System.Console.GetOpt
-import System.Console.Haskeline
import System.Environment
import System.Exit
import System.IO
@@ -57,6 +56,7 @@ import Erebos.Storage
import Erebos.Storage.Merge
import Erebos.Sync
+import Terminal
import Test
import Version
@@ -243,22 +243,20 @@ main = do
exitFailure
-inputSettings :: Settings IO
-inputSettings = setComplete commandCompletion $ defaultSettings
-
interactiveLoop :: Storage -> Options -> IO ()
-interactiveLoop st opts = runInputT inputSettings $ do
+interactiveLoop st opts = withTerminal commandCompletion $ \term -> do
erebosHead <- liftIO $ loadLocalStateHead st
- outputStrLn $ T.unpack $ displayIdentity $ headLocalIdentity erebosHead
+ void $ printLine term $ T.unpack $ displayIdentity $ headLocalIdentity erebosHead
- tui <- haveTerminalUI
- extPrint <- getExternalPrint
+ let tui = hasTerminalUI term
+ let extPrint = void . printLine term
let extPrintLn str = do
let str' = case reverse str of ('\n':_) -> str
_ -> str ++ "\n";
extPrint $! str' -- evaluate str before calling extPrint to avoid blinking
- let getInputLinesTui eprompt = do
+ let getInputLinesTui :: Either CommandState String -> MaybeT IO String
+ getInputLinesTui eprompt = do
prompt <- case eprompt of
Left cstate -> do
pname <- case csContext cstate of
@@ -272,11 +270,14 @@ interactiveLoop st opts = runInputT inputSettings $ do
SelectedConversation conv -> return $ T.unpack $ conversationName conv
return $ pname ++ "> "
Right prompt -> return prompt
- Just input <- lift $ getInputLine prompt
- case reverse input of
- _ | all isSpace input -> getInputLinesTui eprompt
- '\\':rest -> (reverse ('\n':rest) ++) <$> getInputLinesTui (Right ">> ")
- _ -> return input
+ lift $ setPrompt term prompt
+ join $ lift $ getInputLine term $ \case
+ Just input@('/' : _) -> KeepPrompt $ return input
+ Just input -> ErasePrompt $ case reverse input of
+ _ | all isSpace input -> getInputLinesTui eprompt
+ '\\':rest -> (reverse ('\n':rest) ++) <$> getInputLinesTui (Right ">> ")
+ _ -> return input
+ Nothing -> KeepPrompt mzero
getInputCommandTui cstate = do
input <- getInputLinesTui cstate
@@ -289,7 +290,7 @@ interactiveLoop st opts = runInputT inputSettings $ do
return (cmd, line)
getInputLinesPipe = do
- lift (getInputLine "") >>= \case
+ join $ lift $ getInputLine term $ KeepPrompt . \case
Just input -> return input
Nothing -> liftIO $ forever $ threadDelay 100000000
@@ -350,12 +351,12 @@ interactiveLoop st opts = runInputT inputSettings $ do
when (Just shown /= op) $ extPrintLn $ "[" <> show idx <> "] PEER " <> updateType' <> " " <> shown
_ -> return ()
- let process :: CommandState -> MaybeT (InputT IO) CommandState
+ let process :: CommandState -> MaybeT IO CommandState
process cstate = do
(cmd, line) <- getInputCommand cstate
h <- liftIO (reloadHead $ csHead cstate) >>= \case
Just h -> return h
- Nothing -> do lift $ lift $ extPrintLn "current head deleted"
+ Nothing -> do lift $ extPrintLn "current head deleted"
mzero
res <- liftIO $ runExceptT $ flip execStateT cstate { csHead = h } $ runReaderT cmd CommandInput
{ ciServer = server
@@ -375,7 +376,7 @@ interactiveLoop st opts = runInputT inputSettings $ do
| csQuit cstate' -> mzero
| otherwise -> return cstate'
Left err -> do
- lift $ lift $ extPrintLn $ "Error: " ++ err
+ lift $ extPrintLn $ "Error: " ++ err
return cstate
let loop (Just cstate) = runMaybeT (process cstate) >>= loop
diff --git a/main/Terminal.hs b/main/Terminal.hs
new file mode 100644
index 0000000..d117640
--- /dev/null
+++ b/main/Terminal.hs
@@ -0,0 +1,241 @@
+module Terminal (
+ Terminal,
+ hasTerminalUI,
+ withTerminal,
+ setPrompt,
+ getInputLine,
+ InputHandling(..),
+
+ TerminalLine,
+ printLine,
+
+ CompletionFunc, Completion,
+ simpleCompletion,
+ completeWordWithPrev,
+) where
+
+import Control.Arrow
+import Control.Concurrent
+import Control.Concurrent.STM
+import Control.Exception
+import Control.Monad
+
+import Data.Char
+
+import System.IO
+import System.Console.ANSI
+
+
+data Terminal = Terminal
+ { termLock :: MVar ()
+ , termAnsi :: Bool
+ , termPrompt :: TVar String
+ , termShowPrompt :: TVar Bool
+ , termInput :: TVar ( String, String )
+ }
+
+data TerminalLine = TerminalLine
+ { tlTerminal :: Terminal
+ }
+
+data Input
+ = InputChar Char
+ | InputMoveRight
+ | InputMoveLeft
+ | InputMoveEnd
+ | InputMoveStart
+ | InputBackspace
+ | InputClear
+ | InputBackWord
+ | InputEnd
+ | InputEscape String
+ deriving (Eq, Ord, Show)
+
+
+data InputHandling a
+ = KeepPrompt a
+ | ErasePrompt a
+
+
+hasTerminalUI :: Terminal -> Bool
+hasTerminalUI = termAnsi
+
+initTerminal :: IO Terminal
+initTerminal = do
+ termLock <- newMVar ()
+ termAnsi <- hNowSupportsANSI stdout
+ termPrompt <- newTVarIO ""
+ termShowPrompt <- newTVarIO False
+ termInput <- newTVarIO ( "", "" )
+ return Terminal {..}
+
+bracketSet :: IO a -> (a -> IO b) -> a -> IO c -> IO c
+bracketSet get set val = bracket (get <* set val) set . const
+
+withTerminal :: CompletionFunc IO -> (Terminal -> IO a) -> IO a
+withTerminal _ act = do
+ term <- initTerminal
+
+ bracketSet (hGetEcho stdin) (hSetEcho stdin) False $
+ bracketSet (hGetBuffering stdin) (hSetBuffering stdin) NoBuffering $
+ bracketSet (hGetBuffering stdout) (hSetBuffering stdout) (BlockBuffering Nothing) $
+ act term
+
+
+termPutStr :: Terminal -> String -> IO ()
+termPutStr Terminal {..} str = do
+ withMVar termLock $ \_ -> do
+ putStr str
+ hFlush stdout
+
+
+getInput :: IO Input
+getInput = do
+ getChar >>= \case
+ '\ESC' -> do
+ esc <- readEsc
+ case parseEsc esc of
+ Just ( 'C' , [] ) -> return InputMoveRight
+ Just ( 'D' , [] ) -> return InputMoveLeft
+ _ -> return (InputEscape esc)
+ '\b' -> return InputBackspace
+ '\DEL' -> return InputBackspace
+ '\NAK' -> return InputClear
+ '\ETB' -> return InputBackWord
+ '\SOH' -> return InputMoveStart
+ '\ENQ' -> return InputMoveEnd
+ '\EOT' -> return InputEnd
+ c -> return (InputChar c)
+ where
+ readEsc = getChar >>= \case
+ c | c == '\ESC' || isAlpha c -> return [ c ]
+ | otherwise -> (c :) <$> readEsc
+
+ parseEsc = \case
+ '[' : c : [] -> do
+ Just ( c, [] )
+ _ -> Nothing
+
+
+getInputLine :: Terminal -> (Maybe String -> InputHandling a) -> IO a
+getInputLine term@Terminal {..} handleResult = do
+ withMVar termLock $ \_ -> do
+ prompt <- atomically $ do
+ writeTVar termShowPrompt True
+ readTVar termPrompt
+ putStr prompt
+ hFlush stdout
+ (handleResult <$> go) >>= \case
+ KeepPrompt x -> do
+ termPutStr term "\n"
+ return x
+ ErasePrompt x -> do
+ termPutStr term "\r\ESC[K"
+ return x
+ where
+ go = getInput >>= \case
+ InputChar '\n' -> do
+ atomically $ do
+ ( pre, post ) <- readTVar termInput
+ writeTVar termInput ( "", "" )
+ writeTVar termShowPrompt False
+ return $ Just $ pre ++ post
+
+ InputChar c | isPrint c -> withInput $ \case
+ ( _, post ) -> do
+ writeTVar termInput . first (++ [ c ]) =<< readTVar termInput
+ return $ c : (if null post then "" else "\ESC[s" <> post <> "\ESC[u")
+
+ InputChar _ -> go
+
+ InputMoveRight -> withInput $ \case
+ ( pre, c : post ) -> do
+ writeTVar termInput ( pre ++ [ c ], post )
+ return $ "\ESC[C"
+ _ -> return ""
+
+ InputMoveLeft -> withInput $ \case
+ ( pre@(_ : _), post ) -> do
+ writeTVar termInput ( init pre, last pre : post )
+ return $ "\ESC[D"
+ _ -> return ""
+
+ InputBackspace -> withInput $ \case
+ ( pre@(_ : _), post ) -> do
+ writeTVar termInput ( init pre, post )
+ return $ "\b\ESC[K" <> (if null post then "" else "\ESC[s" <> post <> "\ESC[u")
+ _ -> return ""
+
+ InputClear -> withInput $ \_ -> do
+ writeTVar termInput ( "", "" )
+ ("\r\ESC[K" <>) <$> getCurrentPromptLine term
+
+ InputBackWord -> withInput $ \( pre, post ) -> do
+ let pre' = reverse $ dropWhile (not . isSpace) $ dropWhile isSpace $ reverse pre
+ writeTVar termInput ( pre', post )
+ ("\r\ESC[K" <>) <$> getCurrentPromptLine term
+
+ InputMoveStart -> withInput $ \( pre, post ) -> do
+ writeTVar termInput ( "", pre <> post )
+ return $ "\ESC[" <> show (length pre) <> "D"
+
+ InputMoveEnd -> withInput $ \( pre, post ) -> do
+ writeTVar termInput ( pre <> post, "" )
+ return $ "\ESC[" <> show (length post) <> "C"
+
+ InputEnd -> do
+ atomically (readTVar termInput) >>= \case
+ ( "", "" ) -> return Nothing
+ _ -> go
+
+ InputEscape _ -> go
+
+ withInput f = do
+ withMVar termLock $ const $ do
+ str <- atomically $ f =<< readTVar termInput
+ when (not $ null str) $ do
+ putStr str
+ hFlush stdout
+ go
+
+
+getCurrentPromptLine :: Terminal -> STM String
+getCurrentPromptLine Terminal {..} = do
+ prompt <- readTVar termPrompt
+ ( pre, post ) <- readTVar termInput
+ return $ prompt <> pre <> "\ESC[s" <> post <> "\ESC[u"
+
+setPrompt :: Terminal -> String -> IO ()
+setPrompt term@Terminal {..} prompt = do
+ withMVar termLock $ \_ -> do
+ join $ atomically $ do
+ writeTVar termPrompt prompt
+ readTVar termShowPrompt >>= \case
+ True -> do
+ promptLine <- getCurrentPromptLine term
+ return $ do
+ putStr $ "\r\ESC[K" <> promptLine
+ hFlush stdout
+ False -> return $ return ()
+
+printLine :: Terminal -> String -> IO TerminalLine
+printLine tlTerminal@Terminal {..} str = do
+ withMVar termLock $ \_ -> do
+ promptLine <- atomically $ do
+ readTVar termShowPrompt >>= \case
+ True -> getCurrentPromptLine tlTerminal
+ False -> return ""
+ putStr $ "\r\ESC[K" <> str <> "\n" <> promptLine
+ hFlush stdout
+ return TerminalLine {..}
+
+
+type CompletionFunc m = ( String, String ) -> m ( String, [ Completion ] )
+
+data Completion
+
+completeWordWithPrev :: Maybe Char -> [ Char ] -> (String -> String -> m [ Completion ]) -> CompletionFunc m
+completeWordWithPrev = error "TODO"
+
+simpleCompletion :: String -> Completion
+simpleCompletion = error "TODO"