packages feed

reflex-vty 1.0.0.0 → 1.0.0.1

raw patch · 16 files changed

+579/−2 lines, 16 filesdep ~basedep ~containersdep ~reflexnew-component:exe:examplesPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: base, containers, reflex, text, time, vty, vty-crossplatform

API changes (from Hackage documentation)

Files

ChangeLog.md view
@@ -1,5 +1,9 @@ # Revision history for reflex-vty +## 1.0.0.1++* Add some more examples, especially of the new canvas features+ ## 1.0.0.0  * *Breaking change*: Add `HasColorProfile` class (with `colorProfile`/`localColorProfile` and a `ColorProfileReader` transformer) to `Reflex.Vty.Widget`. Widgets can call `colorProfile` to make decisions based on terminal capability.
README.md view
@@ -49,7 +49,23 @@  Run the bundled demo with `cabal run example` to see many of these in action: a text editor, a to-do list, scrollable text, clickable buttons, a live CPU-usage display with a true-color gradient bar, a scrollbar-modes demo, a terminal-cursor demo, and a little styling showcase featuring gradients, color operations, a canvas overlay, border presets, theming, and color-profile downsampling. -Feature requests, pull requests, and other feedback are welcome and appreciated (see the [contribution guide](CONTRIBUTING.md)). This library is still experimental, so big changes are possible.+### Example gallery++Each of these examples is a module in [`src-bin/Example/`](src-bin/Example/). You can run any of them with `cabal run examples -- <name>`.++|                                                 |                                                                                                     |+| :--                                             | :--                                                                                                 |+| **`spinner`** — an animated braille spinner     | <img src="https://vhs.charm.sh/vhs-2ZldM9gFD9ovea54MAwio3.gif" alt="spinner demo" width="420">      |+| **`progress`** — a gradient-filled progress bar | <img src="https://vhs.charm.sh/vhs-3oJNQQcQH6vM4VyH7Yww56.gif" alt="progress bar demo" width="420"> |+| **`stopwatch`** — a tick-driven count-up timer  | <img src="https://vhs.charm.sh/vhs-5C8hkG3W9P4tATBBtLhxIm.gif" alt="stopwatch demo" width="420">    |+| **`timer`** — a count-down timer                | <img src="https://vhs.charm.sh/vhs-3gpq3fy1zWP3IsXysnT7WT.gif" alt="timer demo" width="420">        |+| **`textinput`** — a focused text-input field    | <img src="https://vhs.charm.sh/vhs-4FzSVaJAnGq4V1NNrk1lal.gif" alt="text input demo" width="420">   |+| **`pager`** — a scrollable text viewport        | <img src="https://vhs.charm.sh/vhs-7a2bCmFHleF9MG3KxY4yeh.gif" alt="pager demo" width="420">        |+| **`views`** — toggle between two views          | <img src="https://vhs.charm.sh/vhs-7rflGBcgFrmIQgk0JmZNKe.gif" alt="views demo" width="420">        |++**More demos**: full-screen canvas animations (the classic Doom fire, a plasma field, a starfield, an animated splash) live on the [examples page](EXAMPLES.md).++Feature requests, pull requests, and other feedback are welcome and appreciated (see the [contribution guide](CONTRIBUTING.md)).  ### How to Build 
reflex-vty.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: reflex-vty-version: 1.0.0.0+version: 1.0.0.1 synopsis: Reflex FRP host and widgets for VTY applications description:   Build terminal applications using functional reactive programming (FRP) with Reflex FRP (<https://reflex-frp.org>).@@ -151,6 +151,40 @@     TupleSections     TypeApplications     TypeFamilies++executable examples+  hs-source-dirs: src-bin+  main-is: examples.hs+  ghc-options: -threaded -Wall+  build-depends:+    base,+    containers,+    reflex,+    reflex-vty,+    text,+    time,+    vty+  default-language: Haskell2010+  other-modules:+    Example.CellBuffer+    Example.Common+    Example.DoomFire+    Example.Pager+    Example.Progress+    Example.Space+    Example.Spinner+    Example.Splash+    Example.Stopwatch+    Example.TextInput+    Example.Timer+    Example.Views+  default-extensions:+    ConstraintKinds+    FlexibleContexts+    LambdaCase+    OverloadedStrings+    RankNTypes+    ScopedTypeVariables  executable leaktest   if !flag(leaktest)
+ src-bin/Example/CellBuffer.hs view
@@ -0,0 +1,48 @@+-- |+--   Description: An animated plasma field rendered cell-by-cell, à la the+--   cellbuffer example.+module Example.CellBuffer (cellBuffer) where++import Control.Monad.IO.Class (liftIO)+import Data.Time (getCurrentTime)+import qualified Graphics.Vty as V+import Reflex++import Example.Common+import Reflex.Vty++-- | A classic plasma effect: each cell's character and color are a pure+-- function of its position and the frame, summed over a few sine waves.+cellBuffer :: Demo t m => m (Event t ())+cellBuffer = do+  t0 <- liftIO getCurrentTime+  tick <- tickLossy 0.06 t0+  n <- foldDyn (\_ i -> i + 1) (0 :: Int) tick+  dw <- displayWidth+  dh <- displayHeight+  tellImages $ (\w h i -> [plasma w h i]) <$> current dw <*> current dh <*> current n+  quit+  where+    ramp = " .:-=+*#%@"+    grad =+      gradient1D+        [ (0, RGB 25 20 70)+        , (0.4, RGB 190 40 140)+        , (0.7, RGB 255 150 40)+        , (1, RGB 255 240 150)+        ]+    plasma w h i =+      let t = fromIntegral i * 0.12 :: Double+          cell x y =+            let fx = fromIntegral x+                fy = fromIntegral y+                v =+                  sin (fx * 0.18 + t)+                    + sin (fy * 0.27 + t * 1.2)+                    + sin ((fx + fy) * 0.12 + t * 0.8)+                    + sin (sqrt (fx * fx + fy * fy) * 0.2 - t)+                u = (v + 4) / 8 -- normalize roughly to [0,1]+                ci = max 0 $ min (length ramp - 1) $ floor (u * fromIntegral (length ramp))+                clr = fromRGB $ sampleGradient1D grad u+            in V.char (V.withForeColor V.defAttr clr) (ramp !! ci)+      in V.vertCat [V.horizCat [cell x y | x <- [0 .. w - 1]] | y <- [0 .. h - 1]]
+ src-bin/Example/Common.hs view
@@ -0,0 +1,44 @@+-- |+--   Description: Shared scaffolding for the small standalone example demos.+module Example.Common where++import Control.Monad.Fix (MonadFix)+import Control.Monad.IO.Class (MonadIO)+import qualified Graphics.Vty as V+import Reflex++import Reflex.Vty++-- | The set of capabilities the little single-screen demos rely on. They draw+-- with 'tellImages'\/'text', read the clock via 'tickLossy', and quit on a key.+type Demo t m =+  ( Reflex t+  , MonadFix m+  , MonadHold t m+  , MonadIO m+  , MonadIO (Performable m)+  , PerformEvent t m+  , PostBuild t m+  , TriggerEvent t m+  , HasInput t m+  , HasImageWriter t m+  , HasDisplayRegion t m+  , HasFocusReader t m+  , HasTheme t m+  )++-- | The brand accent color (cyan), matching the rest of the demos.+accent :: V.Color+accent = rgbColor 0 200 230++-- | A dim grey for hints.+dim :: V.Color+dim = rgbColor 120 120 140++-- | An 'Event' that fires when the user asks to quit (q or Ctrl+C). Demos+-- return this from their widget so 'mainWidget' shuts down.+quit :: (Reflex t, Monad m, HasInput t m) => m (Event t ())+quit = do+  q <- key (V.KChar 'q')+  c <- ctrlc+  pure $ leftmost [() <$ q, c]
+ src-bin/Example/DoomFire.hs view
@@ -0,0 +1,63 @@+-- |+--   Description: The classic Doom fire effect, à la the doom-fire example.+module Example.DoomFire (doomFire) where++import Control.Monad.IO.Class (liftIO)+import Data.Bits (shiftR, xor)+import Data.Time (getCurrentTime)+import qualified Graphics.Vty as V+import Reflex++import Example.Common+import Reflex.Vty++maxHeat :: Int+maxHeat = 36++-- | A heat grid seeded from a hot bottom row; each frame every cell cools and+-- drifts upward from the cell below it, producing rising flames. The grid is+-- sized to the terminal when the demo starts.+doomFire :: Demo t m => m (Event t ())+doomFire = do+  w <- sample . current =<< displayWidth+  h <- sample . current =<< displayHeight+  t0 <- liftIO getCurrentTime+  tick <- tickLossy 0.05 t0+  st <- foldDyn (\_ (g, f) -> (step w h f g, f + 1)) (initGrid w h, 0 :: Int) tick+  tellImages $ current $ ffor st $ \(g, _) -> [renderFire g]+  quit+  where+    initGrid w h =+      [[if y == h - 1 then maxHeat else 0 | _ <- [0 .. w - 1]] | y <- [0 .. h - 1]]+    step w h f grid =+      let stepRow y below =+            [ let r = rnd x y f+                  srcX = max 0 $ min (w - 1) $ x + ((r `div` 5) `mod` 3 - 1)+                  decay = r `mod` 5+              in max 0 (below !! srcX - decay)+            | x <- [0 .. w - 1]+            ]+      in [stepRow y (grid !! (y + 1)) | y <- [0 .. h - 2]] ++ [replicate w maxHeat]+    renderFire grid = V.vertCat [V.horizCat (map cell rowHeats) | rowHeats <- grid]+    cell heat+      | heat <= 0 = V.char V.defAttr ' '+      | otherwise = V.char (V.withForeColor V.defAttr (heatColor heat)) (heatChar heat)+    heatChar heat+      | heat < 10 = '░'+      | heat < 19 = '▒'+      | heat < 28 = '▓'+      | otherwise = '█'+    heatColor heat = fromRGB $ sampleGradient1D fireGrad (fromIntegral heat / fromIntegral maxHeat)+    fireGrad =+      gradient1D+        [ (0.0, RGB 12 8 8)+        , (0.18, RGB 90 0 0)+        , (0.4, RGB 210 35 0)+        , (0.6, RGB 255 110 0)+        , (0.8, RGB 255 200 40)+        , (1.0, RGB 255 255 200)+        ]+    rnd x y f =+      let a = x * 374761393 + y * 668265263 + f * 2246822519 + 12345+          b = (a `xor` (a `shiftR` 13)) * 1274126177+      in abs (b `xor` (b `shiftR` 16))
+ src-bin/Example/Pager.hs view
@@ -0,0 +1,28 @@+-- |+--   Description: A scrollable pager, à la bubbles\/viewport (pager example).+module Example.Pager (pager) where++import qualified Data.Text as T+import qualified Graphics.Vty as V+import Reflex++import Example.Common+import Reflex.Vty++-- | A 'scrollableText' viewport over a block of numbered lines. Scroll it with+-- the arrow keys or the mouse wheel.+pager :: Demo t m => m (Event t ())+pager = do+  tellImages $+    pure [V.text' (V.withForeColor V.defAttr accent) "  ↑/↓ or mouse wheel to scroll · q to quit"]+  dw <- displayWidth+  dh <- displayHeight+  let viewport w h = Region 0 2 w (max 1 (h - 2))+  _ <- pane (viewport <$> dw <*> dh) (pure True) $ scrollableText def (pure body)+  quit+  where+    body =+      T.unlines+        [ "Line " <> T.pack (show n) <> " — scrollable viewport content"+        | n <- [1 .. 60 :: Int]+        ]
+ src-bin/Example/Progress.hs view
@@ -0,0 +1,36 @@+-- |+--   Description: An animated, gradient-filled progress bar, à la bubbles\/progress.+module Example.Progress (progressBar) where++import Control.Monad.IO.Class (liftIO)+import qualified Data.Text as T+import Data.Time (getCurrentTime)+import qualified Graphics.Vty as V+import Reflex+import Text.Printf (printf)++import Example.Common+import Reflex.Vty++-- | A progress bar that fills 0→100% (then loops), tinted with a red→yellow→+-- green 'Gradient1D' so each cell carries its own color.+progressBar :: Demo t m => m (Event t ())+progressBar = do+  t0 <- liftIO getCurrentTime+  tick <- tickLossy 0.04 t0+  pct <- foldDyn (\_ p -> if p >= 1 then 0 else min 1 (p + 0.008)) (0 :: Double) tick+  dw <- displayWidth+  tellImages $ (\w p -> [barImage w p]) <$> current dw <*> current pct+  quit+  where+    grad = gradient1D [(0, RGB 255 70 90), (0.5, RGB 250 200 60), (1, RGB 80 220 120)]+    barImage w p =+      let barW = max 1 (w - 7)+          filled = round (p * fromIntegral barW) :: Int+          cellColor i = fromRGB $ sampleGradient1D grad (fromIntegral i / fromIntegral (max 1 (barW - 1)))+          cell i+            | i < filled = V.char (V.withForeColor V.defAttr (cellColor i)) '█'+            | otherwise = V.char (V.withForeColor V.defAttr (rgbColor 64 64 78)) '░'+          bar = V.horizCat $ map cell [0 .. barW - 1]+          label = V.text' (V.withForeColor V.defAttr accent) $ T.pack $ printf " %3d%%" (round (p * 100) :: Int)+      in V.horizCat [bar, label]
+ src-bin/Example/Space.hs view
@@ -0,0 +1,50 @@+-- |+--   Description: A drifting starfield, à la the space example.+module Example.Space (space) where++import Control.Monad.IO.Class (liftIO)+import Data.Bits (shiftR, xor)+import qualified Data.Map as Map+import Data.Time (getCurrentTime)+import qualified Graphics.Vty as V+import Reflex++import Example.Common+import Reflex.Vty++-- | A starfield warping leftward. Each star has a pseudo-random row and speed+-- (its "depth"); faster stars are brighter and drawn with a bolder glyph.+space :: Demo t m => m (Event t ())+space = do+  t0 <- liftIO getCurrentTime+  tick <- tickLossy 0.06 t0+  n <- foldDyn (\_ i -> i + 1) (0 :: Int) tick+  dw <- displayWidth+  dh <- displayHeight+  tellImages $ (\w h i -> [starfield w h i]) <$> current dw <*> current dh <*> current n+  quit+  where+    starfield w h i+      | w <= 0 || h <= 0 = V.emptyImage+      | otherwise =+          let nStars = max 1 (w * h `div` 14)+              stars = Map.fromList [star w h i k | k <- [0 .. nStars - 1]]+              mkRow y = V.horizCat [Map.findWithDefault gap (x, y) stars | x <- [0 .. w - 1]]+          in V.vertCat [mkRow y | y <- [0 .. h - 1]]+    gap = V.char V.defAttr ' '+    star w h i k =+      let y = hash (k * 2 + 1) `mod` h+          speed = 1 + hash (k * 3 + 2) `mod` 3+          x = (hash k - i * speed) `mod` w+          (ch, clr) = case speed of+            1 -> ('·', rgbColor 90 90 120)+            2 -> ('*', rgbColor 170 170 205)+            _ -> ('✦', rgbColor 255 255 255)+      in ((x, y), V.char (V.withForeColor V.defAttr clr) ch)+    -- A cheap integer hash, always non-negative.+    hash :: Int -> Int+    hash k0 =+      let a = k0 * 374761393 + 668265263+          b = (a `xor` (a `shiftR` 13)) * 1274126177+          c = b `xor` (b `shiftR` 16)+      in abs c
+ src-bin/Example/Spinner.hs view
@@ -0,0 +1,29 @@+-- |+--   Description: An animated spinner, à la bubbles\/spinner.+module Example.Spinner (spinner) where++import Control.Monad.IO.Class (liftIO)+import Data.Time (getCurrentTime)+import qualified Graphics.Vty as V+import Reflex++import Example.Common+import Reflex.Vty++-- | A braille spinner that cycles a frame every 80ms.+spinner :: Demo t m => m (Event t ())+spinner = do+  t0 <- liftIO getCurrentTime+  tick <- tickLossy 0.08 t0+  idx <- foldDyn (\_ i -> (i + 1) `mod` n) 0 tick+  tellImages $ current $ ffor idx frame+  quit+  where+    frames = "⣾⣽⣻⢿⡿⣟⣯⣷"+    n = length frames+    frame i =+      [ V.horizCat+          [ V.char (V.withForeColor V.defAttr accent) (frames !! i)+          , V.text' (V.withForeColor V.defAttr dim) "  Spinning forever…  (q to quit)"+          ]+      ]
+ src-bin/Example/Splash.hs view
@@ -0,0 +1,51 @@+-- |+--   Description: An animated splash screen, à la the splash example.+module Example.Splash (splash) where++import Control.Monad.IO.Class (liftIO)+import qualified Data.Text as T+import Data.Time (getCurrentTime)+import qualified Graphics.Vty as V+import Reflex++import Example.Common+import Reflex.Vty++-- | A centered title that types out in a pink→violet→cyan gradient, with a+-- subtitle that fades in beneath it; the whole thing loops.+splash :: Demo t m => m (Event t ())+splash = do+  t0 <- liftIO getCurrentTime+  tick <- tickLossy 0.06 t0+  n <- foldDyn (\_ i -> i + 1) (0 :: Int) tick+  dw <- displayWidth+  dh <- displayHeight+  tellImages $ (\w h i -> [frame w h i]) <$> current dw <*> current dh <*> current n+  quit+  where+    title = "reflex-vty"+    subtitle = "reactive terminal UIs"+    grad = gradient1D [(0, RGB 255 64 160), (0.5, RGB 150 90 255), (1, RGB 0 200 230)]+    frame w h i =+      let p = i `mod` 95+          revealed = min (length title) (max 0 (p `div` 2))+          subAlpha = max 0 (min 1 (fromIntegral (p - 26) / 18 :: Double))+          pad k = V.text' V.defAttr (T.replicate (max 0 k) " ")+          center len = (w - len) `div` 2+          titleImg =+            V.horizCat+              [ V.char (V.withStyle (V.withForeColor V.defAttr (charColor k)) V.bold) c+              | (k, c) <- zip [0 :: Int ..] (take revealed title)+              ]+          charColor k = fromRGB $ sampleGradient1D grad (fromIntegral k / fromIntegral (max 1 (length title - 1)))+          subImg+            | subAlpha <= 0 = V.emptyImage+            | otherwise = V.text' (V.withForeColor V.defAttr (fromRGB (mix subAlpha (RGB 35 35 45) (RGB 150 150 170)))) (T.pack subtitle)+          gap = V.char V.defAttr ' '+          top = max 0 (h `div` 2 - 1)+      in V.vertCat $+           replicate top gap+             ++ [ V.horizCat [pad (center (length title)), titleImg]+                , gap+                , V.horizCat [pad (center (length subtitle)), subImg]+                ]
+ src-bin/Example/Stopwatch.hs view
@@ -0,0 +1,30 @@+-- |+--   Description: A count-up stopwatch, à la bubbles\/stopwatch.+module Example.Stopwatch (stopwatch) where++import Control.Monad.IO.Class (liftIO)+import qualified Data.Text as T+import Data.Time (getCurrentTime)+import qualified Graphics.Vty as V+import Reflex+import Text.Printf (printf)++import Example.Common+import Reflex.Vty++-- | Counts up from zero, ticking every 100ms.+stopwatch :: Demo t m => m (Event t ())+stopwatch = do+  t0 <- liftIO getCurrentTime+  tick <- tickLossy 0.1 t0+  elapsed <- foldDyn (\_ d -> d + 0.1) (0 :: Double) tick+  tellImages $ current $ ffor elapsed line+  quit+  where+    line d =+      [ V.horizCat+          [ V.text' (V.withForeColor V.defAttr accent) "  ⏱  "+          , V.text' V.defAttr $ T.pack $ printf "%.1fs" d+          , V.text' (V.withForeColor V.defAttr dim) "   (q to quit)"+          ]+      ]
+ src-bin/Example/TextInput.hs view
@@ -0,0 +1,27 @@+-- |+--   Description: A single text input field, à la bubbles\/textinput.+module Example.TextInput (textInputExample) where++import qualified Graphics.Vty as V+import Reflex++import Example.Common+import Reflex.Vty++-- | A prompt and a 'textInput' below it. The input lives in an always-focused+-- 'pane', so it accepts typing immediately. Quit with Ctrl+C (q is a character).+textInputExample :: Demo t m => m (Event t ())+textInputExample = do+  dw <- displayWidth+  tellImages $+    pure+      [ V.vertCat+          [ V.text' (V.withForeColor V.defAttr accent) "  What's your name?"+          , V.text' V.defAttr ""+          ]+      ]+  let inputRegion w = Region 2 2 (max 1 (w - 4)) 1+  _ <- pane (inputRegion <$> dw) (pure True) $ textInput def+  tellImages $+    pure [V.translate 2 4 $ V.text' (V.withForeColor V.defAttr dim) "(type away — Ctrl+C to quit)"]+  ctrlc
+ src-bin/Example/Timer.hs view
@@ -0,0 +1,31 @@+-- |+--   Description: A countdown timer, à la bubbles\/timer.+module Example.Timer (timer) where++import Control.Monad.IO.Class (liftIO)+import qualified Data.Text as T+import Data.Time (getCurrentTime)+import qualified Graphics.Vty as V+import Reflex+import Text.Printf (printf)++import Example.Common+import Reflex.Vty++-- | Counts down from ten seconds, ticking every 100ms.+timer :: Demo t m => m (Event t ())+timer = do+  t0 <- liftIO getCurrentTime+  tick <- tickLossy 0.1 t0+  remaining <- foldDyn (\_ r -> max 0 (r - 0.1)) (10 :: Double) tick+  tellImages $ current $ ffor remaining line+  quit+  where+    line r =+      [ V.horizCat+          [ V.text' (V.withForeColor V.defAttr accent) "  ⏳  "+          , V.text' V.defAttr $ T.pack $ printf "%.1fs" r+          , V.text' (V.withForeColor V.defAttr dim) $+              if r <= 0 then "   time's up!  (q to quit)" else "   remaining  (q to quit)"+          ]+      ]
+ src-bin/Example/Views.hs view
@@ -0,0 +1,36 @@+-- |+--   Description: Switching between views, à la the composable-views example.+module Example.Views (views) where++import qualified Graphics.Vty as V+import Reflex++import Example.Common+import Reflex.Vty++-- | Two views toggled with Tab. Each is just a different rendering chosen from+-- a 'Dynamic' index — the FRP way to swap "views".+views :: Demo t m => m (Event t ())+views = do+  tab <- key (V.KChar '\t')+  n <- foldDyn (\_ i -> (i + 1) `mod` 2) (0 :: Int) tab+  tellImages $ current $ ffor n viewImage+  quit+  where+    hint = V.text' (V.withForeColor V.defAttr dim) "  Tab to switch views · q to quit"+    viewImage 0 =+      [ V.vertCat+          [ V.text' (V.withStyle (V.withForeColor V.defAttr accent) V.bold) "  ◖ View one ◗"+          , V.text' V.defAttr "  reactive, composable terminal UIs"+          , V.char V.defAttr ' '+          , hint+          ]+      ]+    viewImage _ =+      [ V.vertCat+          [ V.text' (V.withStyle (V.withForeColor V.defAttr (rgbColor 200 120 255)) V.bold) "  ◆ View two ◆"+          , V.text' V.defAttr "  swapped with a single keystroke"+          , V.char V.defAttr ' '+          , hint+          ]+      ]
+ src-bin/examples.hs view
@@ -0,0 +1,50 @@+-- | A gallery of small, standalone reflex-vty demos — ports of the Bubble Tea+-- examples (https://github.com/charmbracelet/bubbletea/tree/main/examples).+--+-- Run one with: @cabal run examples -- <name>@ (defaults to @spinner@).+module Main where++import qualified Data.Text as T+import Reflex+import System.Environment (getArgs)++import Example.CellBuffer+import Example.Common+import Example.DoomFire+import Example.Pager+import Example.Progress+import Example.Space+import Example.Spinner+import Example.Splash+import Example.Stopwatch+import Example.TextInput+import Example.Timer+import Example.Views+import Reflex.Vty++main :: IO ()+main = do+  args <- getArgs+  let name = case args of+        (a : _) -> a+        _ -> "spinner"+  mainWidget def $ runExample name++-- | Dispatch to a demo by name.+runExample :: Demo t m => String -> m (Event t ())+runExample = \case+  "spinner" -> spinner+  "progress" -> progressBar+  "progress-bar" -> progressBar+  "stopwatch" -> stopwatch+  "timer" -> timer+  "textinput" -> textInputExample+  "pager" -> pager+  "views" -> views+  "cellbuffer" -> cellBuffer+  "doom-fire" -> doomFire+  "space" -> space+  "splash" -> splash+  other -> do+    text $ pure $ "Unknown example: " <> T.pack other <> "  (q to quit)"+    quit