Haskell读取原始键盘输入

时间:2014-04-14 19:06:54

标签: haskell io terminal

我正在Haskell中编写终端模式程序。我将如何阅读原始按键信息?

特别是,似乎有一些东西在Haskell之上提供行编辑工具。如果我getLine,我似乎能够使用向上箭头来获取前一行,编辑文本,并且只有当我按Enter键时,文本才会对Haskell应用程序本身可见。

我所追求的是能够阅读单个按键,因此我可以自己实现行编辑。


也许我的问题不清楚。基本上我想构建类似Vi或Emacs(或Yi)的东西。我已经知道有终端绑定可以让我进行花哨的控制台模式打印,所以输出端不应该是一个问题。我只是想找到一种获取原始按键输入的方法,所以我可以做一些事情,比如(例如)当用户按下字母K时将K添加到当前文本行,或者当用户将文件保存到磁盘时按Ctrl + S.

6 个答案:

答案 0 :(得分:9)

听起来你想要readline支持。有几个软件包可以做到这一点,但haskeline可能是最容易使用支持最多的平台。

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

答案 1 :(得分:9)

这可能是最简单的解决方案,类似于其他编程语言中的典型代码:

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)

它的工作原理是“一次”读取多个字符。允许例如键,由要与实际['\ESC','[','A']字符输入区分的三个字符\ESC组成。

用法示例:

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

这有点hackish,因为理论上,用户可以在程序到达hReady之前输入更多密钥。如果终端允许粘贴,可能会发生这种情况。但实际上,对于交互式输入,这不是一个现实的场景。

有趣的事实:光标字符串可以putStr来实际以编程方式移动光标。

答案 2 :(得分:7)

<强>未完成:

经过几个小时的网上冲浪后,我可以报告以下内容:

  • readline有一个巨大的界面,几乎没有任何文档。从函数名称和类型签名中你可以猜测这些东西的作用......但它远非琐碎。无论如何,这个库似乎提供了一个高级编辑界面 - 这是我试图实现自己的东西。我需要更低级别的东西。

  • 在浏览haskeline的源代码后,似乎它有一个巨大的纠结低级代码,单独用于Win32和POSIX。如果 是一种简单的方法来执行控制台I / O,则此库不会演示它。代码似乎紧密集成,并且对haskeline非常具体,我怀疑我可以重用它们。但也许通过阅读它我可以学到足够的东西来写自己的东西吗?

  • 易是......吓坏了大量的。 Cabal文件列表&gt; 150个暴露的模块。 (!!)但是,它下面显示的是使用名为vty的软件包,它只是POSIX。 (我想知道Yi如何在Windows上运行?)vty看起来它可能对我没有进一步修改直接有用。 (但同样,不是在Windows上。)

  • unix ...基本上没什么有趣的。它有很多东西可以在终端上设置东西,但是从终端读取绝对没有。 (除了可以检查是否有回声,等等。关于按键没什么。)

  • unix-compat完全没有兴趣。

答案 3 :(得分:4)

一种选择是使用ncurses。一个简约的例子:

<p:scrollPanel>

答案 4 :(得分:3)

我认为您正在寻找hSetBuffering。默认情况下,StdIn是行缓冲的,但是 你想立即收到钥匙。

答案 5 :(得分:2)

我认为unix库为此提供了最轻量级的解决方案,特别是如果您对termios有一定的了解,System.Posix.Terminal模块会对其进行镜像。

在gnu.org上有一个很好的页面,其中描述了使用termios为终端设置non-canonical input mode,您可以使用System.Posix.Terminal执行此操作。

这是我的解决方案,它将IO中的计算转换为使用非规范模式:

{- 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