concurrent-output released yesterday got a lot of fun features. It now does full curses-style minimization of the output, to redraw updated lines with optimal efficiency. And supports multiline regions/wrapping too long lines. And allows the user to embed ANSI colors in a region. 3 features that are in some tension and were fun to implement all together.
But I have a more interesting feature to blog about... I've added the ability for the content of a Region to be determined by a (STM transaction).
Here, for example, is a region that's a clock:
timeDisplay :: TVar UTCTime -> STM Text
timeDisplay tv = T.pack . show <$> readTVar tv
clockRegion :: IO ConsoleRegionHandle
clockRegion = do
tv <- atomically . newTVar =<< getCurrentTime
r <- openConsoleRegion Linear
setConsoleRegion r (timeDisplay tv)
async $ forever $ do
threadDelay 1000000 -- 1 sec
atomically . (writeTVar tv) =<< getCurrentTime
return r
There's something magical about this. Whenever a new value is written into the TVar, concurrent-output automatically knows that this region needs to be updated. How does it know how to do that?
Magic of STM. Basically, concurrent-output composes all the STM transactions of Regions, and asks STM to wait until there's something new to display. STM keeps track of whatever TVars might be looked at, and so can put the display thread to sleep until there's a change to display.
Using STM I've gotten extensability for free, due to the nice ways that STM transactions compose.
A few other obvious things to do with this: Compose 2 regions with padding so they display on the same line, left and right aligned. Trim a region's content to the display width. (Handily exported by concurrent-output in a TVar for this kind of thing.)
I'm tempted to write a console spreadsheet using this. Each visible cell of the spreadsheet would have its own region, that uses a STM transaction to display. Plain data Cells would just display their current value. Cells that contain a function would read the current values of other Cells, and use that to calculate what to display. Which means that a Cell containing a function would automatically update whenever any of the Cells that it depends on were updated!
Do you think that a simple interactive spreadsheet built this way would be more than 100 lines of code?
Recursion would indeed be where something so simple becomes problimatic.
One way to limit iterations would be to make a cell have a counter. When the cell calculates its value, it increments the counter and bails if there's been too much calculation. The counter is reset to 0 once the cell is successfully displayed.
Implementation of that approach using console-regions:
Edit: Hmm, looking at that, it doesn't quite work. Since the STM transaction reads from the counter and then modifies it, when concurrent-output asks STM to
retry
in order to wait for changes, it will immediately re-run the action, since a value it looked at has changed. So, dunno..