diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,66 @@
+# Changelog
+
+All notable changes to layoutz-hs will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
+
+## [0.3.4.0] - 2026-03-31
+
+### Added
+- `renderText :: Element a => a -> Text` for rendering directly to `Data.Text.Text`.
+- Unit tests for `renderText`.
+
+### Changed
+- Added `text` as an explicit dep on boot (stil technically zero dep by GHC standards)
+- But easy to comment out `renderText` for porting to MicroHs
+
+## [0.3.3.0] - 2026-03-14
+
+### Added
+- `runAppFinal`: like `runApp` but returns the final application state.
+- `runAppWithFinal`: like `runAppWith` but returns the final application state.
+- `runInline`: renders app inline without alt-screen, for embedded spinners/progress bars.
+- New `InlineLoadingDemo` example.
+
+## [0.3.2.0] - 2026-03-11
+
+### Added
+- `CmdExit` command for graceful app shutdown from within `appUpdate`.
+- `cmdIsExit` helper to check if a command tree contains an exit.
+
+### Fixed
+- Haddock markup errors.
+- `SpinnerDemo.hs` example.
+
+## [0.3.1.0] - 2026-03-01
+
+### Changed
+- Documentation tweaks and Haddock polish.
+
+## [0.3.0.0] - 2026-03-01
+
+### Added
+- TUI runtime (`LayoutzApp`, Elm Architecture style event loop).
+- Keyboard input handling (`Key`, `readKey`).
+- Commands (`Cmd`, `cmdFire`, `cmdTask`, `cmdAfterMs`).
+- Subscriptions (`Sub`, `subKeyPress`, `subEveryMs`).
+- `AppOptions` and `AppAlignment` for layout customization.
+- Spinner animations (`SpinnerDots`, `SpinnerLine`, `SpinnerClock`, `SpinnerBounce`).
+- Visualization primitives: `plotSparkline`, `plotLine`, `plotPie`, `plotBar`, `plotStackedBar`, `plotHeatmap`.
+- Braille-based line and pie chart rendering.
+- 256-color and RGB true-color support (`ColorFull`, `ColorTrue`).
+- Text styles with `Semigroup` combining (`StyleBold <> StyleItalic`).
+- `tightRow` for gapless horizontal layouts.
+- `wrap` for word-boundary text wrapping.
+- Ordered lists (`ol`) with nested numbering (arabic, alpha, roman).
+
+## [0.1.0.0] - 2026-02-27
+
+### Added
+- Initial Haskell port of layoutz.
+- Core DSL: `text`, `layout`, `box`, `row`, `center`, `ul`, `table`, `kv`, `tree`.
+- Border styles: normal, double, thick, round, ASCII, block, dashed, dotted, half-block, markdown, custom.
+- `withBorder`, `withColor`, `withStyle` combinators.
+- ANSI-aware width calculation (`charWidth`, `visibleLength`).
+- `hr`, `vr`, `pad`, `margin`, `chart`, `section`, `alignLeft`/`alignRight`/`alignCenter`/`justify`.
+- `OverloadedStrings` support for `L`.
diff --git a/Layoutz.hs b/Layoutz.hs
--- a/Layoutz.hs
+++ b/Layoutz.hs
@@ -66,6 +66,7 @@
   , withStyle
     -- * Rendering
   , render
+  , renderText
     -- * TUI Runtime
   , LayoutzApp(..)
   , Key(..)
@@ -80,7 +81,9 @@
   , defaultAppOptions
   , AppAlignment(..)
   , runApp
+  , runAppFinal
   , runAppWith
+  , runAppWithFinal
   , runInline
     -- * Subscriptions
   , subKeyPress
@@ -89,6 +92,8 @@
   ) where
 
 import Data.List (intercalate, transpose, nub)
+import qualified Data.Text
+import qualified Data.Text as T
 import Data.Bits ((.|.))
 import Data.String (IsString(..))
 import Data.Char (ord, chr)
@@ -114,17 +119,17 @@
 -- characters, 1 for regular characters, 2 for East Asian wide and emoji.
 charWidth :: Char -> Int
 charWidth c
