packages feed

hs-term-emulator-0.1.0.0: src/System/Terminal/Emulator/Term.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}

module System.Terminal.Emulator.Term
  ( -- * Types
    Term,
    mkTerm,

    -- * Direct 'Term' Lenses
    termAttrs,
    cursorPos,
    cursorState,
    modeWrap,
    insertMode,
    altScreenActive,
    numCols,
    numRows,
    keyboardState,
    scrollTop,
    scrollBottom,
    scrollBackLines,
    numScrollBackLines,
    termScreen,
    termAlt,
    windowTitle,

    -- * Direct 'CursorState' Lenses
    wrapNext,
    origin,

    -- * Helper 'Term' Lenses
    cursorLine,
    activeScreen,

    -- * Misc
    addScrollBackLines,
    vuIndex,
    termGetKeyboardState,
  )
where

import Control.Category ((>>>))
import Control.Exception (assert)
import Control.Lens
import Data.Text (Text)
import qualified Data.Vector.Unboxed as VU
import System.Terminal.Emulator.Attrs (Attrs, blankAttrs)
import System.Terminal.Emulator.KeyboardInput (KeyboardState, initialKeyboardState)
import System.Terminal.Emulator.TermLines (TermLine, TermLines)
import qualified System.Terminal.Emulator.TermLines as TL
import Prelude hiding (lines)

data CursorState = CursorState
  { cursorState_WrapNext :: !Bool,
    cursorState_Origin :: !Bool
  }
  deriving (Show, Eq, Ord)

data CursorPos = CursorPos !Int !Int
  deriving (Show, Eq, Ord)

data Term = Term
  { term_Attrs :: !Attrs,
    -- | (line, column)
    term_CursorPos :: !CursorPos,
    term_CursorState :: !CursorState,
    -- | Set using Wraparound Mode (DECAWM)
    term_ModeWrap :: !Bool,
    -- | Set using Insert/Replace Mode (IRM)
    term_InsertMode :: !Bool,
    term_AltScreenActive :: !Bool,
    term_NumCols :: !Int,
    term_NumRows :: !Int,
    term_KeyboardState :: !KeyboardState,
    -- | Row index of the top of the scroll region
    term_ScrollTop :: !Int,
    -- | Row index of the bottom of the scroll region
    term_ScrollBottom :: !Int,
    -- | Scroll back lines of the Main screen
    term_ScrollBackLines :: !TermLines,
    -- | Maximum scroll back lines to be saved
    term_NumScrollBackLines :: !Int,
    -- | Main screen. This is always the size of the terminal.
    term_Screen :: !TermLines,
    -- | Alternate screen. This is always the size of the terminal.
    --
    -- The Alternate screen does not have any scroll back lines
    --
    -- See also:
    -- <https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer>
    term_Alt :: !TermLines,
    term_WindowTitle :: !Text
  }
  deriving (Show, Eq, Ord)

-- | Create a new blank Terminal with the given size @(width, height)@
mkTerm :: (Int, Int) -> Term
mkTerm (width, height) =
  Term
    { term_Attrs = blankAttrs,
      term_CursorPos = CursorPos 0 0,
      term_CursorState =
        CursorState
          { cursorState_WrapNext = False,
            cursorState_Origin = False
          },
      term_ModeWrap = True,
      term_InsertMode = False,
      term_AltScreenActive = False,
      term_NumCols = width,
      term_NumRows = height,
      term_KeyboardState = initialKeyboardState,
      term_ScrollTop = 0,
      term_ScrollBottom = height - 1,
      term_ScrollBackLines = TL.empty,
      term_NumScrollBackLines = 1000,
      term_Screen = TL.replicate height (VU.replicate width ((' ', 0))),
      term_Alt = TL.replicate height (VU.replicate width ((' ', 0))),
      term_WindowTitle = "hs-term"
    }

-----------------------------------------------------------------------
-- Direct 'Term' Lenses
-----------------------------------------------------------------------

termAttrs :: Lens' Term Attrs
termAttrs = lens term_Attrs (\term newVal -> term {term_Attrs = newVal})

-- | Cursor line is always in the range [0..numRows-1]
--
-- Cursor col is always in the range [0..numCols-1]
cursorPos :: Lens' Term (Int, Int)
cursorPos = lens getter setter
  where
    getter :: Term -> (Int, Int)
    getter term = let CursorPos row col = term_CursorPos term in (row, col)
    setter :: Term -> (Int, Int) -> Term
    setter term (newRow, newCol) =
      assert (newCol >= minX) $
        assert (newCol <= maxX) $
          assert (newRow >= minY) $
            assert (newRow <= maxY) $
              term {term_CursorPos = CursorPos newRow newCol}
      where
        minX = 0
        maxX = (term ^. numCols) - 1
        minY = 0
        maxY = (term ^. numRows) - 1

