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
|
{-# 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"
-- Check if we support ANSI
let supportsANSI = case (term, area) of
(Just "dumb", _) -> False
(_, Just "Live") -> False -- production logs
(Nothing, _) -> False
_ -> True
-- Get terminal size, catching exceptions from stdin issues
mSize <- 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 <> "..."
|