-  | c < '\x0300' = 1  -- Fast path for ASCII and common Latin
-  | c >= '\x0300' && c < '\x0370' = 0  -- Combining diacriticals
-  | c >= '\x1100' && c < '\x1200' = 2  -- Hangul Jamo
-  | c >= '\x2E80' && c < '\x9FFF' = 2  -- CJK
-  | c >= '\xAC00' && c < '\xD7A4' = 2  -- Hangul Syllables
-  | c >= '\xF900' && c < '\xFB00' = 2  -- CJK Compatibility Ideographs
-  | c >= '\xFE10' && c < '\xFE20' = 2  -- Vertical forms
-  | c >= '\xFE30' && c < '\xFE70' = 2  -- CJK Compatibility Forms
-  | c >= '\xFF00' && c < '\xFF61' = 2  -- Fullwidth Forms
-  | c >= '\xFFE0' && c < '\xFFE7' = 2  -- Fullwidth symbols
-  | c >= '\x1F000' = 2  -- Emoji, symbols, supplementary ideographs
+  | c < '\x0300' = 1                     -- Fast path for ASCII and common Latin
+  | c >= '\x0300' && c < '\x0370' = 0    -- Combining diacriticals
+  | c >= '\x1100' && c < '\x1200' = 2    -- Hangul Jamo
+  | c >= '\x2E80' && c < '\x9FFF' = 2    -- CJK
+  | c >= '\xAC00' && c < '\xD7A4' = 2    -- Hangul Syllables
+  | c >= '\xF900' && c < '\xFB00' = 2    -- CJK Compatibility Ideographs
+  | c >= '\xFE10' && c < '\xFE20' = 2    -- Vertical forms
+  | c >= '\xFE30' && c < '\xFE70' = 2    -- CJK Compatibility Forms
+  | c >= '\xFF00' && c < '\xFF61' = 2    -- Fullwidth Forms
+  | c >= '\xFFE0' && c < '\xFFE7' = 2    -- Fullwidth symbols
+  | c >= '\x1F000' = 2                   -- Emoji, symbols, supplementary ideographs
   | c >= '\x20000' && c < '\x2FFFF' = 2  -- Supplementary ideographs
   | c >= '\x30000' && c < '\x3FFFF' = 2  -- Tertiary ideographs
   | otherwise = 1
@@ -202,6 +207,9 @@
 render :: Element a => a -> String
 render = renderElement
 
+renderText :: Element a => a -> Data.Text.Text
+renderText = T.pack . renderElement
+
 -- | L is the universal layout element type - a type-erased wrapper for the DSL.
 --
 -- This allows mixing different element types in layouts while providing a common interface.
@@ -384,7 +392,7 @@
 
 -- | Border character set supporting asymmetric borders (e.g. half-block styles)
 data BorderChars = BorderChars
