packages feed

LambdaHack-0.9.5.0: engine-src/Game/LambdaHack/Client/UI/Frontend/Curses.hs

-- | Text frontend based on HSCurses. This frontend is not fully supported
-- due to the limitations of the curses library (keys, colours, last character
-- of the last line).
module Game.LambdaHack.Client.UI.Frontend.Curses
  ( startup, frontendName
  ) where

import Prelude ()

import Game.LambdaHack.Core.Prelude

import           Control.Concurrent.Async
import           Data.Char (chr, ord)
import qualified Data.Map.Strict as M
import qualified UI.HSCurses.Curses as C
import qualified UI.HSCurses.CursesHelper as C

import           Game.LambdaHack.Client.ClientOptions
import           Game.LambdaHack.Client.UI.Content.Screen
import           Game.LambdaHack.Client.UI.Frame
import           Game.LambdaHack.Client.UI.Frontend.Common
import qualified Game.LambdaHack.Client.UI.Key as K
import           Game.LambdaHack.Common.Point
import qualified Game.LambdaHack.Common.PointArray as PointArray
import qualified Game.LambdaHack.Definition.Color as Color

-- | Session data maintained by the frontend.
data FrontendSession = FrontendSession
  { swin    :: C.Window  -- ^ the window to draw to
  , sstyles :: M.Map (Color.Color, Color.Color) C.CursesStyle
      -- ^ map from fore/back colour pairs to defined curses styles
  }

-- | The name of the frontend.
frontendName :: String
frontendName = "curses"

-- | Starts the main program loop using the frontend input and output.
startup :: ScreenContent -> ClientOptions -> IO RawFrontend
startup coscreen _soptions = do
  C.start
  void $ C.cursSet C.CursorInvisible
  let s = [ ((fg, bg), C.Style (toFColor fg) (toBColor bg))
          | -- Almost no more color combinations possible: 15*4, 64 is max.
            fg <- Color.legalFgCol
          , bg <- [Color.Black, Color.Blue, Color.White, Color.BrBlack] ]
  nr <- C.colorPairs
  when (nr < length s) $
    C.end >> error ("terminal has too few color pairs" `showFailure` nr)
  let (ks, vs) = unzip s
  ws <- C.convertStyles vs
  let swin = C.stdScr
      sstyles = M.fromDistinctAscList (zip ks ws)
      sess = FrontendSession{..}
  rf <- createRawFrontend coscreen (display coscreen sess) shutdown
  let storeKeys :: IO ()
      storeKeys = do
        K.KM{..} <- keyTranslate <$> C.getKey C.refresh
        saveKMP rf modifier key originPoint
        storeKeys
  void $ async storeKeys
  return $! rf

shutdown :: IO ()
shutdown = C.end

-- | Output to the screen via the frontend.
display :: ScreenContent
        -> FrontendSession
        -> SingleFrame
        -> IO ()
display coscreen FrontendSession{..} SingleFrame{singleFrame} = do
  -- let defaultStyle = C.defaultCursesStyle
  -- Terminals with white background require this:
  let defaultStyle = sstyles M.! (Color.defFG, Color.Black)
  C.erase
  C.setStyle defaultStyle
  -- We need to remove the last character from the status line,
  -- because otherwise it would overflow a standard size xterm window,
  -- due to the curses historical limitations.
  let sf = chunk $ map Color.attrCharFromW32
                 $ PointArray.toListA singleFrame
      level = init sf ++ [init $ last sf]
      nm = zip [0..] $ map (zip [0..]) level
      chunk [] = []
      chunk l = let (ch, r) = splitAt (rwidth coscreen) l
                in ch : chunk r
  sequence_ [ C.setStyle (M.findWithDefault defaultStyle acAttr2 sstyles)
              >> C.mvWAddStr swin y x [acChar]
            | (y, line) <- nm
            , (x, Color.AttrChar{acAttr=Color.Attr{..}, ..}) <- line
            , let acAttr2 = case bg of
                    Color.HighlightNone -> (fg, Color.Black)
                    Color.HighlightGreen -> (fg, Color.Black)  -- too few bgs
                    Color.HighlightBlue ->
                      if fg /= Color.Blue
                      then (fg, Color.Blue)
                      else (fg, Color.BrBlack)
                    Color.HighlightGrey ->
                      if fg /= Color.BrBlack
                      then (fg, Color.BrBlack)
                      else (fg, Color.defFG)
                    Color.HighlightWhite -> (fg, Color.Black)
                    Color.HighlightMagenta -> (fg, Color.Black)
                    Color.HighlightRed ->
                      if fg /= Color.Red
                      then (fg, Color.Red)
                      else (fg, Color.defFG)
                    Color.HighlightYellow -> (Color.Black, Color.defFG)
                    Color.HighlightYellowAim -> (Color.Black, Color.defFG)
                    Color.HighlightRedAim ->
                      if fg /= Color.Red
                      then (fg, Color.Red)
                      else (fg, Color.defFG)
                    Color.HighlightNoneCursor -> (fg, Color.Black) ]
  C.refresh

