diff options
| author | Ben Sima <ben@bsima.me> | 2025-11-15 08:04:26 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bsima.me> | 2025-11-15 08:04:26 -0500 |
| commit | 335a1a4692dc921d6b0b6fab0e93c30063b622d5 (patch) | |
| tree | c339a247e12dad2087604af5d5587bb3867e0fb8 /Omni/Log/Terminal.hs | |
| parent | c435b9a8a87f558cc76c761d6d4ba7a9ae69740c (diff) | |
feat(bild): adaptive terminal output with width detection
- Add Omni/Log/Terminal module to detect terminal capabilities -
Implement 3 output modes: RichMultiLine (≥80 cols), SingleLine
(40-79),
SimpleFallback (<40 or dumb terminals)
- Terminal width auto-detection via System.Console.ANSI.getTerminalSize
- Text truncation with ellipsis to prevent line wrapping - Manual
override via BILD_OUTPUT_MODE env var (simple|single|rich|auto)
Fixes UI corruption on small terminals (mobile SSH, narrow windows).
Wide terminals keep existing multi-line behavior but with truncation.
Design doc: _/llm/CONCURRENT_LOG_DESIGN.md
Diffstat (limited to 'Omni/Log/Terminal.hs')
| -rw-r--r-- | Omni/Log/Terminal.hs | 78 |
1 files changed, 78 insertions, 0 deletions
diff --git a/Omni/Log/Terminal.hs b/Omni/Log/Terminal.hs new file mode 100644 index 0000000..1230eb3 --- /dev/null +++ b/Omni/Log/Terminal.hs @@ -0,0 +1,78 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE NoImplicitPrelude #-} + +-- | Terminal detection and output mode selection +module Omni.Log.Terminal + ( TerminalInfo (..), + OutputMode (..), + detectTerminal, + truncateToWidth, + ) +where + +import Alpha +import qualified Data.Text as Text +import qualified System.Console.ANSI as ANSI +import qualified System.Environment as Env + +data OutputMode + = RichMultiLine -- Wide terminals (≥80 cols) + | SingleLine -- Narrow terminals (40-79 cols) + | SimpleFallback -- Very narrow (<40) or dumb terminals + 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" + modeOverride <- Env.lookupEnv "BILD_OUTPUT_MODE" + + -- 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 + mSize <- ANSI.getTerminalSize + let (width, height) = case mSize of + Just (h, w) -> (w, h) + Nothing -> (80, 24) -- sensible default + + -- Determine mode + let autoMode + | not supportsANSI = SimpleFallback + | width < 40 = SimpleFallback + | width < 80 = SingleLine + | otherwise = RichMultiLine + + -- Allow manual override + let mode = case modeOverride of + Just "simple" -> SimpleFallback + Just "single" -> SingleLine + Just "rich" -> RichMultiLine + _ -> autoMode + + 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 <> "..." |