-  { bcTL, bcTR, bcBL, bcBR             :: String   -- corners
+  { bcTL, bcTR, bcBL, bcBR              :: String   -- corners
   , bcHTop, bcHBottom                   :: String   -- horizontal (top vs bottom)
   , bcVLeft, bcVRight                   :: String   -- vertical (left vs right)
   , bcLeftTee, bcRightTee, bcCross      :: String   -- separator connectors
@@ -1080,7 +1088,7 @@
 ansiReset :: String
 ansiReset = "\ESC[0m"
 
--- Sparkline ------------------------------------------------------------------
+-- Sparkline --------------------------------------------------
 
 data SparklineData = SparklineData [Double]
 
@@ -1098,7 +1106,7 @@
 plotSparkline :: [Double] -> L
 plotSparkline = L . SparklineData
 
--- Line Plot (Braille) --------------------------------------------------------
+-- Line Plot (Braille) --------------------------------------------------
 
 -- | A data series for line plots: points, label, color
 data Series = Series [(Double, Double)] String Color
@@ -1176,7 +1184,7 @@
 plotLine :: Int -> Int -> [Series] -> L
 plotLine w h ss = L (PlotData ss w h)
 
--- Pie Chart (Braille) --------------------------------------------------------
+-- Pie Chart (Braille) --------------------------------------------------
 
 -- | A slice of a pie chart: value, label, color
 data Slice = Slice Double String Color
@@ -1234,7 +1242,7 @@
 plotPie :: Int -> Int -> [Slice] -> L
 plotPie w h sl = L (PieData sl w h)
 
--- Bar Chart (Vertical) -------------------------------------------------------
+-- Bar Chart (Vertical) --------------------------------------------------------
 
 -- | A bar item: value, label, color
 data BarItem = BarItem Double String Color
@@ -1278,7 +1286,7 @@
 plotBar :: Int -> Int -> [BarItem] -> L
 plotBar w h items = L (BarChartData items w h)
 
--- Stacked Bar Chart -----------------------------------------------------------
+-- Stacked Bar Chart -------------------------------------------------------
 
 -- | A group of stacked bars: segments and group label
 data StackedBarGroup = StackedBarGroup [BarItem] String
@@ -1359,7 +1367,7 @@
 plotStackedBar :: Int -> Int -> [StackedBarGroup] -> L
 plotStackedBar w h groups = L (StackedBarChartData groups w h)
 
--- Heatmap --------------------------------------------------------------------
+-- Heatmap -----------------------------------------------------------
 
 -- | Heatmap data: grid of values, row labels, column labels
 data HeatmapData = HeatmapData [[Double]] [String] [String]
@@ -1595,14 +1603,19 @@
 --
 -- Press ESC, Ctrl+C, or Ctrl+D to quit the application.
 runApp :: LayoutzApp state msg -> IO ()
-runApp = runAppWith defaultAppOptions
+runApp app = runAppWithFinal defaultAppOptions app >> pure ()
 
+-- | Like 'runApp', but returns the final application state after exit.
+-- Useful for interactive tweaking of state followed by further processing.
+runAppFinal :: LayoutzApp state msg -> IO state
+runAppFinal = runAppWithFinal defaultAppOptions
+
 -- | Run an app inline — no alt screen, no clearing. The app renders in-place
 -- below whatever is already on screen and returns when it issues 'CmdExit'.
 -- Useful for embedding animated progress bars or spinners in scripts.
 runInline :: LayoutzApp state msg -> IO ()
-runInline = runAppWith defaultAppOptions
-  { optClearOnStart = False, optClearOnExit = False }
+runInline app = runAppWithFinal defaultAppOptions
+  { optClearOnStart = False, optClearOnExit = False } app >> pure ()
 
 -- | Run an interactive TUI application with custom options.
 --
@@ -1610,7 +1623,16 @@
 -- 'runAppWith' 'defaultAppOptions' { 'optAlignment' = 'AppAlignCenter' } myApp
 -- @
 runAppWith :: AppOptions -> LayoutzApp state msg -> IO ()
-runAppWith opts LayoutzApp{..} = do
+runAppWith opts app = runAppWithFinal opts app >> pure ()
+
+-- | Like 'runAppWith', but returns the final application state after exit.
+-- Useful for interactive tweaking of state followed by further processing.
+--
+-- @
+-- finalState <- 'runAppWithFinal' 'defaultAppOptions' { 'optAlignment' = 'AppAlignCenter' } myApp
+-- @
+runAppWithFinal :: AppOptions -> LayoutzApp state msg -> IO state
+runAppWithFinal opts LayoutzApp{..} = do
   oldBuffering <- hGetBuffering stdin
   oldEcho <- hGetEcho stdin
 
@@ -1746,9 +1768,11 @@
                 Nothing -> checkExit inputLoop
               Nothing -> checkExit inputLoop
 
-  inputLoop `catch` \ex -> case ex of
+  (inputLoop `catch` \ex -> case ex of
     UserInterrupt -> doCleanup
-    _             -> doCleanup
+    _             -> doCleanup)
+
+  readIORef stateRef
 
 -- | Clear the screen and move cursor to top-left
 clearScreen :: IO ()
diff --git a/examples/ShowcaseApp.hs b/examples/ShowcaseApp.hs
--- a/examples/ShowcaseApp.hs
+++ b/examples/ShowcaseApp.hs
@@ -79,7 +79,7 @@
 sceneWidth :: Int
 sceneWidth = 75
 
--- Helpers ---------------------------------------------------------------------
+-- Helpers
 
 toggleIn :: Int -> [Int] -> [Int]
 toggleIn x xs = if x `elem` xs then filter (/= x) xs else x : xs
@@ -154,7 +154,7 @@
   | abs vy < 0.05 && y < 0.1 = (0, 0)
   | otherwise                 = (y, vy)
 
--- Subscriptions ---------------------------------------------------------------
+-- Subscriptions
 
 subscriptions :: ShowcaseState -> Sub Msg
 subscriptions s = subBatch
@@ -176,7 +176,7 @@
       _            -> Nothing
   ]
 
--- View ------------------------------------------------------------------------
+-- View
 
 view :: ShowcaseState -> L
 view s =
@@ -226,7 +226,7 @@
         _ -> "  </> scenes  ESC quit"
   in withStyle StyleDim $ withColor ColorBrightBlack $ text hints
 
--- Scene 1: Physics Game -------------------------------------------------------
+-- Scene 1: Physics Game
 
 scenePhysicsGame :: ShowcaseState -> L
 scenePhysicsGame s =
@@ -261,7 +261,7 @@
         ]
     ]
 