keyTranslate :: C.Key -> K.KM
keyTranslate e = (\(key, modifier) -> K.KM modifier key) $
  case e of
    C.KeyChar '\ESC' -> (K.Esc,     K.NoModifier)
    C.KeyExit        -> (K.Esc,     K.NoModifier)
    C.KeyChar '\n'   -> (K.Return,  K.NoModifier)
    C.KeyChar '\r'   -> (K.Return,  K.NoModifier)
    C.KeyEnter       -> (K.Return,  K.NoModifier)
    C.KeyChar ' '    -> (K.Space,   K.NoModifier)
    C.KeyChar '\t'   -> (K.Tab,     K.NoModifier)
    C.KeyBTab        -> (K.BackTab, K.NoModifier)
    C.KeyBackspace   -> (K.BackSpace, K.NoModifier)
    C.KeyUp          -> (K.Up,      K.NoModifier)
    C.KeyDown        -> (K.Down,    K.NoModifier)
    C.KeyLeft        -> (K.Left,    K.NoModifier)
    C.KeySLeft       -> (K.Left,    K.NoModifier)
    C.KeyRight       -> (K.Right,   K.NoModifier)
    C.KeySRight      -> (K.Right,   K.NoModifier)
    C.KeyHome        -> (K.Home,    K.NoModifier)
    C.KeyEnd         -> (K.End,     K.NoModifier)
    C.KeyPPage       -> (K.PgUp,    K.NoModifier)
    C.KeyNPage       -> (K.PgDn,    K.NoModifier)
    C.KeyBeg         -> (K.Begin,   K.NoModifier)
    C.KeyB2          -> (K.Begin,   K.NoModifier)
    C.KeyClear       -> (K.Begin,   K.NoModifier)
    C.KeyIC          -> (K.Insert,  K.NoModifier)
    -- No KP_ keys; see <https://github.com/skogsbaer/hscurses/issues/10>
    C.KeyChar c
      -- This case needs to be considered after Tab, since, apparently,
      -- on some terminals ^i == Tab and Tab is more important for us.
      | ord '\^A' <= ord c && ord c <= ord '\^Z' ->
        -- Alas, only lower-case letters.
        (K.Char $ chr $ ord c - ord '\^A' + ord 'a', K.Control)
        -- Movement keys are more important than leader picking,
        -- so disabling the latter and interpreting the keypad numbers
        -- as movement:
      | c `elem` ['1'..'9'] -> (K.KP c, K.NoModifier)
      | otherwise           -> (K.Char c, K.NoModifier)
    _                       -> (K.Unknown (show e), K.NoModifier)

toFColor :: Color.Color -> C.ForegroundColor
toFColor Color.Black     = C.BlackF
toFColor Color.Red       = C.DarkRedF
toFColor Color.Green     = C.DarkGreenF
toFColor Color.Brown     = C.BrownF
toFColor Color.Blue      = C.DarkBlueF
toFColor Color.Magenta   = C.PurpleF
toFColor Color.Cyan      = C.DarkCyanF
toFColor Color.White     = C.WhiteF
toFColor Color.AltWhite  = C.WhiteF
toFColor Color.BrBlack   = C.GreyF
toFColor Color.BrRed     = C.RedF
toFColor Color.BrGreen   = C.GreenF
toFColor Color.BrYellow  = C.YellowF
toFColor Color.BrBlue    = C.BlueF
toFColor Color.BrMagenta = C.MagentaF
toFColor Color.BrCyan    = C.CyanF
toFColor Color.BrWhite   = C.BrightWhiteF

toBColor :: Color.Color -> C.BackgroundColor
toBColor Color.Black     = C.BlackB
toBColor Color.Red       = C.DarkRedB
toBColor Color.Green     = C.DarkGreenB
toBColor Color.Brown     = C.BrownB
toBColor Color.Blue      = C.DarkBlueB
toBColor Color.Magenta   = C.PurpleB
toBColor Color.Cyan      = C.DarkCyanB
toBColor Color.White     = C.WhiteB
toBColor _               = C.BlackB  -- a limitation of curses