| {-# LANGUAGE OverloadedStrings #-} |
|
|
| |
| |
| |
| |
|
|
| module LiquidLean.QuantumPiper.Terminal |
| ( Terminal(..) |
| , TerminalSession(..) |
| , initTerminal |
| , executeCommand |
| , streamOutput |
| , closeTerminal |
| ) where |
|
|
| import Data.Text (Text) |
| import qualified Data.Text as T |
| import qualified Data.Text.IO as TIO |
| import Data.ByteString (ByteString) |
| import qualified Data.ByteString as BS |
| import System.Process |
| ( createProcess, proc, std_in, std_out, std_err, CreateProcess(..) |
| , StdStream(..), waitForProcess, getPid |
| ) |
| import System.IO |
| ( Handle, hGetContents, hPutStrLn, hClose, hFlush, hGetLine |
| , hIsEOF, hSetBuffering, BufferMode(..) |
| ) |
| import Control.Exception (catch, SomeException, try, bracket) |
| import Data.Time.Clock.POSIX (getPOSIXTime) |
| import Data.List (intercalate) |
| import Control.Concurrent (forkIO, threadDelay) |
| import Control.Concurrent.Chan (newChan, writeChan, readChan) |
|
|
| |
| |
| |
|
|
| data Terminal = Terminal |
| { tName :: Text |
| , tRows :: Int |
| , tCols :: Int |
| , tBackend :: TerminalBackend |
| } deriving (Show) |
|
|
| data TerminalBackend |
| = WinConsole |
| | UnixPTY |
| | VirtualTerminal |
| deriving (Show, Eq) |
|
|
| data TerminalSession = TerminalSession |
| { tsTerminal :: Terminal |
| , tsProcess :: Maybe (Handle, Handle, Handle) |
| , tsSessionId :: Text |
| , tsHistory :: [Text] |
| } deriving (Show) |
|
|
| |
| |
| |
|
|
| initTerminal :: Text -> Int -> Int -> IO (Either String TerminalSession) |
| initTerminal name rows cols = do |
| result <- try $ do |
| |
| backend <- detectTerminalBackend |
|
|
| let terminal = Terminal name rows cols backend |
|
|
| |
| let sessionId = T.concat [name, "-", T.pack (show rows), "x", T.pack (show cols)] |
|
|
| pure (TerminalSession terminal Nothing sessionId []) |
|
|
| case result of |
| Left (e :: SomeException) -> pure (Left $ "Terminal init failed: " ++ show e) |
| Right session -> pure (Right session) |
|
|
| detectTerminalBackend :: IO TerminalBackend |
| detectTerminalBackend = do |
| |
| let isWindows = False |
| if isWindows |
| then pure WinConsole |
| else pure UnixPTY |
|
|
| |
| |
| |
|
|
| executeCommand :: TerminalSession -> Text -> IO (Either String (TerminalSession, [Text])) |
| executeCommand session cmd = do |
| result <- try $ do |
| let cmdStr = T.unpack cmd |
| let cmdParts = words cmdStr |
|
|
| |
| (exitCode, stdout, stderr) <- readProcessWithExitCode |
| (head cmdParts) |
| (tail cmdParts) |
| "" |
|
|
| let output = lines (stdout ++ stderr) |
| let outputText = map T.pack output |
|
|
| |
| let history' = tsHistory session ++ [cmd] ++ outputText |
|
|
| let session' = session { tsHistory = history' } |
|
|
| pure (session', outputText) |
|
|
| case result of |
| Left (e :: SomeException) -> pure (Left $ "Command execution failed: " ++ show e) |
| Right output -> pure (Right output) |
|
|
| |
| readProcessWithExitCode :: FilePath -> [String] -> String -> IO (Int, String, String) |
| readProcessWithExitCode _ _ _ = pure (0, "", "") |
|
|
| |
| |
| |
|
|
| streamOutput :: TerminalSession -> (Text -> IO ()) -> IO (Either String ()) |
| streamOutput session onLine = do |
| result <- try $ do |
| |
| mapM_ onLine (tsHistory session) |
| pure () |
|
|
| case result of |
| Left (e :: SomeException) -> pure (Left $ "Stream failed: " ++ show e) |
| Right () -> pure (Right ()) |
|
|
| |
| |
| |
|
|
| startREPL :: TerminalSession -> IO (Either String ()) |
| startREPL session = do |
| result <- try $ do |
| putStrLn (T.unpack $ tName (tsTerminal session)) |
| putStrLn "> " |
|
|
| |
| let repl = do |
| line <- getLine |
| if line == "exit" |
| then pure () |
| else do |
| (exitCode, stdout, stderr) <- readProcessWithExitCode "bash" ["-c", line] "" |
| putStrLn (stdout ++ stderr) |
| putStrLn "> " |
| repl |
|
|
| repl |
|
|
| case result of |
| Left (e :: SomeException) -> pure (Left $ "REPL failed: " ++ show e) |
| Right () -> pure (Right ()) |
|
|
| |
| |
| |
|
|
| closeTerminal :: TerminalSession -> IO () |
| closeTerminal session = do |
| case tsProcess session of |
| Nothing -> pure () |
| Just (stdin, stdout, stderr) -> do |
| hClose stdin |
| hClose stdout |
| hClose stderr |
|
|
| |
| |
| |
|
|
| data TerminalBuffer = TerminalBuffer |
| { tbLines :: [Text] |
| , tbCursor :: (Int, Int) |
| , tbScrollback :: Int |
| } deriving (Show) |
|
|
| initBuffer :: Int -> Int -> TerminalBuffer |
| initBuffer rows cols = TerminalBuffer |
| { tbLines = replicate rows "" |
| , tbCursor = (0, 0) |
| , tbScrollback = 1000 |
| } |
|
|
| writeLine :: TerminalBuffer -> Int -> Text -> TerminalBuffer |
| writeLine buf row line = |
| let lines' = take row (tbLines buf) ++ [line] ++ drop (row + 1) (tbLines buf) |
| in buf { tbLines = lines' } |
|
|
| readLine :: TerminalBuffer -> Int -> Text |
| readLine buf row |
| | row < length (tbLines buf) = tbLines buf !! row |
| | otherwise = "" |
|
|
| |
| |
| |
|
|
| ansiClear :: Text |
| ansiClear = "\ESC[2J" |
|
|
| ansiMoveCursor :: Int -> Int -> Text |
| ansiMoveCursor row col = T.concat ["\ESC[", T.pack (show row), ";", T.pack (show col), "H"] |
|
|
| ansiSetColor :: Text -> Text |
| ansiSetColor "red" = "\ESC[31m" |
| ansiSetColor "green" = "\ESC[32m" |
| ansiSetColor "yellow" = "\ESC[33m" |
| ansiSetColor "blue" = "\ESC[34m" |
| ansiSetColor _ = "\ESC[0m" |
|
|
| |
| |
| |
|
|
| data TerminalState |
| = Idle |
| | Executing |
| | Streaming |
| | Suspended |
| deriving (Show, Eq) |
|
|
| |
| |
| |
|
|
| shellBash :: Text -> IO Text |
| shellBash cmd = do |
| (_, stdout, stderr, _) <- createProcess |
| (proc "bash" ["-c", T.unpack cmd]) |
| { std_out = CreatePipe |
| , std_err = CreatePipe |
| } |
|
|
| outLines <- case stdout of |
| Nothing -> pure [] |
| Just handle -> lines <$> hGetContents handle |
|
|
| errLines <- case stderr of |
| Nothing -> pure [] |
| Just handle -> lines <$> hGetContents handle |
|
|
| pure (T.unlines (map T.pack (outLines ++ errLines))) |
|
|
| |
| |
| |
|
|
| sovereignTerminal :: IO (Either String TerminalSession) |
| sovereignTerminal = do |
| |
| result <- initTerminal "sovereign-terminal" 24 80 |
|
|
| case result of |
| Left err -> pure (Left err) |
| Right session -> do |
| |
| (exitCode, stdout, stderr) <- readProcessWithExitCode "bash" ["-c", "uname -a"] "" |
| if exitCode == 0 |
| then do |
| let session' = session |
| { tsHistory = T.pack stdout : tsHistory session |
| } |
| pure (Right session') |
| else pure (Left "Shell access failed") |
|
|