summaryrefslogtreecommitdiff
path: root/Omni/Log/Concurrent.hs
blob: 5187367517e214714cc7fe8b6ce49014e2bcbb1d (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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
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 <| \linesMap ->
        case findFirstFree linesMap of
          Nothing -> (linesMap, Nothing)
          Just lineNum ->
            let status = BuildStatus ns "" Building
                linesMap' = Map.insert lineNum (Just status) linesMap
             in (linesMap', 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 <| \linesMap ->
          Map.adjust (fmap (\bs -> bs {bsLastOutput = output})) lineNum linesMap

releaseLine :: LineManager -> Maybe Int -> BuildState -> IO ()
releaseLine LineManager {..} mLineNum buildState =
  case mLineNum of
    Nothing -> pure ()
    Just lineNum -> do
      modifyIORef' lmLines <| \linesMap ->
        Map.insert lineNum Nothing linesMap

      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 buildState of
              Success -> "✓"
              Failed -> "✗"
              Building -> "…"
        IO.hPutStr IO.stderr statusChar
        IO.hFlush IO.stderr

        ANSI.hRestoreCursor IO.stderr