Haskell читает исходный ввод с клавиатуры
Я пишу программу терминального режима в Haskell. Как я могу прочитать информацию о сыром keypress?
В частности, похоже, что есть что-то, предоставляющее средства редактирования строк поверх Haskell. Если я выполняю getLine
, я, похоже, могу использовать стрелку вверх, чтобы получить предыдущие строки, отредактировать текст, и только когда я нажимаю Enter, текст становится видимым для самого приложения Haskell.
То, что мне нужно, - это возможность читать отдельные нажатия клавиш, поэтому я могу самостоятельно выполнять редактирование строк.
Возможно, мой вопрос был неясным. В принципе, я хочу построить что-то вроде Vi или Emacs (или Yi). Я уже знаю, что есть привязки терминалов, которые позволят мне притворяться в режиме консольного режима, поэтому выходная сторона не должна быть проблемой. Я просто ищу способ получить исходный ввод ввода, поэтому я могу делать такие вещи, как (например) добавить K в текущую строку текста, когда пользователь нажимает букву K или сохраняет файл на диск, когда пользователь нажимает Ctrl + S.
Ответы
Ответ 1
Похоже, вы хотите поддержку 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
Ответ 2
Неполная:
После нескольких часов веб-серфинга я могу сообщить следующее:
-
readline
имеет огромный интерфейс практически без документации. Из имен функций и подписи типов вы могли бы догадаться, что делает этот материал... но это далеко не тривиально. Во всяком случае, эта библиотека, кажется, обеспечивает интерфейс редактирования на высоком уровне - это то, что я пытаюсь реализовать сам. Мне нужно что-то более низкоуровневое.
-
После прохода через источник haskeline
, похоже, у него огромный низкоуровневый код путаницы, отдельно для Win32 и POSIX. Если есть простой способ выполнить консольный ввод-вывод, эта библиотека не демонстрирует его. Код выглядит настолько тесно интегрированным и очень специфичным для haskeline
, что я сомневаюсь, что смогу его повторно использовать. Но, возможно, читая его, я могу учиться достаточно, чтобы написать свой собственный?
-
Yi... бесстрашный массив. В файле Cabal перечислены > 150 открытых модулей. (!!) Похоже, что под ним используется пакет под названием vty
, который является только POSIX. (Интересно, как, черт возьми, Yi работает на Windows?) vty
выглядит так, как будто он может быть полезен для меня без дальнейшей модификации. (Но опять же, не в Windows.)
-
unix
имеет... в принципе ничего интересного. У этого есть куча вещей, чтобы установить вещи на терминале, но абсолютно ничего для чтения с терминала. (Кроме того, чтобы проверить, включено ли эхо и т.д. Ничего о нажатиях клавиш.)
-
unix-compat
не имеет абсолютно никакого интереса.
Ответ 3
Это может быть самое простое решение, похожее на типичный код на других языках программирования:
import System.IO (stdin, hReady)
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)
import Control.Monad (when)
-- Simple menu controller
main = do
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
Это немного хакерское, поскольку теоретически пользователь может вводить больше ключей, прежде чем программа попадет на hReady
. Что может произойти, если терминал позволяет вставить. Но на практике для интерактивного ввода это не реалистичный сценарий.
Фактор удовольствия: Строки курсора могут быть putStr
d, чтобы на самом деле перемещать курсор программно.
Ответ 4
Один из вариантов заключается в использовании ncurses. Минималистский пример:
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
Ответ 5
Я думаю, что вы ищете hSetBuffering. По умолчанию StdIn привязан к строке, но
вы хотите получить ключи сразу.
Ответ 6
Я думаю, что библиотека unix
обеспечивает наиболее легкое решение для этого, особенно если вы знакомы с termios
, что зеркалированный модулем System.Posix.Terminal
.
На gnu.org хорошая страница, описывающая использование termios
для настройки неканонического режима ввода для терминала и вы можете сделать это с помощью 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