packages feed

concurrent-output 1.2.0 → 1.3.0

raw patch · 7 files changed

+247/−101 lines, 7 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- System.Console.Regions: instance GHC.Classes.Eq System.Console.Regions.Region
- System.Console.Regions: updateRegionListSTM :: ([ConsoleRegionHandle] -> [ConsoleRegionHandle]) -> STM ()
+ System.Console.Regions: RegionContent :: (TVar Text) -> RegionContent
+ System.Console.Regions: RegionContentSTM :: (STM Text) -> RegionContent
+ System.Console.Regions: class Displayable v
+ System.Console.Regions: consoleSize :: TVar (Window Int)
+ System.Console.Regions: consoleWidth :: STM Width
+ System.Console.Regions: data RegionContent
+ System.Console.Regions: instance System.Console.Regions.Displayable (GHC.Conc.Sync.STM Data.Text.Internal.Text)
+ System.Console.Regions: instance System.Console.Regions.Displayable Data.Text.Internal.Text
+ System.Console.Regions: instance System.Console.Regions.Displayable GHC.Base.String
+ System.Console.Regions: newConsoleRegionSTM :: Displayable v => RegionLayout -> v -> STM ConsoleRegionHandle
+ System.Console.Regions: readRegionContent :: ConsoleRegionHandle -> STM Text
+ System.Console.Regions: regionList :: TMVar [ConsoleRegionHandle]
+ System.Console.Regions: toRegionContent :: Displayable v => v -> STM RegionContent
+ System.Console.Regions: type Width = Int
- System.Console.Regions: openConsoleRegionSTM :: RegionLayout -> STM ConsoleRegionHandle
+ System.Console.Regions: openConsoleRegionSTM :: Displayable v => RegionLayout -> v -> STM ConsoleRegionHandle
- System.Console.Regions: setConsoleRegion :: Outputable v => ConsoleRegionHandle -> v -> IO ()
+ System.Console.Regions: setConsoleRegion :: Displayable v => ConsoleRegionHandle -> v -> IO ()
- System.Console.Regions: setConsoleRegionSTM :: Outputable v => ConsoleRegionHandle -> v -> STM ()
+ System.Console.Regions: setConsoleRegionSTM :: Displayable v => ConsoleRegionHandle -> v -> STM ()

Files

