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