{-# LANGUAGE CPP #-} #if __GLASGOW_HASKELL__ >= 702 # if defined(WINDOWS) {-# LANGUAGE Trustworthy #-} # else # if __GLASGOW_HASKELL__ >= 704 {-# LANGUAGE Safe #-} # else {-# LANGUAGE Trustworthy #-} # endif # endif #endif {-| Module: System.IO.Echo.Internal Copyright: (C) 2016-2017 Ryan Scott License: BSD-style (see the file LICENSE) Maintainer: Ryan Scott Stability: Provisional Portability: Portable Exports functions that handle whether or not terminal input is handled in a way that should be portable across different platforms and consoles. Unlike "System.IO.Echo", this module exports internal functionality which, if used improperly, can lead to runtime errors. Make sure to read the documentation beforehand! -} module System.IO.Echo.Internal ( -- * Safe public interface withoutInputEcho, bracketInputEcho , getInputEchoState, setInputEchoState , echoOff, echoOn -- * Alternative (safe) interface , getInputEcho, setInputEcho -- * Unsafe STTY internals , EchoState(..), STTYSettings , getInputEchoSTTY, setInputEchoSTTY, sttyRaw -- * MinTTY , minTTY ) where import Control.Exception (bracket, throw) import Control.Monad (void) import Data.List (isInfixOf) import System.Exit (ExitCode(..)) import System.IO (hGetContents, hGetEcho, hSetEcho, stdin) import System.Process (StdStream(..), createProcess, shell, std_in, std_out, waitForProcess) #if defined(WINDOWS) import Graphics.Win32.Misc (getStdHandle, sTD_INPUT_HANDLE) import System.Console.MinTTY (isMinTTYHandle) import System.IO.Unsafe (unsafePerformIO) #endif -- | Return whether the terminal's echoing is on ('True') or off ('False'). -- -- Note that while this works on MinTTY, it is not as efficient as -- 'getInputEchoState', as it involves a somewhat expensive substring -- computation. getInputEcho :: IO Bool getInputEcho = if minTTY then do settings <- sttyRaw "-a" -- This assumes that other settings come after -- [-]echo in the output of `stty -a`. Luckily, this -- seems to be the case on every incarnation of -- MinTTY that I've tried. return $ not ("-echo " `isInfixOf` settings) else hGetEcho stdin -- | Return the terminal's current input 'EchoState'. getInputEchoState :: IO EchoState getInputEchoState = if minTTY then fmap MinTTY getInputEchoSTTY else fmap DefaultTTY $ hGetEcho stdin -- | Return all of @stty@'s current settings in a non-human-readable format. -- -- This function is not very useful on its own. Its greater purpose is to -- provide a compact 'STTYSettings' that can be fed back into -- 'setInputEchoState'. getInputEchoSTTY :: IO STTYSettings getInputEchoSTTY = sttyRaw "-g" -- | Set the terminal's echoing on ('True') or off ('False'). setInputEcho :: Bool -> IO () setInputEcho echo = if minTTY then setInputEchoSTTY $ ['-' | not echo] ++ "echo" else hSetEcho stdin echo -- | Set the terminal's input 'EchoState'. setInputEchoState :: EchoState -> IO () setInputEchoState (MinTTY settings) = setInputEchoSTTY settings setInputEchoState (DefaultTTY echo) = hSetEcho stdin echo -- | Create an @stty@ process and wait for it to complete. This is useful for -- changing @stty@'s settings, after which @stty@ does not output anything. -- -- @ -- setInputEchoSTTY = 'void' . 'sttyRaw' -- @ setInputEchoSTTY :: STTYSettings -> IO () setInputEchoSTTY = void . sttyRaw -- | Save the terminal's current input 'EchoState', perform a computation, -- restore the saved 'EchoState', and then return the result of the -- computation. -- -- @ -- bracketInputEcho action = 'bracket' 'getInputEchoState' 'setInputEchoState' (const action) -- @ bracketInputEcho :: IO a -> IO a bracketInputEcho action = bracket getInputEchoState setInputEchoState (const action) -- | Perform a computation with the terminal's input echoing disabled. Before -- running the computation, the terminal's input 'EchoState' is saved, and the -- saved 'EchoState' is restored after the computation finishes. -- -- @ -- withoutInputEcho action = 'bracketInputEcho' ('setInputEchoState' 'echoOff' >> action) -- @ withoutInputEcho :: IO a -> IO a withoutInputEcho action = bracketInputEcho (setInputEchoState echoOff >> action) -- | Create an @stty@ process, wait for it to complete, and return its output. sttyRaw :: String -> IO STTYSettings sttyRaw arg = do let stty = (shell $ "stty " ++ arg) { std_in = UseHandle stdin , std_out = CreatePipe } (_, mbStdout, _, rStty) <- createProcess stty exStty <- waitForProcess rStty case exStty of e@ExitFailure{} -> throw e ExitSuccess -> maybe (return "") hGetContents mbStdout -- | A representation of the terminal input's current echoing state. Example -- values include 'echoOff' and 'echoOn'. data EchoState = MinTTY STTYSettings -- ^ The argument to (or value returned from) an invocation of the @stty@ -- command-line utility. Most POSIX-like shells have @stty@, including -- MinTTY on Windows. Since neither 'hGetEcho' nor 'hSetEcho' work on -- MinTTY, when 'getInputEchoState' runs on MinTTY, it returns a value -- built with this constructor. -- -- However, native Windows consoles like @cmd.exe@ or PowerShell do not -- have @stty@, so if you construct an 'EchoState' with this constructor -- manually, take care not to use it with a native Windows console. | DefaultTTY Bool -- ^ A simple on ('True') or off ('False') toggle. This is returned by -- 'hGetEcho' and given as an argument to 'hSetEcho', which work on most -- consoles, with the notable exception of MinTTY on Windows. If you -- construct an 'EchoState' with this constructor manually, take care not -- to use it with MinTTY. deriving (Eq, Ord, Show) -- | Indicates that the terminal's input echoing is (or should be) off. echoOff :: EchoState echoOff = if minTTY then MinTTY "-echo" else DefaultTTY False -- | Indicates that the terminal's input echoing is (or should be) on. echoOn :: EchoState echoOn = if minTTY then MinTTY "echo" else DefaultTTY True -- | Settings used to configure the @stty@ command-line utility. type STTYSettings = String -- | Is the current process attached to a MinTTY console (e.g., Cygwin or MSYS)? minTTY :: Bool #if defined(WINDOWS) minTTY = unsafePerformIO $ do h <- getStdHandle sTD_INPUT_HANDLE isMinTTYHandle h {-# NOINLINE minTTY #-} #else minTTY = False #endif