--- Scene 2: Text Input & Lists ------------------------------------------------
+-- Scene 2: Text Input & Lists
 
 sceneTextInput :: ShowcaseState -> L
 sceneTextInput s =
@@ -316,7 +316,7 @@
         ]
     ]
 
--- Scene 3: Borders & Styles --------------------------------------------------
+-- Scene 3: Borders & Styles
 
 sceneBordersStyles :: ShowcaseState -> L
 sceneBordersStyles _ =
@@ -347,7 +347,7 @@
         ]
     ]
 
--- Scene 4: Tables -------------------------------------------------------------
+-- Scene 4: Tables
 
 sceneTables :: ShowcaseState -> L
 sceneTables s =
@@ -376,7 +376,7 @@
         ]
     ]
 
--- Scene 5: Charts & Plots ----------------------------------------------------
+-- Scene 5: Charts & Plots
 
 sceneChartsPlots :: ShowcaseState -> L
 sceneChartsPlots s =
@@ -406,7 +406,7 @@
         ]
     ]
 
--- Scene 6: Bar Charts & Sparklines -------------------------------------------
+-- Scene 6: Bar Charts & Sparklines
 
 sceneBarChartsSparklines :: ShowcaseState -> L
 sceneBarChartsSparklines s =
@@ -446,7 +446,7 @@
         ]
     ]
 
--- Scene 7: Selections & Heatmap ----------------------------------------------
+-- Scene 7: Selections & Heatmap
 
 sceneSelectionsHeatmap :: ShowcaseState -> L
 sceneSelectionsHeatmap s =
@@ -492,7 +492,7 @@
         [ plotHeatmap' 5 (HeatmapData heatData days hours) ]
     ]
 
--- App -------------------------------------------------------------------------
+-- App
 
 showcaseApp :: LayoutzApp ShowcaseState Msg
 showcaseApp = LayoutzApp
diff --git a/layoutz.cabal b/layoutz.cabal
--- a/layoutz.cabal
+++ b/layoutz.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.0
 name:          layoutz
-version:       0.3.3.0
+version:       0.3.4.0
 synopsis:      Simple, beautiful CLI output
 description:   Zero-dep, compositional terminal output
 homepage:      https://github.com/mattlianje/layoutz
@@ -15,6 +15,7 @@
 
 extra-source-files:
     README.md
+    CHANGELOG.md
     pix/*.png
 
 source-repository head
@@ -25,6 +26,7 @@
 library
   exposed-modules:     Layoutz
   build-depends:       base >= 4.7 && < 5
+                     , text
   hs-source-dirs:      .
   default-language:    Haskell2010
   ghc-options:         -Wall
@@ -58,6 +60,7 @@
   main-is:             Test.hs
   build-depends:       base >= 4.7 && < 5
                      , layoutz
+                     , text
                      , tasty >= 1.4
                      , tasty-hunit >= 0.10
   hs-source-dirs:      test
diff --git a/test/Test.hs b/test/Test.hs
--- a/test/Test.hs
+++ b/test/Test.hs
@@ -4,6 +4,7 @@
 import Test.Tasty.HUnit
 import Layoutz
 import Data.List (isInfixOf)
+import qualified Data.Text as T
 import Data.IORef (newIORef, readIORef, writeIORef)
 
 -- Helper to strip ANSI codes for testing
@@ -24,6 +25,7 @@
   [ basicElementTests
   , visualFeatureTests
   , dataVisualizationTests
+  , renderTextTests
   , containerTests
   , layoutTests
   , dimensionTests
@@ -55,6 +57,19 @@
       
   , testCase "center custom width" $
       render (center' 10 $ text "Test") @?= "   Test   "
+  ]
+
+-- renderText tests
+renderTextTests :: TestTree
+renderTextTests = testGroup "renderText"
+  [ testCase "renderText returns Text, not String" $
+      renderText (text "Hello World") @?= T.pack "Hello World"
+
+  , testCase "renderText matches render" $
+      renderText (text "Test") @?= T.pack (render (text "Test"))
+
+  , testCase "renderText on composed layout" $
+      renderText (layout [text "A", text "B"]) @?= T.pack "A\nB"
   ]
 
 -- Visual feature tests
