{-# language InstanceSigs #-} {-# language LambdaCase #-} {-# language RankNTypes #-} {-# language RecordWildCards #-} {-# language ScopedTypeVariables #-} {-# language TypeFamilies #-} {-# language UnicodeSyntax #-} module Termbox ( -- $intro -- * Initialization main , InitError(..) -- * Terminal contents , Cell(..) , set , buffer , clear , flush -- * Terminal size , size -- * Cursor manipulation , setCursor , hideCursor -- * Event handling , Event(..) , Key(..) , Mouse(..) , poll , PollError(..) -- * Attributes , Attr , black , red , green , yellow , blue , magenta , cyan , white , bold , underline , reverse -- * Terminal modes , InputMode(..) , MouseMode(..) , getInputMode , setInputMode , OutputMode(..) , getOutputMode , setOutputMode ) where import Prelude hiding (mod, reverse) import qualified Termbox.Internal as Tb import Control.Exception import Control.Monad (join) import Data.Array.Storable import Data.Bits ((.|.), (.&.)) import Data.Functor (void) import Data.Semigroup (Semigroup(..)) import Data.Word import Foreign (ForeignPtr, Ptr, newForeignPtr_) import Foreign.Marshal.Alloc (alloca) import Foreign.Storable import qualified Data.Array.Storable.Internals as Array -- $intro -- This module is intended to be imported qualified. -- -- @ -- import qualified Termbox -- @ -------------------------------------------------------------------------------- -- Initialization -------------------------------------------------------------------------------- -- | Initialization errors that can be thrown by 'main'. data InitError = FailedToOpenTTY | PipeTrapError | UnsupportedTerminal deriving (Show) instance Exception InitError -- | Run a @termbox@ program and restore the terminal state afterwards. May -- throw an 'InitError' exception. main :: IO a -> IO a main = bracket_ (Tb.init >>= \case Tb.InitOk -> pure () Tb.FailedToOpenTTY -> throwIO FailedToOpenTTY Tb.PipeTrapError -> throwIO PipeTrapError Tb.UnsupportedTerminal -> throwIO UnsupportedTerminal) Tb.shutdown -------------------------------------------------------------------------------- -- Terminal size -------------------------------------------------------------------------------- -- | Get the terminal width and height. size :: (width ~ Int, height ~ Int) => IO (width, height) size = (,) <$> Tb.width <*> Tb.height -------------------------------------------------------------------------------- -- Cursor -------------------------------------------------------------------------------- -- | Set the cursor coordinates. setCursor :: (col ~ Int, row ~ Int) => col -> row -> IO () setCursor = Tb.setCursor -- | Hide the cursor. hideCursor :: IO () hideCursor = Tb.setCursor Tb._HIDE_CURSOR Tb._HIDE_CURSOR -------------------------------------------------------------------------------- -- Terminal contents -------------------------------------------------------------------------------- -- | A 'Cell' contains a character, foreground attribute, and background -- attribute. data Cell = Cell !Char !Attr !Attr deriving (Eq) instance Show Cell where show (Cell ch fg bg) = "Cell " ++ show ch ++ " " ++ show (attrToWord fg) ++ " " ++ show (attrToWord bg) instance Storable Cell where sizeOf :: Cell -> Int sizeOf _ = Tb.sizeofCell alignment :: Cell -> Int alignment _ = Tb.alignofCell peek :: Ptr Cell -> IO Cell peek ptr = Cell <$> Tb.getCellCh ptr <*> (wordToAttr <$> Tb.getCellFg ptr) <*> (wordToAttr <$> Tb.getCellBg ptr) poke :: Ptr Cell -> Cell -> IO () poke ptr (Cell ch fg bg) = do Tb.setCellCh ptr ch Tb.setCellFg ptr (attrToWord fg) Tb.setCellBg ptr (attrToWord bg) -- | Set the 'Cell' at the given coordinates. set :: (col ~ Int, row ~ Int) => col -> row -> Cell -> IO () set x y (Cell ch fg bg) = Tb.changeCell x y ch (attrToWord fg) (attrToWord bg) -- | Get the terminal's internal back buffer as a two-dimensional array of -- 'Cell's indexed by their coordinates. -- -- /Warning/: the data is only valid until the next call to 'clear' or -- 'flush'. buffer :: (row ~ Int, col ~ Int) => IO (StorableArray (row, col) Cell) buffer = join (mkbuffer <$> (tb_cell_buffer >>= newForeignPtr_) <*> Tb.width <*> Tb.height) where mkbuffer :: ForeignPtr Cell -> Int -> Int -> IO (StorableArray (Int, Int) Cell) mkbuffer buff w h = Array.unsafeForeignPtrToStorableArray buff ((0, 0), (h-1, w-1)) -- | Clear the back buffer with the given foreground and background attributes. clear :: (fg ~ Attr, bg ~ Attr) => fg -> bg -> IO () clear fg bg = do Tb.setClearAttributes (attrToWord fg) (attrToWord bg) Tb.clear -- | Synchronize the internal back buffer with the terminal. flush :: IO () flush = Tb.present -------------------------------------------------------------------------------- -- Terminal mode -------------------------------------------------------------------------------- -- | The input modes. -- -- * __Esc__. When ESC sequence is in the buffer and it doesn't match any known -- sequence, ESC means 'KeyEsc'. -- -- * __Alt__. When ESC sequence is in the buffer and it doesn't match any known -- sequence, ESC enables the /alt/ modifier for the next keyboard event. data InputMode = InputModeEsc MouseMode -- ^ Default. | InputModeAlt MouseMode deriving (Eq, Ord, Show) -- | The mouse mode. -- -- * __No__. Don't handle mouse events. -- -- * __Yes__. Handle mouse events. data MouseMode = MouseModeNo -- ^ Default. | MouseModeYes deriving (Eq, Ord, Show) -- | Get the current input mode. getInputMode :: IO InputMode getInputMode = f <$> Tb.selectInputMode Tb._INPUT_CURRENT where f :: Int -> InputMode f = \case 1 -> InputModeEsc MouseModeNo 2 -> InputModeAlt MouseModeNo 5 -> InputModeEsc MouseModeYes 6 -> InputModeAlt MouseModeYes n -> error ("getInputMode: " ++ show n) -- | Set the input mode. setInputMode :: InputMode -> IO () setInputMode = void . Tb.selectInputMode . f where f :: InputMode -> Int f = \case InputModeEsc MouseModeNo -> Tb._INPUT_ESC InputModeEsc MouseModeYes -> Tb._INPUT_ESC .|. Tb._INPUT_MOUSE InputModeAlt MouseModeNo -> Tb._INPUT_ALT InputModeAlt MouseModeYes -> Tb._INPUT_ALT .|. Tb._INPUT_MOUSE -- | The output modes. -- -- * __Normal__. Supports colors /0..8/, which includes all named color -- attributes exported by this library, e.g. 'red'. -- -- * __Grayscale__. Supports colors /0..23/. -- -- * __216__. Supports colors /0..216/. -- -- * __256__. Supports colors /0..255/. data OutputMode = OutputModeNormal -- ^ Default. | OutputModeGrayscale | OutputMode216 | OutputMode256 deriving (Eq, Ord, Show) -- | Get the current output mode. getOutputMode :: IO OutputMode getOutputMode = f <$> Tb.selectOutputMode Tb.OutputModeCurrent where f :: Tb.OutputMode -> OutputMode f = \case Tb.OutputModeNormal -> OutputModeNormal Tb.OutputMode256 -> OutputMode256 Tb.OutputMode216 -> OutputMode216 Tb.OutputModeGrayscale -> OutputModeGrayscale Tb.OutputModeCurrent -> error "getOutputMode: OutputModeCurrent" -- | Set the output mode. setOutputMode :: OutputMode -> IO () setOutputMode = void . Tb.selectOutputMode . f where f :: OutputMode -> Tb.OutputMode f = \case OutputModeNormal -> Tb.OutputModeNormal OutputMode256 -> Tb.OutputMode256 OutputMode216 -> Tb.OutputMode216 OutputModeGrayscale -> Tb.OutputModeGrayscale -------------------------------------------------------------------------------- -- Event handling -------------------------------------------------------------------------------- -- | A input event. data Event = EventKey !Key !Bool -- ^ Key event. The bool indicates the /alt/ modifier. | EventResize !Int !Int -- ^ Resize event (width, then height) | EventMouse !Mouse !Int !Int -- ^ Mouse event (column, then row) deriving (Eq, Show) -- | A key event. data Key = KeyChar Char | KeyArrowDown | KeyArrowLeft | KeyArrowRight | KeyArrowUp | KeyBackspace | KeyBackspace2 | KeyCtrl2 | KeyCtrl3 | KeyCtrl4 | KeyCtrl5 | KeyCtrl6 | KeyCtrl7 | KeyCtrl8 | KeyCtrlA | KeyCtrlB | KeyCtrlBackslash | KeyCtrlC | KeyCtrlD | KeyCtrlE | KeyCtrlF | KeyCtrlG | KeyCtrlH | KeyCtrlI | KeyCtrlJ | KeyCtrlK | KeyCtrlL | KeyCtrlLsqBracket | KeyCtrlM | KeyCtrlN | KeyCtrlO | KeyCtrlP | KeyCtrlQ | KeyCtrlR | KeyCtrlRsqBracket | KeyCtrlS | KeyCtrlSlash | KeyCtrlT | KeyCtrlTilde | KeyCtrlU | KeyCtrlUnderscore | KeyCtrlV | KeyCtrlW | KeyCtrlX | KeyCtrlY | KeyCtrlZ | KeyDelete | KeyEnd | KeyEnter | KeyEsc | KeyF1 | KeyF10 | KeyF11 | KeyF12 | KeyF2 | KeyF3 | KeyF4 | KeyF5 | KeyF6 | KeyF7 | KeyF8 | KeyF9 | KeyHome | KeyInsert | KeyPageDn | KeyPageUp | KeySpace | KeyTab deriving (Eq, Ord, Show) -- | A mouse event. data Mouse = MouseLeft | MouseMiddle | MouseRelease | MouseRight | MouseWheelDown | MouseWheelUp deriving (Eq, Ord, Show) -- | Block until an 'Event' arrives. -- -- /Note/: @termbox v1.1.2@ does not properly handle OS signals that interrupt -- the underlying @select@ system call, so unfortunately the familiar @Ctrl-C@ -- will not be able to stop a program stuck in 'pollEvent'. -- -- You can work around this issue by polling in a background thread using the -- @threaded@ runtime, or simply writing event-handling code that is responsive -- to intuitive "quit" keys like @q@ and @Esc@. -- -- This function may throw a 'PollError' exception under mysterious -- circumstances that are not well-documented in the original C codebase. poll :: IO Event poll = alloca $ \ptr -> Tb.pollEvent ptr >>= \case -1 -> throwIO PollError _ -> parseEvent <$> peek ptr -- | An error occurred when 'poll'ing. data PollError = PollError deriving Show instance Exception PollError -- | Parse an 'Event' from a 'Tb.Event'. parseEvent :: Tb.Event -> Event parseEvent = \case Tb.Event Tb.EventKey mod key ch _ _ _ _ -> parseEventKey mod key ch Tb.Event Tb.EventResize _ _ _ w h _ _ -> EventResize w h Tb.Event Tb.EventMouse _ key _ _ _ x y -> EventMouse (parseMouse key) x y -- | Parse a key 'Event'. parseEventKey :: Tb.Mod -> Tb.Key -> Char -> Event parseEventKey mod key ch = EventKey key' alt where key' :: Key key' = case ch of '\0' -> parseKey key _ -> KeyChar ch alt :: Bool alt = case mod of Tb.ModAlt -> True _ -> False -- | Parse a 'Key' from a 'Tb.Key'. parseKey :: Tb.Key -> Key parseKey = \case Tb.KeyArrowDown -> KeyArrowDown Tb.KeyArrowLeft -> KeyArrowLeft Tb.KeyArrowRight -> KeyArrowRight Tb.KeyArrowUp -> KeyArrowUp Tb.KeyBackspace -> KeyBackspace Tb.KeyBackspace2 -> KeyBackspace2 Tb.KeyCtrl2 -> KeyCtrl2 Tb.KeyCtrl3 -> KeyCtrl3 Tb.KeyCtrl4 -> KeyCtrl4 Tb.KeyCtrl5 -> KeyCtrl5 Tb.KeyCtrl6 -> KeyCtrl6 Tb.KeyCtrl7 -> KeyCtrl7 Tb.KeyCtrl8 -> KeyCtrl8 Tb.KeyCtrlA -> KeyCtrlA Tb.KeyCtrlB -> KeyCtrlB Tb.KeyCtrlBackslash -> KeyCtrlBackslash Tb.KeyCtrlC -> KeyCtrlC Tb.KeyCtrlD -> KeyCtrlD Tb.KeyCtrlE -> KeyCtrlE Tb.KeyCtrlF -> KeyCtrlF Tb.KeyCtrlG -> KeyCtrlG Tb.KeyCtrlH -> KeyCtrlH Tb.KeyCtrlI -> KeyCtrlI Tb.KeyCtrlJ -> KeyCtrlJ Tb.KeyCtrlK -> KeyCtrlK Tb.KeyCtrlL -> KeyCtrlL Tb.KeyCtrlLsqBracket -> KeyCtrlLsqBracket Tb.KeyCtrlM -> KeyCtrlM Tb.KeyCtrlN -> KeyCtrlN Tb.KeyCtrlO -> KeyCtrlO Tb.KeyCtrlP -> KeyCtrlP Tb.KeyCtrlQ -> KeyCtrlQ Tb.KeyCtrlR -> KeyCtrlR Tb.KeyCtrlRsqBracket -> KeyCtrlRsqBracket Tb.KeyCtrlS -> KeyCtrlS Tb.KeyCtrlSlash -> KeyCtrlSlash Tb.KeyCtrlT -> KeyCtrlT Tb.KeyCtrlTilde -> KeyCtrlTilde Tb.KeyCtrlU -> KeyCtrlU Tb.KeyCtrlUnderscore -> KeyCtrlUnderscore Tb.KeyCtrlV -> KeyCtrlV Tb.KeyCtrlW -> KeyCtrlW Tb.KeyCtrlX -> KeyCtrlX Tb.KeyCtrlY -> KeyCtrlY Tb.KeyCtrlZ -> KeyCtrlZ Tb.KeyDelete -> KeyDelete Tb.KeyEnd -> KeyEnd Tb.KeyEnter -> KeyEnter Tb.KeyEsc -> KeyEsc Tb.KeyF1 -> KeyF1 Tb.KeyF10 -> KeyF10 Tb.KeyF11 -> KeyF11 Tb.KeyF12 -> KeyF12 Tb.KeyF2 -> KeyF2 Tb.KeyF3 -> KeyF3 Tb.KeyF4 -> KeyF4 Tb.KeyF5 -> KeyF5 Tb.KeyF6 -> KeyF6 Tb.KeyF7 -> KeyF7 Tb.KeyF8 -> KeyF8 Tb.KeyF9 -> KeyF9 Tb.KeyHome -> KeyHome Tb.KeyInsert -> KeyInsert Tb.KeyPageDn -> KeyPageDn Tb.KeyPageUp -> KeyPageUp Tb.KeySpace -> KeySpace Tb.KeyTab -> KeyTab key -> error ("parseKey: " ++ show key) -- | Parse a 'Mouse' from a 'Tb.Key'. parseMouse :: Tb.Key -> Mouse parseMouse = \case Tb.KeyMouseLeft -> MouseLeft Tb.KeyMouseMiddle -> MouseMiddle Tb.KeyMouseRelease -> MouseRelease Tb.KeyMouseRight -> MouseRight Tb.KeyMouseWheelDown -> MouseWheelDown Tb.KeyMouseWheelUp -> MouseWheelUp key -> error ("parseMouse: " ++ show key) -------------------------------------------------------------------------------- -- Attributes -------------------------------------------------------------------------------- -- | A cell attribute, which includes its color, and whether or not it is -- bold, underlined, and/or reversed. -- -- A cell can only have one color, but may be (for example) bold /and/ -- underlined. The 'Monoid' instance combines 'Attr's this way, with a left -- bias. That is, -- -- @ -- red <> bold <> black <> underline = red <> bold <> underline -- @ -- -- /Warning/: the 'Num' instance is /very partial/! It only includes an -- implementation of 'fromInteger', for numeric literals. data Attr = Attr !Word16 {- color -} !Word16 {- attr -} deriving (Eq) instance Monoid Attr where mempty :: Attr mempty = Attr Tb._DEFAULT 0 mappend :: Attr -> Attr -> Attr mappend = (<>) -- | Only 'fromInteger' is defined. instance Num Attr where fromInteger :: Integer -> Attr fromInteger n | n >= 0 && n < 256 = Attr (fromIntegral n) 0 | otherwise = error ("Attr.fromInteger: " ++ show n ++ " out of range [0..255]") (+) = error ("Attr.(+): not defined") (*) = error ("Attr.(*): not defined") (-) = error ("Attr.(-): not defined") abs = error ("Attr.abs: not defined") signum = error ("Attr.signum: not defined") -- | Left-biased color; attributes are merged. instance Semigroup Attr where (<>) :: Attr -> Attr -> Attr Attr 0 ax <> Attr cy ay = Attr cy (ax .|. ay) Attr cx ax <> Attr 0 ay = Attr cx (ax .|. ay) Attr cx ax <> Attr _ ay = Attr cx (ax .|. ay) wordToAttr :: Word16 -> Attr wordToAttr w = Attr (w .&. 0x00FF) (w .&. 0xFF00) attrToWord :: Attr -> Word16 attrToWord (Attr x y) = x .|. y -- | @black = 1@. black :: Attr black = Attr Tb._BLACK 0 -- | @red = 2@. red :: Attr red = Attr Tb._RED 0 -- | @green = 3@. green :: Attr green = Attr Tb._GREEN 0 -- | @yellow = 4@. yellow :: Attr yellow = Attr Tb._YELLOW 0 -- | @blue = 5@. blue :: Attr blue = Attr Tb._BLUE 0 -- | @magenta = 6@. magenta :: Attr magenta = Attr Tb._MAGENTA 0 -- | @cyan = 7@. cyan :: Attr cyan = Attr Tb._CYAN 0 -- | @white = 8@. white :: Attr white = Attr Tb._WHITE 0 -- | Bold modifier attribute. bold :: Attr bold = Attr Tb._DEFAULT Tb._BOLD -- | Underline modifier attribute. underline :: Attr underline = Attr Tb._DEFAULT Tb._UNDERLINE -- | Reverse modifier attribute. reverse :: Attr reverse = Attr Tb._DEFAULT Tb._REVERSE -------------------------------------------------------------------------------- -- Foreign imports -------------------------------------------------------------------------------- foreign import ccall safe "termbox.h tb_cell_buffer" tb_cell_buffer :: IO (Ptr Cell)