summaryrefslogtreecommitdiff
path: root/Omni/Log/Terminal.hs
blob: 0d2ca7a3e2429b15d54964bc6a1cdf353a1cca84 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoImplicitPrelude #-}

-- | Terminal detection and output mode selection
module Omni.Log.Terminal
  ( TerminalInfo (..),
    OutputMode (..),
    detectTerminal,
    truncateToWidth,
  )
where

import Alpha
import qualified Control.Exception as Exception
import qualified Data.Text as Text
import qualified System.Console.ANSI as ANSI
import qualified System.Environment as Env

data OutputMode
  = MultiLine -- Wide terminals (≥80 cols) - reserved lines per namespace
  | SingleLine -- Narrow terminals (<80 cols) - rotating single line
  deriving (Eq, Show)

data TerminalInfo = TerminalInfo
  { tiWidth :: Int,
    tiHeight :: Int,
    tiMode :: OutputMode,
    tiSupportsANSI :: Bool
  }
  deriving (Eq, Show)

detectTerminal :: IO TerminalInfo
detectTerminal = do
  term <- Env.lookupEnv "TERM"
  area <- Env.lookupEnv "AREA"
  noColor <- Env.lookupEnv "NO_COLOR"

  -- Check if we support ANSI
  let supportsANSI = case (term, area, noColor) of
        (_, _, Just _) -> False -- NO_COLOR set
        (Just "dumb", _, _) -> False
        (_, Just "Live", _) -> False -- production logs
        (Nothing, _, _) -> False
        _ -> True

  -- Get terminal size, catching exceptions from stdin issues
  -- When NO_COLOR is set or ANSI is not supported, skip terminal size detection
  -- to avoid outputting escape codes
  mSize <- case supportsANSI of
    False -> pure Nothing -- Skip if no ANSI support
    True -> Exception.catch ANSI.getTerminalSize <| \(_ :: Exception.IOException) -> pure Nothing
  let (width, height) = case mSize of
        Just (h, w) -> (w, h)
        Nothing -> (80, 24) -- sensible default

  -- Determine mode based on terminal width
  let mode
        | not supportsANSI = SingleLine -- Fallback to single line for dumb terminals
        | width < 80 = SingleLine
        | otherwise = MultiLine

  pure
    TerminalInfo
      { tiWidth = width,
        tiHeight = height,
        tiMode = mode,
        tiSupportsANSI = supportsANSI
      }

-- | Truncate text to fit width with ellipsis
truncateToWidth :: Int -> Text -> Text
truncateToWidth maxWidth text
  | Text.length text <= maxWidth = text
  | maxWidth <= 3 = Text.take maxWidth text
  | otherwise = Text.take (maxWidth - 3) text <> "..."