Haskell read raw keyboard input

前端 未结 6 1349
暗喜
暗喜 2020-11-28 12:04

I\'m writing a terminal-mode program in Haskell. How would I go about reading raw keypress information?

In particular, there seems to be something providing line-edi

相关标签:
6条回答
  • 2020-11-28 12:32

    This might be the simplest solution, resembling typical code in other programming languages:

    import System.IO (stdin, hReady)
    
    getKey :: IO [Char]
    getKey = reverse <$> getKey' ""
      where getKey' chars = do
              char <- getChar
              more <- hReady stdin
              (if more then getKey' else return) (char:chars)
    

    It works by reading more than one character “at a time”. Allowing E.g. the key, which consists of the three characters ['\ESC','[','A'] to be distinguished from an actual \ESC character input.

    Usage example:

    import System.IO (stdin, hSetEcho, hSetBuffering, NoBuffering)
    import Control.Monad (when)
    
    -- Simple menu controller
    main = do
      hSetBuffering stdin NoBuffering
      hSetEcho stdin False
      key <- getKey
      when (key /= "\ESC") $ do
        case key of
          "\ESC[A" -> putStr "↑"
          "\ESC[B" -> putStr "↓"
          "\ESC[C" -> putStr "→"
          "\ESC[D" -> putStr "←"
          "\n"     -> putStr "⎆"
          "\DEL"   -> putStr "⎋"
          _        -> return ()
        main
    

    This is a bit hackish, since in theory, a user could input more keys before the program gets to the hReady. Which could happen if the terminal allows pasting. But in practice, for interactive input, this is not a realistic scenario.

    Fun fact: The cursor strings can be putStrd to actually move the cursor programmatically.

    0 讨论(0)
  • 2020-11-28 12:33

    Incomplete:

    After several hours of web surfing, I can report the following:

    • readline has a huge interface with virtually no documentation whatsoever. From the function names and type signatures you could maybe guess what this stuff does... but it's far from trivial. At any rate, this library seems to provide a high-level editing interface - which is the thing I'm trying to implement myself. I need something more low-level.

    • After wading through the source of haskeline, it seems it has a huge tangle low-level code, seperately for Win32 and POSIX. If there is an easy way to do console I/O, this library does not demonstrate it. The code appears to be so tightly integrated and highly specific to haskeline that I doubt I can reuse any of it. But perhaps by reading it I can learn enough to write my own?

    • Yi is... freaking massive. The Cabal file lists > 150 exposed modules. (!!) It appears, though, that underneath it's using a package called vty, which is POSIX-only. (I wonder how the hell Yi works on Windows then?) vty looks like it might be directly useful to me without further modification. (But again, not on Windows.)

    • unix has... basically nothing interesting. It has a bunch of stuff to set things on a terminal, but absolutely nothing for reading from a terminal. (Except maybe to check whether echo is on, etc. Nothing about keypresses.)

    • unix-compat has absolutely nothing of interest.

    0 讨论(0)
  • 2020-11-28 12:37

    Sounds like you want readline support. There are a couple of packages to do this, but haskeline is probably the easiest to use with the most supported platforms.

    import Control.Monad.Trans
    import System.Console.Haskeline
    
    type Repl a = InputT IO a
    
    process :: String -> IO ()
    process = putStrLn
    
    repl :: Repl ()
    repl = do
      minput <- getInputLine "> "
      case minput of
        Nothing -> outputStrLn "Goodbye."
        Just input -> (liftIO $ process input) >> repl
    
    main :: IO ()
    main = runInputT defaultSettings repl
    
    0 讨论(0)
  • 2020-11-28 12:37

    I think you are looking for hSetBuffering. StdIn is line buffered by default, but you want to receive the keys right away.

    0 讨论(0)
  • 2020-11-28 12:39

    One option would be to use ncurses. A minimalistic example:

    import Control.Monad
    import UI.NCurses
    
    main :: IO ()
    main = runCurses $ do
        w <- defaultWindow
        forever $ do
            e <- getEvent w Nothing
            updateWindow w $ do
                moveCursor 0 0
                drawString (show e)
            render
    
    0 讨论(0)
  • 2020-11-28 12:39

    I think the unix library provides the most lightweight solution for this, especially if you have some familiarity with termios, which is mirrored by the System.Posix.Terminal module.

    There's a good page over at gnu.org that describes using termios to set up non-canonical input mode for a terminal, and you can do this with System.Posix.Terminal.

    Here's my solution, which transforms a computation in IO to use non-canonical mode:

    {- from unix library -}
    import System.Posix.Terminal
    import System.Posix.IO (fdRead, stdInput)
    
    {- from base -}
    import System.IO (hFlush, stdout)
    import Control.Exception (finally, catch, IOException)
    
    {- run an application in raw input / non-canonical mode with given
     - VMIN and VTIME settings. for a description of these, see:
     - http://www.gnu.org/software/libc/manual/html_node/Noncanonical-Input.html
     - as well as `man termios`.
     -}
    withRawInput :: Int -> Int -> IO a -> IO a
    withRawInput vmin vtime application = do
    
      {- retrieve current settings -}
      oldTermSettings <- getTerminalAttributes stdInput
    
      {- modify settings -}
      let newTermSettings = 
            flip withoutMode  EnableEcho   . -- don't echo keystrokes
            flip withoutMode  ProcessInput . -- turn on non-canonical mode
            flip withTime     vtime        . -- wait at most vtime decisecs per read
            flip withMinInput vmin         $ -- wait for >= vmin bytes per read
            oldTermSettings
    
      {- install new settings -}
      setTerminalAttributes stdInput newTermSettings Immediately
    
      {- restore old settings no matter what; this prevents the terminal
       - from becoming borked if the application halts with an exception
       -}
      application 
        `finally` setTerminalAttributes stdInput oldTermSettings Immediately
    
    {- sample raw input method -}
    tryGetArrow = (do
      (str, bytes) <- fdRead stdInput 3
      case str of
        "\ESC[A" -> putStrLn "\nUp"
        "\ESC[B" -> putStrLn "\nDown"
        "\ESC[C" -> putStrLn "\nRight"
        "\ESC[D" -> putStrLn "\nLeft"
        _        -> return ()
      ) `catch` (
        {- if vmin bytes have not been read by vtime, fdRead will fail
         - with an EOF exception. catch this case and do nothing. 
         - The type signature is necessary to allow other exceptions 
         - to get through.
         -}
        (const $ return ()) :: IOException -> IO ()
      ) 
    
    {- sample application -}
    loop = do
      tryGetArrow 
      putStr "." >> hFlush stdout
      loop 
    
    {- run with:
     - VMIN  = 0 (don't wait for a fixed number of bytes)
     - VTIME = 1 (wait for at most 1/10 sec before fdRead returns)
     -}
    main = withRawInput 0 1 $ loop
    
    0 讨论(0)
提交回复
热议问题