cursorState :: Lens' Term CursorState
cursorState = lens term_CursorState (\term newVal -> term {term_CursorState = newVal})

-- | Wraparound Mode (DECAWM)
modeWrap :: Lens' Term Bool
modeWrap = lens term_ModeWrap (\term newVal -> term {term_ModeWrap = newVal})

-- | Insert/Replace Mode (IRM)
insertMode :: Lens' Term Bool
insertMode = lens term_InsertMode (\term newVal -> term {term_InsertMode = newVal})

altScreenActive :: Lens' Term Bool
altScreenActive = lens term_AltScreenActive (\term newVal -> term {term_AltScreenActive = newVal})

numCols :: Lens' Term Int
numCols = lens term_NumCols (\term newVal -> term {term_NumCols = newVal})

numRows :: Lens' Term Int
numRows = lens term_NumRows (\term newVal -> term {term_NumRows = newVal})

keyboardState :: Lens' Term KeyboardState
keyboardState = lens term_KeyboardState (\term newVal -> term {term_KeyboardState = newVal})

scrollTop :: Lens' Term Int
scrollTop = lens term_ScrollTop (\term newVal -> term {term_ScrollTop = newVal})

scrollBottom :: Lens' Term Int
scrollBottom = lens term_ScrollBottom (\term newVal -> term {term_ScrollBottom = newVal})

scrollBackLines :: Lens' Term TermLines
scrollBackLines = lens term_ScrollBackLines (\term newVal -> term {term_ScrollBackLines = newVal})

numScrollBackLines :: Lens' Term Int
numScrollBackLines = lens term_NumScrollBackLines (\term newVal -> term {term_NumScrollBackLines = newVal})

termScreen :: Lens' Term TermLines
termScreen = lens term_Screen (\term newVal -> term {term_Screen = newVal})

termAlt :: Lens' Term TermLines
termAlt = lens term_Alt (\term newVal -> term {term_Alt = newVal})

windowTitle :: Lens' Term Text
windowTitle = lens term_WindowTitle (\term newWindowTitle -> term {term_WindowTitle = newWindowTitle})

-----------------------------------------------------------------------
-- Direct 'Term' Lenses
-----------------------------------------------------------------------

wrapNext :: Lens' CursorState Bool
wrapNext = lens cursorState_WrapNext (\cs newWrapNext -> cs {cursorState_WrapNext = newWrapNext})

origin :: Lens' CursorState Bool
origin = lens cursorState_Origin (\cs newOrigin -> cs {cursorState_Origin = newOrigin})

-----------------------------------------------------------------------
-- Helper 'Term' Lenses
-----------------------------------------------------------------------

-- | A lens to the line where the cursor currently is
cursorLine :: Lens' Term TermLine
cursorLine = lens getter setter
  where
    getter :: Term -> TermLine
    getter term = term ^. activeScreen . TL.vIndex (term ^. cursorPos . _1)
    setter :: Term -> TermLine -> Term
    setter term newTermLine = ((activeScreen . TL.vIndex (term ^. cursorPos . _1)) .~ newTermLine) term

-- | Either the main screen or the alternate screen (depending on which is
-- active)
activeScreen :: Lens' Term TermLines
activeScreen = lens getter setter
  where
    getter :: Term -> TermLines
    getter term = (if term_AltScreenActive term then term_Alt else term_Screen) term
    setter :: Term -> TermLines -> Term
    setter term newLines = (if term_AltScreenActive term then term {term_Alt = newLines} else term {term_Screen = newLines})

-----------------------------------------------------------------------

termGetKeyboardState :: Term -> KeyboardState
termGetKeyboardState = term_KeyboardState

vuIndex :: VU.Unbox a => Int -> Lens' (VU.Vector a) a
vuIndex i = lens getter setter
  where
    getter :: VU.Unbox a => VU.Vector a -> a
    getter v = assert (i >= 0 && i <= VU.length v - 1) $ v VU.! i
    setter :: VU.Unbox a => VU.Vector a -> a -> VU.Vector a
    setter v val = assert (i >= 0 && i <= VU.length v - 1) $ v VU.// [(i, val)]

addScrollBackLines :: TermLines -> Term -> Term
addScrollBackLines newLines term =
  (scrollBackLines %~ ((<> newLines) >>> TL.takeLast (term ^. numScrollBackLines))) term