我正在Haskell中编写终端模式程序。我将如何阅读原始按键信息?
特别是,似乎有一些东西在Haskell之上提供行编辑工具。如果我getLine
,我似乎能够使用向上箭头来获取前一行,编辑文本,并且只有当我按Enter键时,文本才会对Haskell应用程序本身可见。
我所追求的是能够阅读单个按键,因此我可以自己实现行编辑。
也许我的问题不清楚。基本上我想构建类似Vi或Emacs(或Yi)的东西。我已经知道有终端绑定可以让我进行花哨的控制台模式打印,所以输出端不应该是一个问题。我只是想找到一种获取原始按键输入的方法,所以我可以做一些事情,比如(例如)当用户按下字母K时将K添加到当前文本行,或者当用户将文件保存到磁盘时按Ctrl + S.
答案 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