From 335a1a4692dc921d6b0b6fab0e93c30063b622d5 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Sat, 15 Nov 2025 08:04:26 -0500 Subject: feat(bild): adaptive terminal output with width detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- Omni/Log/Terminal.hs | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 Omni/Log/Terminal.hs (limited to 'Omni/Log/Terminal.hs') 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 <> "..." -- cgit v1.2.3