packages feed

tadka-2.0.0.0: src/Tadka/Internal/Terminal.hs

-- | Terminal-capability detection and the pure resolution of @Auto@ render
-- modes into concrete ones (post-v1 hardening). Detection (the only IO here)
-- gathers capabilities; 'resolveConfig' — pure and total — turns 'ColorAuto',
-- 'UnicodeAuto', and 'HyperlinkAuto' into concrete modes, so
-- 'Tadka.Internal.Render.selectRenderer' stays pure. Explicit modes are always
-- passed through unchanged.
--
-- No compatibility guarantee.
module Tadka.Internal.Terminal
  ( TerminalCaps (..)
  , detectTerminalCaps
  , resolveColor
  , resolveUnicode
  , resolveHyperlink
  , resolveConfig
  ) where

import           Data.Maybe          (isJust, listToMaybe)
import qualified Data.Text           as T
import           System.Environment  (lookupEnv)
import           System.IO           (Handle, hIsTerminalDevice)

import           Tadka.Internal.Config (ColorMode (..), Config, HyperlinkMode (..),
                                        UnicodeMode (..), configColorMode,
                                        configHyperlinkMode, configUnicodeMode,
                                        withColorMode, withHyperlinkMode,
                                        withUnicodeMode)

-- | Detected capabilities of an output destination and environment.
data TerminalCaps = TerminalCaps
  { capIsTerminal      :: Bool   -- ^ the handle is an interactive terminal
  , capNoColor         :: Bool   -- ^ @NO_COLOR@ is present (any value)
  , capForceColor      :: Bool   -- ^ @CLICOLOR_FORCE@ is present and not @"0"@
  , capUnicode         :: Bool   -- ^ the active locale looks UTF-8
  , capNoHyperlink     :: Bool   -- ^ @NO_HYPERLINK@ is present (any value)
  , capForceHyperlink  :: Bool   -- ^ @FORCE_HYPERLINK@ is present and not @"0"@
  }
  deriving (Eq, Show)

-- | Gather capabilities for a handle: TTY status, @NO_COLOR@\/@CLICOLOR_FORCE@,
-- a UTF-8 locale check (@LC_ALL@ > @LC_CTYPE@ > @LANG@, POSIX precedence), and
-- the hyperlink pair below.
--
-- @FORCE_HYPERLINK@ is the convention the @supports-hyperlinks@ package (used
-- by Yarn and other JS CLIs) already established for forcing OSC 8 on.
-- @NO_HYPERLINK@ has no equivalent cross-tool precedent the way @NO_COLOR@
-- does; it is a tadka-local variable that simply mirrors @NO_COLOR@'s shape
-- (any value disables) for consistency and so a future shared convention could
-- slot in without an API change.
detectTerminalCaps :: Handle -> IO TerminalCaps
detectTerminalCaps h = do
  term        <- hIsTerminalDevice h
  noColor     <- isJust <$> lookupEnv "NO_COLOR"
  force       <- maybe False (/= "0") <$> lookupEnv "CLICOLOR_FORCE"
  uni         <- localeIsUtf8
  noHyper     <- isJust <$> lookupEnv "NO_HYPERLINK"
  forceHyper  <- maybe False (/= "0") <$> lookupEnv "FORCE_HYPERLINK"
  pure TerminalCaps { capIsTerminal = term, capNoColor = noColor
                    , capForceColor = force, capUnicode = uni
                    , capNoHyperlink = noHyper, capForceHyperlink = forceHyper }

localeIsUtf8 :: IO Bool
localeIsUtf8 = do
  vals <- traverse lookupEnv ["LC_ALL", "LC_CTYPE", "LANG"]
  let active = listToMaybe [ v | Just v <- vals, not (null v) ]
  pure (maybe False (T.isInfixOf (T.pack "utf") . T.toLower . T.pack) active)

-- | Resolve a colour mode against capabilities. Explicit modes pass through;
-- @Auto@ obeys @NO_COLOR@ (off), then @CLICOLOR_FORCE@ (on), then TTY status.
resolveColor :: TerminalCaps -> ColorMode -> ColorMode
resolveColor _    ColorAlways = ColorAlways
resolveColor _    ColorNever  = ColorNever
resolveColor caps ColorAuto
  | capNoColor caps    = ColorNever
  | capForceColor caps = ColorAlways
  | capIsTerminal caps = ColorAlways
  | otherwise          = ColorNever

-- | Resolve a Unicode mode against capabilities. Explicit modes pass through;
-- @Auto@ becomes 'UnicodeAlways' on a UTF-8 locale, else 'UnicodeAscii'.
resolveUnicode :: TerminalCaps -> UnicodeMode -> UnicodeMode
resolveUnicode _    UnicodeAlways = UnicodeAlways
resolveUnicode _    UnicodeAscii  = UnicodeAscii
resolveUnicode caps UnicodeAuto
  | capUnicode caps = UnicodeAlways
  | otherwise       = UnicodeAscii

-- | Resolve a hyperlink mode against capabilities. Explicit modes pass
-- through; @Auto@ obeys @NO_HYPERLINK@ (off), then @FORCE_HYPERLINK@ (on),
-- then TTY status — the same three-tier shape as 'resolveColor', since both
-- ultimately answer "can this destination usefully show a terminal escape?".
resolveHyperlink :: TerminalCaps -> HyperlinkMode -> HyperlinkMode
resolveHyperlink _    HyperlinkAlways = HyperlinkAlways
resolveHyperlink _    HyperlinkNever  = HyperlinkNever
resolveHyperlink caps HyperlinkAuto
  | capNoHyperlink caps    = HyperlinkNever
  | capForceHyperlink caps = HyperlinkAlways
  | capIsTerminal caps     = HyperlinkAlways
  | otherwise              = HyperlinkNever

-- | Resolve all three @Auto@ modes in a 'Config' to concrete modes.
resolveConfig :: TerminalCaps -> Config -> Config
resolveConfig caps cfg =
    withUnicodeMode   (resolveUnicode   caps (configUnicodeMode   cfg))
  . withColorMode     (resolveColor     caps (configColorMode     cfg))
  . withHyperlinkMode (resolveHyperlink caps (configHyperlinkMode cfg))
  $ cfg