From 4c4dc3a991ffde0aa821f8669024787fb65635ba Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Fri, 14 Nov 2025 13:33:39 -0500 Subject: Create Omni/Log/Concurrent module for multi-line output - Implement LineManager abstraction with IORef state - Line reservation/update/release functions - ANSI cursor positioning for concurrent updates - Terminal capability detection (ANSI vs dumb) - Graceful fallback for non-ANSI terminals Tasks: t-1a1DzES, t-1a1DGY0, t-1a1DOev, t-1a1DVM5 --- .tasks/tasks.jsonl | 12 ++-- Omni/Log/Concurrent.hs | 155 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 6 deletions(-) create mode 100644 Omni/Log/Concurrent.hs diff --git a/.tasks/tasks.jsonl b/.tasks/tasks.jsonl index de02b86..5bd93c2 100644 --- a/.tasks/tasks.jsonl +++ b/.tasks/tasks.jsonl @@ -72,12 +72,12 @@ {"taskCreatedAt":"2025-11-14T18:19:33.701736325Z","taskDependencies":[],"taskId":"t-1a0OVBs","taskNamespace":null,"taskParent":"t-19ZF6A8","taskStatus":"Done","taskTitle":"Add mapConcurrentlyBounded helper using QSemN","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:20:20.979870628Z"} {"taskCreatedAt":"2025-11-14T18:19:37.810028305Z","taskDependencies":[],"taskId":"t-1a16ame","taskNamespace":null,"taskParent":"t-19ZF6A8","taskStatus":"Done","taskTitle":"Refactor build function to extract buildTarget worker","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:20:58.231039244Z"} {"taskCreatedAt":"2025-11-14T18:19:45.688391211Z","taskDependencies":[],"taskId":"t-1a1DdSB","taskNamespace":null,"taskParent":"t-19ZF6A8","taskStatus":"Done","taskTitle":"Replace forM with mapConcurrentlyBounded in build","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:20:58.290149792Z"} -{"taskCreatedAt":"2025-11-14T18:19:45.716079624Z","taskDependencies":[],"taskId":"t-1a1Dl5c","taskNamespace":null,"taskParent":"t-19ZF6A8","taskStatus":"InProgress","taskTitle":"Test basic parallel builds without UI changes","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:21:03.785801206Z"} -{"taskCreatedAt":"2025-11-14T18:19:45.744631636Z","taskDependencies":[],"taskId":"t-1a1DsvI","taskNamespace":null,"taskParent":"t-19ZF6A8","taskStatus":"Open","taskTitle":"Research ansi-terminal and design LineManager API","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:19:45.744631636Z"} -{"taskCreatedAt":"2025-11-14T18:19:45.772108017Z","taskDependencies":[],"taskId":"t-1a1DzES","taskNamespace":null,"taskParent":"t-19ZF6A8","taskStatus":"Open","taskTitle":"Create Omni/Log/Concurrent.hs module with LineManager","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:19:45.772108017Z"} -{"taskCreatedAt":"2025-11-14T18:19:45.800202144Z","taskDependencies":[],"taskId":"t-1a1DGY0","taskNamespace":null,"taskParent":"t-19ZF6A8","taskStatus":"Open","taskTitle":"Implement line reservation and release logic","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:19:45.800202144Z"} -{"taskCreatedAt":"2025-11-14T18:19:45.82813327Z","taskDependencies":[],"taskId":"t-1a1DOev","taskNamespace":null,"taskParent":"t-19ZF6A8","taskStatus":"Open","taskTitle":"Implement concurrent line update with ANSI codes","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:19:45.82813327Z"} -{"taskCreatedAt":"2025-11-14T18:19:45.857123437Z","taskDependencies":[],"taskId":"t-1a1DVM5","taskNamespace":null,"taskParent":"t-19ZF6A8","taskStatus":"Open","taskTitle":"Add terminal capability detection","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:19:45.857123437Z"} +{"taskCreatedAt":"2025-11-14T18:19:45.716079624Z","taskDependencies":[],"taskId":"t-1a1Dl5c","taskNamespace":null,"taskParent":"t-19ZF6A8","taskStatus":"Done","taskTitle":"Test basic parallel builds without UI changes","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:31:57.019839638Z"} +{"taskCreatedAt":"2025-11-14T18:19:45.744631636Z","taskDependencies":[],"taskId":"t-1a1DsvI","taskNamespace":null,"taskParent":"t-19ZF6A8","taskStatus":"Done","taskTitle":"Research ansi-terminal and design LineManager API","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:32:29.399532791Z"} +{"taskCreatedAt":"2025-11-14T18:19:45.772108017Z","taskDependencies":[],"taskId":"t-1a1DzES","taskNamespace":null,"taskParent":"t-19ZF6A8","taskStatus":"Done","taskTitle":"Create Omni/Log/Concurrent.hs module with LineManager","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:33:02.794492847Z"} +{"taskCreatedAt":"2025-11-14T18:19:45.800202144Z","taskDependencies":[],"taskId":"t-1a1DGY0","taskNamespace":null,"taskParent":"t-19ZF6A8","taskStatus":"Done","taskTitle":"Implement line reservation and release logic","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:33:02.855747669Z"} +{"taskCreatedAt":"2025-11-14T18:19:45.82813327Z","taskDependencies":[],"taskId":"t-1a1DOev","taskNamespace":null,"taskParent":"t-19ZF6A8","taskStatus":"Done","taskTitle":"Implement concurrent line update with ANSI codes","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:33:02.915807677Z"} +{"taskCreatedAt":"2025-11-14T18:19:45.857123437Z","taskDependencies":[],"taskId":"t-1a1DVM5","taskNamespace":null,"taskParent":"t-19ZF6A8","taskStatus":"Done","taskTitle":"Add terminal capability detection","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:33:02.975985146Z"} {"taskCreatedAt":"2025-11-14T18:19:45.886073324Z","taskDependencies":[],"taskId":"t-1a1E3j1","taskNamespace":null,"taskParent":"t-19ZF6A8","taskStatus":"Open","taskTitle":"Thread LineManager through build/nixBuild functions","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:19:45.886073324Z"} {"taskCreatedAt":"2025-11-14T18:19:45.914626247Z","taskDependencies":[],"taskId":"t-1a1EaJy","taskNamespace":null,"taskParent":"t-19ZF6A8","taskStatus":"Open","taskTitle":"Create runWithLineManager and logsToLine functions","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:19:45.914626247Z"} {"taskCreatedAt":"2025-11-14T18:19:45.94320795Z","taskDependencies":[],"taskId":"t-1a1Eiay","taskNamespace":null,"taskParent":"t-19ZF6A8","taskStatus":"Open","taskTitle":"Test parallel builds with ANSI multi-line output","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:19:45.94320795Z"} diff --git a/Omni/Log/Concurrent.hs b/Omni/Log/Concurrent.hs new file mode 100644 index 0000000..2a46df5 --- /dev/null +++ b/Omni/Log/Concurrent.hs @@ -0,0 +1,155 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE NoImplicitPrelude #-} + +-- | Concurrent logging with multi-line output support +module Omni.Log.Concurrent + ( LineManager, + BuildState (..), + withLineManager, + reserveLine, + updateLine, + releaseLine, + ) +where + +import Alpha +import Data.IORef (IORef, atomicModifyIORef', modifyIORef', newIORef, readIORef) +import qualified Data.Map as Map +import qualified Data.Text as Text +import Omni.Namespace (Namespace) +import qualified Omni.Namespace as Namespace +import qualified System.Console.ANSI as ANSI +import qualified System.Environment as Env +import qualified System.IO as IO + +data BuildState = Building | Success | Failed + deriving (Eq, Show) + +data BuildStatus = BuildStatus + { bsTarget :: Namespace, + bsLastOutput :: Text, + bsState :: BuildState + } + +data LineManager = LineManager + { lmLines :: IORef (Map Int (Maybe BuildStatus)), + lmMaxLines :: Int, + lmCurrentLine :: IORef Int, + lmSupportsANSI :: Bool + } + +withLineManager :: Int -> (LineManager -> IO a) -> IO a +withLineManager maxLines action = do + supportsANSI <- checkANSISupport + + if not supportsANSI + then do + linesRef <- newIORef Map.empty + currentRef <- newIORef 0 + action + LineManager + { lmLines = linesRef, + lmMaxLines = 1, + lmCurrentLine = currentRef, + lmSupportsANSI = False + } + else do + replicateM_ maxLines (IO.hPutStrLn IO.stderr "") + ANSI.hCursorUp IO.stderr maxLines + + linesRef <- newIORef (Map.fromList [(i, Nothing) | i <- [0 .. maxLines - 1]]) + currentRef <- newIORef maxLines + + result <- + action + LineManager + { lmLines = linesRef, + lmMaxLines = maxLines, + lmCurrentLine = currentRef, + lmSupportsANSI = True + } + + ANSI.hCursorDown IO.stderr maxLines + pure result + +checkANSISupport :: IO Bool +checkANSISupport = do + term <- Env.lookupEnv "TERM" + area <- Env.lookupEnv "AREA" + pure <| case (term, area) of + (Just "dumb", _) -> False + (_, Just "Live") -> False + (Nothing, _) -> False + _ -> True + +reserveLine :: LineManager -> Namespace -> IO (Maybe Int) +reserveLine LineManager {..} ns = + if not lmSupportsANSI + then pure Nothing + else + atomicModifyIORef' lmLines <| \lines -> + case findFirstFree lines of + Nothing -> (lines, Nothing) + Just lineNum -> + let status = BuildStatus ns "" Building + lines' = Map.insert lineNum (Just status) lines + in (lines', Just lineNum) + where + findFirstFree :: Map Int (Maybe BuildStatus) -> Maybe Int + findFirstFree m = + Map.toList m + |> filter (\(_, mbs) -> isNothing mbs) + |> map fst + |> listToMaybe + +updateLine :: LineManager -> Maybe Int -> Namespace -> Text -> IO () +updateLine LineManager {..} mLineNum ns output = + if not lmSupportsANSI + then do + IO.hPutStr IO.stderr (Text.unpack <| output <> "\n") + IO.hFlush IO.stderr + else case mLineNum of + Nothing -> pure () + Just lineNum -> do + currentLine <- readIORef lmCurrentLine + + ANSI.hSaveCursor IO.stderr + ANSI.hSetCursorColumn IO.stderr 0 + + let linesToMove = currentLine - lineNum + when (linesToMove > 0) <| ANSI.hCursorUp IO.stderr linesToMove + when (linesToMove < 0) <| ANSI.hCursorDown IO.stderr (abs linesToMove) + + ANSI.hClearLine IO.stderr + IO.hPutStr IO.stderr (Text.unpack output) + IO.hFlush IO.stderr + + ANSI.hRestoreCursor IO.stderr + + modifyIORef' lmLines <| \lines -> + Map.adjust (fmap (\bs -> bs {bsLastOutput = output})) lineNum lines + +releaseLine :: LineManager -> Maybe Int -> BuildState -> IO () +releaseLine LineManager {..} mLineNum state = + case mLineNum of + Nothing -> pure () + Just lineNum -> do + modifyIORef' lmLines <| \lines -> + Map.insert lineNum Nothing lines + + when lmSupportsANSI <| do + current <- readIORef lmCurrentLine + ANSI.hSaveCursor IO.stderr + ANSI.hSetCursorColumn IO.stderr 0 + ANSI.hCursorUp IO.stderr (current - lineNum) + ANSI.hClearLine IO.stderr + + let statusChar = case state of + Success -> "✓" + Failed -> "✗" + Building -> "…" + IO.hPutStr IO.stderr statusChar + IO.hFlush IO.stderr + + ANSI.hRestoreCursor IO.stderr -- cgit v1.2.3