CHANGELOG view
@@ -1,3 +1,14 @@+concurrent-output (1.3.0) unstable; urgency=medium++  * The contents of a Region can now be set to a STM Text+    transaction. Their display will be automatically updated whenever the+    transaction's value changes.+  * Removed updateRegionListSTM, and export regionList instead, which is+    more general-purpose.+  * Other improvements to STM interface.++ -- Joey Hess <id@joeyh.name>  Tue, 03 Nov 2015 15:50:47 -0400+ concurrent-output (1.2.0) unstable; urgency=medium    * Avoid crash when not all of a program's output is consumed, as
System/Console/Regions.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE BangPatterns, TupleSections #-}+{-# LANGUAGE BangPatterns, TupleSections, TypeSynonymInstances, FlexibleInstances #-}  -- |  -- Copyright: 2015 Joey Hess <id@joeyh.name>@@ -68,7 +68,8 @@ 	withConsoleRegion, 	openConsoleRegion, 	closeConsoleRegion,-	-- * Output+	-- * Region display+	Displayable(..), 	setConsoleRegion, 	appendConsoleRegion, 	finishConsoleRegion,@@ -78,10 +79,38 @@ 	-- once the transaction completes the console will be updated 	-- a single time to reflect all the changes made. 	openConsoleRegionSTM,+	newConsoleRegionSTM, 	closeConsoleRegionSTM, 	setConsoleRegionSTM, 	appendConsoleRegionSTM,-	updateRegionListSTM,+	-- * STM regions+	--+	-- | The `Displayable` instance for STM text can be used to+	-- make regions that automatically update whenever there's+	-- a change to any of the STM values that they use.+	--+	-- For example, a region that displays the screen size,+	-- and automatically refreshes it:+	--+	-- > import System.Console.Terminal.Size+	-- > import qualified Data.Text as T+	--+	-- > r <- openConsoleRegion Linear s+	-- > setConsoleRegion r $ do+	-- > 	sz <- readTVar consoleSize+	-- > 	return $ T.pack $ unwords+	-- > 		[ "size:"+	-- >		, show (width sz)+	-- > 		, "x"+	-- >		, show (height sz)+	-- > 		]+	-- >+	RegionContent(..),+	readRegionContent,+	consoleSize,+	Width,+	consoleWidth,+	regionList, ) where  import Data.Monoid@@ -120,8 +149,8 @@ -- > bbbbbbbbbbbbbbb -- Linear -- > bbb............       (expanded to multiple lines) -- > ccccccccc...... -- Linear--- > ddd eee fffffff -- [InLine]--- > ffff ggggg.....       (expanded to multiple lines)+-- > ddddeeeefffffff -- [InLine]+-- > fffffggggg.....       (expanded to multiple lines) -- >  data RegionLayout = Linear | InLine ConsoleRegionHandle 	deriving (Eq)@@ -130,43 +159,63 @@ 	deriving (Eq)  data Region = Region-	{ regionContent :: T.Text-	, regionLines :: [T.Text] -- cache+	{ regionContent :: RegionContent+	, regionLines :: TVar [T.Text] -- ^ cache of regionContent 	, regionLayout :: RegionLayout 	, regionChildren :: Maybe [ConsoleRegionHandle] 	} -instance Eq Region where-	a == b = regionContent a == regionContent b-		&& regionLayout a == regionLayout b--type RegionList = TMVar [ConsoleRegionHandle]+data RegionContent+	= RegionContent (TVar T.Text) +	| RegionContentSTM (STM T.Text) --- | A shared global list of regions.+-- | All the regions that are currently displayed on the screen.+--+-- The list is ordered from the bottom of the screen up. Reordering+-- it will change the order in which regions are displayed.+-- It's also fine to remove, duplicate, or add new regions to the list. {-# NOINLINE regionList #-}-regionList :: RegionList+regionList :: TMVar [ConsoleRegionHandle] regionList = unsafePerformIO newEmptyTMVarIO -type Width = Int+-- | On unix systems, this TVar is automatically updated when the+-- terminal is resized.+{-# NOINLINE consoleSize #-}+consoleSize :: TVar (Console.Window Int)+consoleSize = unsafePerformIO $ newTVarIO $ +	Console.Window { Console.width = 80, Console.height = 25} --- | A shared global console width.-{-# NOINLINE consoleWidth #-}-consoleWidth :: TVar Width-consoleWidth = unsafePerformIO (newTVarIO 80)+type Width = Int --- | Updates the list of regions. The list is ordered from the bottom of--- the screen up. Reordering it will change the order in which regions are--- displayed. It's also fine to remove, duplicate, or add new regions to the--- list.-updateRegionListSTM :: ([ConsoleRegionHandle] -> [ConsoleRegionHandle]) -> STM ()-updateRegionListSTM f = -	maybe noop (putTMVar regionList . f) =<< tryTakeTMVar regionList+-- Get the width from the `consoleSize`+consoleWidth :: STM Width+consoleWidth = Console.width <$> readTVar consoleSize  -- | The RegionList TMVar is left empty when `displayConsoleRegions` -- is not running. regionDisplayEnabled :: IO Bool regionDisplayEnabled = atomically $ not <$> isEmptyTMVar regionList +-- | Values that can be displayed in a region.+class Displayable v where+	toRegionContent :: v -> STM RegionContent++instance Displayable String where+	toRegionContent = fromOutput++instance Displayable T.Text where+	toRegionContent = fromOutput++fromOutput :: Outputable v => v -> STM RegionContent+fromOutput v = RegionContent <$> newTVar (toOutput v)++-- | Makes a STM action be run to get the content of a region.+--+-- Any change to the values that action reads will result in an immediate+-- refresh of the display.+instance Displayable (STM T.Text) where+	toRegionContent = pure . RegionContentSTM+ -- | Sets the value to display within a console region. -- -- It's fine for the value to be longer than the terminal is wide,@@ -178,14 +227,16 @@ --  -- Other ANSI escape sequences, especially those doing cursor -- movement, will mess up the layouts of regions. Caveat emptor.-setConsoleRegion :: Outputable v => ConsoleRegionHandle -> v -> IO ()+setConsoleRegion :: Displayable v => ConsoleRegionHandle -> v -> IO () setConsoleRegion h = atomically . setConsoleRegionSTM h -setConsoleRegionSTM :: Outputable v => ConsoleRegionHandle -> v -> STM ()+-- | STM version of `setConsoleRegion`+setConsoleRegionSTM :: Displayable v => ConsoleRegionHandle -> v -> STM () setConsoleRegionSTM (ConsoleRegionHandle tv) v = do 	r <- readTVar tv-	width <- readTVar consoleWidth-	writeTVar tv (modifyRegion r width (const (toOutput v)))+	width <- consoleWidth+	r' <- modifyRegion r width $ const $ toRegionContent v+	writeTVar tv r' 	case regionLayout r of 		Linear -> return () 		InLine p -> refreshParent p@@ -195,26 +246,46 @@ appendConsoleRegion :: Outputable v => ConsoleRegionHandle -> v -> IO () appendConsoleRegion h = atomically . appendConsoleRegionSTM h +-- | STM version of `appendConsoleRegion` appendConsoleRegionSTM :: Outputable v => ConsoleRegionHandle -> v -> STM () appendConsoleRegionSTM (ConsoleRegionHandle tv) v = do 	r <- readTVar tv-	width <- readTVar consoleWidth-	writeTVar tv (modifyRegion r width (<> toOutput v))+	width <- consoleWidth+	r' <- modifyRegion r width $ \rc -> case rc of+		RegionContent cv -> do+			modifyTVar' cv (<> toOutput v)+			return rc+		RegionContentSTM a -> return $ RegionContentSTM $ do+			t <- a+			return (t <> toOutput v)+	writeTVar tv r' 	case regionLayout r of 		Linear -> return () 		InLine p -> refreshParent p -modifyRegion :: Region -> Width -> (T.Text -> T.Text) -> Region-modifyRegion r width f = r { regionContent = c, regionLines = calcRegionLines c width }-  where-	!c = f (regionContent r)+modifyRegion :: Region -> Width -> (RegionContent -> STM RegionContent) -> STM Region+modifyRegion r width f = do+	rc <- f (regionContent r)+	t <- readRegionContent' rc+	writeTVar (regionLines r) (calcRegionLines t width)+	return $ r { regionContent = rc } -resizeRegion :: Width -> ConsoleRegionHandle -> STM Region+-- | Reads the content of a region.+readRegionContent :: ConsoleRegionHandle -> STM T.Text+readRegionContent (ConsoleRegionHandle tv) =+	readRegionContent' . regionContent =<< readTVar tv++readRegionContent' :: RegionContent -> STM T.Text+readRegionContent' (RegionContent t) = readTVar t+readRegionContent' (RegionContentSTM a) = a++resizeRegion :: Width -> ConsoleRegionHandle -> STM (Region, [T.Text]) resizeRegion width (ConsoleRegionHandle tv) = do 	r <- readTVar tv-	let !r' = modifyRegion r width id-	writeTVar tv r'-	return r'+	t <- readRegionContent' (regionContent r)+	let ls = calcRegionLines t width+	writeTVar (regionLines r) ls+	return (r, ls)  -- | Runs the action with a new console region, closing the region when -- the action finishes or on exception.@@ -223,23 +294,16 @@  -- | Opens a new console region for output. openConsoleRegion :: RegionLayout -> IO ConsoleRegionHandle-openConsoleRegion = atomically . openConsoleRegionSTM+openConsoleRegion ly = atomically $ openConsoleRegionSTM ly T.empty  -- | STM version of `openConsoleRegion`. Allows atomically opening multiple--- regions at the same time, which guarantees they are on adjacent lines.+-- regions at the same time, which guarantees they are adjacent. -- -- > [r1, r2, r3] <- atomically $--- >	replicateM 3 (openConsoleRegionSTM Linear)-openConsoleRegionSTM :: RegionLayout -> STM ConsoleRegionHandle-openConsoleRegionSTM ly = do-	width <- readTVar consoleWidth-	let r = Region-		{ regionContent = mempty-		, regionLines = calcRegionLines mempty width-		, regionLayout = ly-		, regionChildren = Nothing-		}-	h <- ConsoleRegionHandle <$> newTVar r+-- >	replicateM 3 (openConsoleRegionSTM Linear T.empty)+openConsoleRegionSTM :: Displayable v => RegionLayout -> v -> STM ConsoleRegionHandle+openConsoleRegionSTM ly v = do+	h <- newConsoleRegionSTM ly T.empty 	case ly of 		Linear -> do 			v <- tryTakeTMVar regionList@@ -249,7 +313,23 @@ 				-- it's not put on any list, and won't display 				Nothing -> return () 		InLine parent -> addChild h parent+	setConsoleRegionSTM h v+	return h +-- | Makes a new region, but does not add it to the display.+newConsoleRegionSTM :: Displayable v => RegionLayout -> v -> STM ConsoleRegionHandle+newConsoleRegionSTM ly v = do+	width <- consoleWidth+	rc <- newTVar mempty+	ls <- newTVar $ calcRegionLines mempty width+	let r = Region+		{ regionContent = RegionContent rc+		, regionLines = ls+		, regionLayout = ly+		, regionChildren = Nothing+		}+	h <- ConsoleRegionHandle <$> newTVar r+	setConsoleRegionSTM h v 	return h  -- | Closes a console region. Once closed, the region is removed from the@@ -296,19 +376,18 @@ refreshParent :: ConsoleRegionHandle -> STM () refreshParent (ConsoleRegionHandle pv) = do 	p <- readTVar pv-	width <- readTVar consoleWidth+	width <- consoleWidth 	case regionChildren p of 		Nothing -> return () 		Just l -> do-			cs <- forM l $ \child@(ConsoleRegionHandle cv) -> do+			cs <- forM l $ \child -> do 				refreshParent child-				regionContent <$> readTVar cv+				readRegionContent child 			let !c = mconcat cs-			let p' = p-				{ regionContent = c-				, regionLines = calcRegionLines c width-				}+			rc <- newTVar c+			let p' = p { regionContent = RegionContent rc } 			writeTVar pv p'+			writeTVar (regionLines p) (calcRegionLines c width)  -- | Handles all display for the other functions in this module. --@@ -344,13 +423,8 @@  trackConsoleWidth :: IO () trackConsoleWidth = do-	let getwidth = do-		v <- Console.size-		case v of-			Nothing -> return ()-			Just (Console.Window _height width) ->-				atomically $ void $-					swapTVar consoleWidth width+	let getwidth = maybe noop (atomically . writeTVar consoleSize)+		=<< Console.size 	getwidth 	installResizeHandler (Just getwidth) @@ -360,19 +434,19 @@ 	| TerminalResize Width 	| EndSignal () -type RegionSnapshot = ([ConsoleRegionHandle], [Region])+type RegionSnapshot = ([ConsoleRegionHandle], [Region], [[T.Text]])  displayThread :: Bool -> TSem -> IO () displayThread isterm endsignal = do-	origwidth <- atomically $ readTVar consoleWidth-	go ([], []) origwidth+	origwidth <- atomically consoleWidth+	go ([], [], []) origwidth   where-	go origsnapshot@(orighandles, origregions) origwidth = do+	go origsnapshot@(orighandles, _origregions, origlines) origwidth = do 		let waitwidthchange = do-			w <- readTVar consoleWidth+			w <- consoleWidth 			if w == origwidth then retry else return w 		change <- atomically $-			(RegionChange <$> regionWaiter origsnapshot)+			(RegionChange <$> regionWaiter origsnapshot origwidth) 				`orElse` 			(RegionChange <$> regionListWaiter origsnapshot) 				`orElse`@@ -382,19 +456,18 @@ 				`orElse` 			(EndSignal <$> waitTSem endsignal) 		case change of-			RegionChange snapshot@(_, newregions) -> do-				when isterm $-					changedLines-						(getlines origregions)-						(getlines newregions)+			RegionChange snapshot@(_, _, newlines) -> do+				when isterm $ do+					changedLines (concat origlines) (concat newlines) 				go snapshot origwidth 			BufferChange buffers -> do-				inAreaAbove isterm (length $ getlines origregions) (getlines origregions) $+				inAreaAbove isterm (length $ concat origlines) (concat origlines) $ 					mapM_ (uncurry emitOutputBuffer) buffers 				go origsnapshot origwidth 			TerminalResize newwidth -> do-				newregions <- atomically $-					mapM (resizeRegion newwidth) orighandles+				(newregions, lls) <- unzip <$> +					atomically (mapM (resizeRegion newwidth) orighandles)+				let newlines = map reverse lls 				when isterm $ do 					-- A resize can leave the cursor 					-- in a fairly undefined location,@@ -403,32 +476,41 @@ 					-- cursor to top of screen, clear 					-- entire screen, and redisplay. 					setCursorPosition 0 0-					inAreaAbove isterm 0 (getlines newregions) $+					inAreaAbove isterm 0 (concat newlines) $ 						return ()-				go (orighandles, newregions) newwidth+				go (orighandles, newregions, newlines) newwidth 			EndSignal () -> return ()-	getlines = concatMap (reverse . regionLines)  readRegions :: [ConsoleRegionHandle] -> STM [Region] readRegions = mapM (\(ConsoleRegionHandle h) -> readTVar h)  -- Wait for any changes to the region list, eg adding or removing a handle. regionListWaiter :: RegionSnapshot -> STM RegionSnapshot-regionListWaiter (orighandles, _origregions) = do+regionListWaiter (orighandles, _origregions, origlines) = do 	handles <- readTMVar regionList 	if handles == orighandles 		then retry 		else do 			rs <- readRegions handles-			return (handles, rs)-		+			return (handles, rs, origlines)+ -- Wait for any changes to any of the regions currently in the region list.-regionWaiter :: RegionSnapshot -> STM RegionSnapshot-regionWaiter (orighandles, origregions) = do+regionWaiter :: RegionSnapshot -> Width -> STM RegionSnapshot+regionWaiter (orighandles, _origregions, origlines) width = do 	rs <- readRegions orighandles-	if rs == origregions-		then retry-		else return (orighandles, rs)+	newlines <- mapM getr (zip rs (origlines ++ repeat [T.empty]))+	unless (newlines /= origlines)+		retry+	return (orighandles, rs, newlines)+  where+	getr (r, ols) = case regionContent r of+		RegionContent _ -> reverse <$> readTVar (regionLines r)+		RegionContentSTM a -> do+			c <- a+			let ls = reverse $ calcRegionLines c width+			when (ls /= ols) $+				writeTVar (regionLines r) ls+			return ls  -- This is not an optimal screen update like curses can do, but it's -- pretty efficient, most of the time!
TODO view
@@ -1,4 +0,0 @@-It would be nice to have regions whose content was determined by running a-STM Text action. Such regions should update whenever the values that action-accessed change. Could be used for such things as spreadsheets.-I have this mostly done in the stmregions git branch, but not quite working.
concurrent-output.cabal view
@@ -1,5 +1,5 @@ Name: concurrent-output-Version: 1.2.0+Version: 1.3.0 Cabal-Version: >= 1.8 License: BSD2 Maintainer: Joey Hess <id@joeyh.name>@@ -28,6 +28,7 @@   demo2.hs   demo3.hs   aptdemo.hs+  stmdemo.hs  Library   GHC-Options: -Wall -fno-warn-tabs -O2
demo.hs view
@@ -1,8 +1,11 @@ import Control.Concurrent.Async import Control.Concurrent+import Control.Concurrent.STM import System.Console.Concurrent import System.Console.Regions import System.Process+import qualified Data.Text as T+import Data.List  main = displayConsoleRegions $ do 	mapConcurrently download [1..5]@@ -25,8 +28,8 @@ 		| c < 1 = finishConsoleRegion r 			(basemsg ++ " done!\n  Took xxx seconds.") 		| otherwise = do-			threadDelay 1000000 			appendConsoleRegion r " ... "+			threadDelay 1000000 			go (c-1) r  ls :: IO ()@@ -37,5 +40,3 @@ 	_ <- waitForProcessConcurrent p 	outputConcurrent "<<< ls is done!\n" 	return ()-	-	
demo2.hs view
@@ -2,11 +2,17 @@ import Control.Concurrent import System.Console.Concurrent import System.Console.Regions+import System.Console.Terminal.Size+import qualified Data.Text as T+import Control.Concurrent.STM+import Control.Applicative+import Data.Time.Clock+import Control.Monad  main = displayConsoleRegions $ mapConcurrently id 	[ spinner 100 1 "Pinwheels!!" setConsoleRegion "/-\\|" (withtitle 1) 	, spinner 100 1 "Bubbles!!!!" setConsoleRegion ".oOo." (withtitle 1)-	, spinner 100 1 "Dots......!" appendConsoleRegion "."  (const (take 1))+	, spinner 100 1 "Dots......!" appendConsoleRegion "."  (const (take 3)) 	, spinner  30 2 "KleisiFish?" setConsoleRegion "  <=<   <=<  " (withtitle 10) 	, spinner  10 9 "Countdowns!" setConsoleRegion 		(reverse [1..10])
+ stmdemo.hs view
@@ -0,0 +1,49 @@+import Control.Concurrent.Async+import Control.Concurrent+import System.Console.Regions+import System.Console.Terminal.Size+import qualified Data.Text as T+import Control.Concurrent.STM+import Control.Applicative+import Data.Time.Clock+import Control.Monad+import Data.Monoid++main :: IO ()+main = void $ displayConsoleRegions $ mapConcurrently id+	[ infoRegion+	, clockRegion+	]++infoRegion :: IO ()+infoRegion = do+	r <- openConsoleRegion Linear+	setConsoleRegion r $ do+	 	sz <- readTVar consoleSize+		regions <- readTMVar regionList+ 		return $ T.pack $ unwords+ 			[ "size:"+			, show (width sz)+ 			, "x"+			, show (height sz)+			, "regions: "+			, show (length regions)+ 			]++timeDisplay :: TVar UTCTime -> STM T.Text+timeDisplay tv = T.pack . show <$> readTVar tv++clockRegion :: IO ()+clockRegion = do+	tv <- atomically . newTVar =<< getCurrentTime+	void $ atomically $ openConsoleRegionSTM Linear . rightAlign+		=<< newConsoleRegionSTM Linear (timeDisplay tv)+	forever $ do+		threadDelay 1000000 -- 1 sec+		atomically . (writeTVar tv) =<< getCurrentTime++rightAlign :: ConsoleRegionHandle -> STM T.Text+rightAlign r = do+        t <- readRegionContent r+        w <- consoleWidth+        return (T.replicate (w - T.length t) (T.singleton ' ') <> t)