diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,37 @@
+# Changelog for ascii85x
+
+## 0.2.4.1
+
+- Dependency updates
+
+## 0.2.4.0
+
+- Handle GDB variables
+
+## 0.2.3.0
+
+- Handle window settings
+
+## 0.2.2.0
+
+- Handle ZRCL settings
+
+## 0.2.1.0
+
+- Handle picture variables
+
+## 0.2.0.0
+
+- Also parse Backup files
+- Significant refactor
+
+## 0.1.2.1
+
+- Internal change: Added testing
+
+## 0.1.2.0
+
+- Added complete set of variable IDs
+- Make real/comlex a type parameter for some variable types
+
+## Unreleased changes
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Nigel Stepp (c) 2021
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Nigel Stepp nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,106 @@
+# ascii85x
+
+## Introduction
+
+This module is meant to display information from
+TI-85 variable files in a human readable way.
+
+There were a few reasons for making this in 2021:
+
+1. ascii85p, an ancient program for displaying
+   program text, does not come with source code
+2. TokenIDE, a more modern and very featureful
+   tool, is written in C#, and I didn't want to
+   use mono.
+3. TokenIDE also does not come with source code.
+4. Just let me have some fun.
+
+## Background
+
+Perhaps you, like many technically minded folks who
+went through school with their trusty TI calculator,
+have a catalog of TI-BASIC programs that you wrote
+for both fun and utility.
+
+If I look through my backup directory and see `RND.85p`,
+I might wonder what's in there and try `vim RND.85p`:
+
+```
+**TI85**^Z^L^@Group file dated Sun Sep 26 16:12:47 202^@^@^L^A^G^@^A^A^R^CRND^A^Aÿ^@D0^@^K3AoD0^@^K3BoD0^@^K3CoD0^@^K3DoD0^@^K3SoD0^@^K=^DoD100^@^K=^Eo¡D35^@^K=^FoD35^@^K=^GoD25^@^K=^BoD5^@^K=^Coìo<91>o<83>oÛ^P^PCUD22^@^Q@^P3CSD500^@^Q^QoØ^P^P¥^PApD2^@^Q^QPD1^@^QoÙo3A`D1^@^K3AoÚo3B`D1^@^K3BoÞo3C`D1^@^K3Co3S`^P3Aa3B^Q^K3So<98>^P3CqD5^@/3Aa3B^Qo<98>^P3CqD5^@/3Sq3C^QoÞoë^PD2^@/D1^@/3Aoë^PD3^@/D1^@/3Boë^PD4^@/D1^@/3Sq3Cv?
+```
+
+Not so nice. As mentioned above, there are some options for decoding these files, but they didn't work for me, so I made `ascii85x`.
+
+```
+$ ascii85x RND.85p
+
+Program "RND":
+0→A
+0→B
+0→C
+0→D
+0→S
+0→xMin
+100→xMax
+-35→yMin
+35→yMax
+25→xScl
+5→yScl
+ClLCD
+FnOff 
+ClDrw
+While ((getKy≠22) and (C<=500))
+If ((int(rand*2))==1)
+Then
+A+1→A
+Else
+B+1→B
+End
+C+1→C
+S+(A-B)→S
+PtOn(C/5,A-B)
+PtOn(C/5,S/C)
+End
+Outpt(2,1,A
+Outpt(3,1,B
+Outpt(4,1,S/C
+```
+
+Much better!
+
+## Usage
+
+`ascii85x` will read and display any variable or group file as output by the TI-85 link backup utility.
+The variable types supported are:
+
+| Type       | Extension |
+|------------|-----------|
+|RealValue   | 85n       |
+|ComplexValue| 85c       |
+|Vector      | 85v       |
+|List        | 85l       |
+|Matrix      | 85m       |
+|Constant    | 85k       |
+|Equation    | 85y       |
+|String      | 85s       |
+|Program     | 85p       |
+|Picture     | 85i       |
+|Settings    | 85?       |
+|GDB         | 85d       |
+
+A group file (`85g`) may contain many of these types of variables.
+
+```
+$ ascii85x --help
+Usage: ascii85x [-i|--info] [-D|--debug] [-v|--verbose] VARFILE [-V|--version]
+  Convert TI-85 variable files to text
+
+Available options:
+  -i,--info                Show file info only
+  -D,--debug               Show extra variable details
+  -v,--verbose             Show variable file summary
+  VARFILE                  85x variable file
+  -V,--version             Show version only
+  -h,--help                Show this help text
+```
+
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,131 @@
+module Main where
+
+import Prelude hiding (putStrLn,take,drop)
+import Control.Monad
+import Data.Text (pack,unpack)
+import Data.Text.IO (putStrLn)
+import Data.ByteString (foldr',take,drop)
+import Data.Word
+import Data.Version
+import Numeric (showHex)
+import System.Exit (exitSuccess)
+
+import Data.Attoparsec.ByteString (parseOnly)
+import Options.Applicative
+
+import Data.TI85
+import Paths_ascii85x (version)
+
+data Config = Config {
+    showInfo :: Bool,
+    debug :: Bool,
+    verbose :: Bool,
+    imgOut :: Maybe FilePath,
+    filePath :: FilePath
+    }
+
+versionOpt = infoOption (showVersion version) (
+    long "version"
+    <> short 'V'
+    <> help "Show version only"
+    )
+
+argParser :: Parser Config
+argParser = Config
+    <$> switch (
+        long "info"
+        <> short 'i'
+        <> help "Show file info only"
+        )
+    <*> switch (
+        long "debug"
+        <> short 'D'
+        <> help "Show extra variable details"
+        )
+    <*> switch (
+        long "verbose"
+        <> short 'v'
+        <> help "Show file summary"
+        )
+    <*> optional (strOption (
+        long "image-dir"
+        <> short 'I'
+        <> metavar "DIR"
+        <> help "Output PNG versions of PIC variables to this directory"
+        ))
+    <*> strArgument (
+        metavar "FILE"
+        <> help "85x file"
+        )
+
+argInfo = info (argParser <**> versionOpt <**> helper) (
+    fullDesc <> progDesc "Convert TI-85 variable files to text"
+    )
+
+processVarData :: Config -> TIVarData -> IO ()
+processVarData config (TIVarData tiVars) = do
+    let vars = map readVariable tiVars
+    when (debug config) $ print vars
+    let names = map (tiDecode.varName) tiVars
+    let types = map (showType.idToType.varId) tiVars
+    forM_ (zip3 names vars types) $ \(name,var,varType) -> do
+        putStrLn $ "\n" <> varType <> " \"" <> name <> "\":"
+        printVariable var
+
+        -- Check for a picture variable, and write
+        -- to an image if configured to do so
+        let pic = case var of
+                    TIPicture bitmap -> Just bitmap
+                    _ -> Nothing
+
+        -- The following completes only when the variable is
+        -- a picture and when the image path option is set
+        sequence_ $ do
+            bitmap <- pic
+            imgPath <- imgOut config
+            return $ do
+                let imgFile = imgPath <> "/" <> unpack name <> ".png"
+                writePicPng imgFile bitmap
+                putStrLn $ "Saved " <> name <> " to " <> pack imgFile
+
+processBackupData :: Config -> TIBackupData -> IO ()
+processBackupData config tiBackup = do
+    let backupHdr = backupHeader tiBackup
+    let data2Addr = hdrData2Addr backupHdr
+    let dataDisplay = if verbose config
+        then print . foldr' hexify ""
+        else print
+    putStrLn $ pack $ "Data Section 1 (" <> show (data1Len tiBackup) <> "):"
+    dataDisplay (data1 tiBackup)
+    putStrLn $ pack $ "Data Section 2 (" <> show (data2Len tiBackup) <> "):"
+    dataDisplay (data2 tiBackup)
+    putStrLn $ pack $ "Data 2 Address: " <> showHex data2Addr "\n"
+    putStrLn $ pack $ "Variable Table (" <> show (varTableLen tiBackup) <> "):"
+    printVariableTable data2Addr (varTable tiBackup)
+
+    when (verbose config) $ do
+        extractVariableTable data2Addr tiBackup
+
+  where
+    hexify :: Word8 -> ShowS
+    hexify w =
+        let pad = if w < 0xf then "0" else ""
+        in \s' -> pad <> showHex w s'
+
+main :: IO ()
+main = do
+    config <- execParser argInfo
+
+    tiFile <- readTIFile (filePath config)
+
+    when (showInfo config) $ do
+        printFileSummary tiFile
+        exitSuccess
+
+    when (verbose config) $ do
+        printFileSummary tiFile
+
+    case tiData tiFile of
+        BackupData backup -> processBackupData config backup
+        VariableData vars -> processVarData config vars
+
diff --git a/ascii85x.cabal b/ascii85x.cabal
new file mode 100644
--- /dev/null
+++ b/ascii85x.cabal
@@ -0,0 +1,97 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.37.0.
+--
+-- see: https://github.com/sol/hpack
+
+name:           ascii85x
+version:        0.2.4.1
+synopsis:       Displays TI-85 variable files as text
+description:    Please see the README on GitHub at <https://github.com/nstepp/ascii85x#readme>
+category:       Data
+homepage:       https://github.com/nstepp/ascii85x#readme
+bug-reports:    https://github.com/nstepp/ascii85x/issues
+author:         Nigel Stepp
+maintainer:     stepp@atistar.net
+copyright:      2021 Nigel Stepp
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+    ChangeLog.md
+
+source-repository head
+  type: git
+  location: https://github.com/nstepp/ascii85x
+
+library
+  exposed-modules:
+      Data.TI85
+      Data.TI85.Encoding
+      Data.TI85.File
+      Data.TI85.File.Backup
+      Data.TI85.File.Variable
+      Data.TI85.IO
+      Data.TI85.Parsers
+      Data.TI85.Token
+      Data.TI85.Var
+      Data.TI85.Var.Pic
+  other-modules:
+      Paths_ascii85x
+  hs-source-dirs:
+      src
+  default-extensions:
+      OverloadedStrings
+  build-depends:
+      JuicyPixels >=3.3 && <4
+    , array ==0.5.*
+    , attoparsec >=0.13 && <0.15
+    , base >=4.7 && <5
+    , bytestring >=0.10 && <0.12
+    , text >=1.2 && <3
+    , vector >=0.12 && <1
+  default-language: Haskell2010
+
+executable ascii85x
+  main-is: Main.hs
+  other-modules:
+      Paths_ascii85x
+  hs-source-dirs:
+      app
+  default-extensions:
+      OverloadedStrings
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      JuicyPixels >=3.3 && <4
+    , array ==0.5.*
+    , ascii85x
+    , attoparsec >=0.13 && <0.15
+    , base >=4.7 && <5
+    , bytestring >=0.10 && <0.12
+    , optparse-applicative >=0.18.1 && <0.19
+    , text >=1.2 && <3
+    , vector >=0.12 && <1
+  default-language: Haskell2010
+
+test-suite ascii85x-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Paths_ascii85x
+  hs-source-dirs:
+      test
+  default-extensions:
+      OverloadedStrings
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      JuicyPixels >=3.3 && <4
+    , array ==0.5.*
+    , ascii85x
+    , attoparsec >=0.13 && <0.15
+    , base >=4.7 && <5
+    , bytestring >=0.10 && <0.12
+    , hedgehog
+    , text >=1.2 && <3
+    , vector >=0.12 && <1
+  default-language: Haskell2010
diff --git a/src/Data/TI85.hs b/src/Data/TI85.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/TI85.hs
@@ -0,0 +1,41 @@
+{-|
+Module      : Data.TI85
+Description : TI-85 variable file utilities.
+Copyright   : (c) Nigel Stepp, 2021
+License     : BSD3
+Maintainer  : stepp@atistar.net
+Stability   : experimental
+Portability : POSIX
+
+This module is meant to display information from
+TI-85 variable files in a human readable way.
+
+There were a few reasons for making this in 2021:
+
+1. ascii85p, an ancient program for displaying
+   program text, does not come with source code
+2. TokenIDE, a more modern and very featureful
+   tool, is written in C#, and I didn't want to
+   use mono.
+3. TokenIDE also does not come with source code.
+4. Just let me have some fun.
+
+-}
+module Data.TI85 (
+    module Data.TI85.Encoding,
+    module Data.TI85.Token,
+    module Data.TI85.Parsers,
+    module Data.TI85.Var,
+    module Data.TI85.Var.Pic,
+    module Data.TI85.File,
+    module Data.TI85.IO
+    ) where
+
+import Data.TI85.Encoding
+import Data.TI85.Token
+import Data.TI85.Parsers
+import Data.TI85.Var
+import Data.TI85.Var.Pic
+import Data.TI85.File
+import Data.TI85.IO
+
diff --git a/src/Data/TI85/Encoding.hs b/src/Data/TI85/Encoding.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/TI85/Encoding.hs
@@ -0,0 +1,43 @@
+
+-- | Text data in a variable file is encoded using a
+-- special purpose code page. This module supports
+-- decoding such text, using an approximate projection
+-- into unicode.
+module Data.TI85.Encoding (
+    tiDecode
+    ) where
+
+
+import Data.Word
+import Data.Array (Array, listArray, (!))
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as BS
+import Data.Text (Text)
+import qualified Data.Text as T
+
+-- | An approximation of the TI-86 code page as unocide.
+tiCodePage :: Array Word8 Text
+tiCodePage = listArray (0x00,0xff) [
+    "■","b","o","d","h","▸","⬆","⬇","∫","⨉","A","B","C","D","E","F",
+    "√","⁻¹","²","∠","°","ʳ","ᵀ","≤","≠","≥","⁻","ᴇ","→","₁₀","↑","↓",
+    " ","!","\"","#","$","%","&","'","(",")","*","+",",","-",".","/",
+    "0","1","2","3","4","5","6","7","8","9",":",";","<","=",">","?",
+    "@","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O",
+    "P","Q","R","S","T","U","V","W","X","Y","Z","[","\\","]","^","_",
+    "`","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o",
+    "p","q","r","s","t","u","v","w","x","y","z","{","|","}","~","=",
+    "₀","₁","₂","₃","₄","₅","₆","₇","₈","₉","Á","À","Â","Ä","á","à",
+    "â","ä","É","È","Ê","Ë","é","è","ê","ë","Í","Ì","Î","Ï","í","ì",
+    "î","ï","Ó","Ò","Ô","Ö","ó","ò","ô","ö","Ú","Ù","Û","Ü","ú","ù",
+    "û","ü","Ç","ç","Ñ","ñ","´","`","¨","¿","¡","α","β","γ","Δ","δ",
+    "ε","θ","λ","μ","π","ρ","Σ","σ","τ","φ","Ω","x̅","y̅","ˣ","…","◂",
+    "■","≀","-","²","°","³","\n","➡","╲","╲","◥","◣","⊸","∘","⋱","█",
+    "⇧","A","a","_","↥","A̲","a̲","▩","▫","₊",".","⁴","=","■","■","■",
+    "■","■","■","■","■","■","■","■","■","■","■","■","■","■","■","■"]
+
+
+-- |Decode a bytestring into text using the 
+-- TI-86 screen code page.
+tiDecode :: ByteString -> Text
+tiDecode = T.concat . map (tiCodePage!) . BS.unpack
+
diff --git a/src/Data/TI85/File.hs b/src/Data/TI85/File.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/TI85/File.hs
@@ -0,0 +1,56 @@
+
+-- | A collection of types and utilities for dealing with
+-- TI Link backup files.
+module Data.TI85.File (
+    -- * Re-exported modules
+    module Data.TI85.File.Variable,
+    module Data.TI85.File.Backup,
+    -- * Types
+    TIHeader(..),
+    TIFileData(..),
+    TIFile(..)
+    ) where
+
+import Data.ByteString
+import Data.Word
+
+import Data.TI85.File.Variable
+import Data.TI85.File.Backup
+
+-- | The TI-85 header is common between backup files
+-- and variable files.
+--
+-- +--------+---+---------------------------------+
+-- | 8 Byte | 3 | 42 Byte                         |
+-- +========+===+=================================+
+-- |**TI85**|xyz| Comment                         |
+-- +--------+---+---------------------------------+
+--
+-- where @xyz@ is always @0x1a,0x0c,0x00@.
+data TIHeader = TIHeader {
+    hdrSig :: ByteString, -- 8 bytes
+    hdrSig2 :: ByteString, -- 3 bytes
+    hdrComment :: ByteString, -- 42 bytes
+    hdrDataLen :: Word16
+    } deriving Show
+
+-- | There are two possible file formats
+--
+-- - Variable files contain one or more variables,
+--   encoded in a variable-specific way.
+-- - Backup files contain memory dumps, which also
+--   include variable data in a raw form.
+data TIFileData = BackupData TIBackupData
+    | VariableData TIVarData
+    deriving Show
+
+-- | All TI Link files have a common header and
+-- checksum, with a data section that differs
+-- between backup and variable types.
+-- See `TIFileData`.
+data TIFile = TIFile {
+    tiHeader :: TIHeader,
+    tiData :: TIFileData,
+    tiChecksum :: Word16
+    } deriving Show
+
diff --git a/src/Data/TI85/File/Backup.hs b/src/Data/TI85/File/Backup.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/TI85/File/Backup.hs
@@ -0,0 +1,50 @@
+
+-- | This module defines the structure of a TI-85 backup file.
+-- A backup file contains system memory (data section 1), user
+-- memory (section 2), and a variable table that maps where
+-- user variables are located in data section 2.
+module Data.TI85.File.Backup where
+
+import Data.ByteString (ByteString)
+import Data.Text (Text)
+import Data.Word (Word8,Word16)
+
+import Data.TI85.File.Variable (VarType)
+
+-- | An entry in the variable table.
+-- Note: the variable table is stored in
+-- reverse byte order (including 2-byte
+-- words!).
+data VarTableEntry = VarTableEntry {
+    entryId :: Word8,
+    entryAddr :: Word16,
+    entryNameLen :: Word8,
+    entryName :: ByteString
+    } deriving Show
+
+-- | A list of variable table entries
+type VarTable = [VarTableEntry]
+
+-- | Backup-specific header. This header comes after
+-- the more general `Data.TI85.File.TIHeader`.
+data TIBackupHeader = TIBackupHeader {
+    hdrDataLenOffset :: Word16, -- Always 9
+    hdrData1Len :: Word16,
+    hdrTypeID :: Word8, -- Always 0x1D
+    hdrData2Len :: Word16,
+    hdrData3Len :: Word16,
+    hdrData2Addr :: Word16
+    } deriving Show
+
+-- | The top-level structure of a backup
+-- file.
+data TIBackupData = TIBackupData {
+    backupHeader :: TIBackupHeader,
+    data1Len :: Word16,
+    data1 :: ByteString,
+    data2Len :: Word16,
+    data2 :: ByteString,
+    varTableLen :: Word16,
+    varTable :: VarTable
+    } deriving Show
+
diff --git a/src/Data/TI85/File/Variable.hs b/src/Data/TI85/File/Variable.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/TI85/File/Variable.hs
@@ -0,0 +1,227 @@
+
+-- | This module defines the structure of a TI-85 variable file.
+module Data.TI85.File.Variable (
+    -- * Types
+    TIVar(..),
+    TIVarData(..),
+    VarField(..),
+    VarType(..),
+    -- * Utilities
+    idToType,
+    typeToId,
+    showType
+    ) where
+
+import Data.ByteString (ByteString)
+import Data.Text (Text)
+import Data.Word (Word8,Word16)
+
+-- | The structure of a single variable.
+-- For the meaning of variable IDs, see `idToType`.
+data TIVar = TIVar {
+    varOffset :: Word16,
+    varLen :: Word16,
+    varId :: Word8,
+    varNameLen :: Word8,
+    varName :: ByteString,
+    varDataLen :: Word16,
+    varData :: ByteString
+    } deriving Show
+
+-- | The contents of a variable file (minus standard
+-- header and checksum).
+newtype TIVarData = TIVarData {
+    varsData :: [TIVar]
+    } deriving Show
+
+-- | Scalar values can either be real or complex.
+-- Likewise, vectors, lists, etc can contain values
+-- of either.
+data VarField = VarReal | VarComplex deriving Show
+
+-- | Possible variable types.
+-- See also `Data.TI85.Var.Variable`.
+data VarType = VarUnknown
+    | VarValue VarField
+    | VarVector VarField
+    | VarList VarField
+    | VarMatrix VarField
+    | VarConstant VarField
+    | VarEquation
+    | VarString
+    | VarGDBFunc
+    | VarGDBPolar
+    | VarGDBParam
+    | VarGDBDiff
+    | VarPicture
+    | VarProgram
+    | VarDirectory
+    | VarSettingsFunc
+    | VarSettingsPolar
+    | VarSettingsParam
+    | VarSettingsDiff
+    | VarSavedWinSize
+    | VarMemory
+    deriving Show
+
+-- | Convert the variable ID word from
+-- a variable file into its type.
+-- From https://www.ticalc.org/pub/text/calcinfo/ti86prot.txt:
+--
+-- +---------+-------------------+
+-- | Type ID | Description       |
+-- +=========+===================+
+-- |      00 | Real Number       |
+-- +---------+-------------------+
+-- |      01 | Complex Number    |
+-- +---------+-------------------+
+-- |      02 | Real Vector       |
+-- +---------+-------------------+
+-- |      03 | Complex Vector    |
+-- +---------+-------------------+
+-- |      04 | Real List         |
+-- +---------+-------------------+
+-- |      05 | Complex List      |
+-- +---------+-------------------+
+-- |      06 | Real Matrix       |
+-- +---------+-------------------+
+-- |      07 | Complex Matrix    |
+-- +---------+-------------------+
+-- |      08 | Real Constant     |
+-- +---------+-------------------+
+-- |      09 | Complex Constant  |
+-- +---------+-------------------+
+-- |      0A | Equation          |
+-- +---------+-------------------+
+-- |      0C | String            |
+-- +---------+-------------------+
+-- |      0D | Function GDB      |
+-- +---------+-------------------+
+-- |      0E | Polar GDB         |
+-- +---------+-------------------+
+-- |      0F | Parametric GDB    |
+-- +---------+-------------------+
+-- |      10 | Differential      |
+-- |         | Equation GDB      |
+-- +---------+-------------------+
+-- |      11 | Picture           |
+-- +---------+-------------------+
+-- |      12 | Program           |
+-- +---------+-------------------+
+-- |      15 | Directory (only   |
+-- |         | used when         |
+-- |         | requesting dir)   |
+-- +---------+-------------------+
+-- |      17 | Function Window   |
+-- |         | Settings          |
+-- +---------+-------------------+
+-- |      18 | Polar Window      |
+-- |         | Settings          |
+-- +---------+-------------------+
+-- |      19 | Parametric Window |
+-- |         | Settings          |
+-- +---------+-------------------+
+-- |      1A | Differential      |
+-- |         | Equation Window   |
+-- |         | Settings          |
+-- +---------+-------------------+
+-- |      1B | Saved Window Size |
+-- |         | (ZRCL)            |
+-- +---------+-------------------+
+-- |      1D | Memory backup     |
+-- +---------+-------------------+
+-- |      1E | Unknown (only used|
+-- |         | when requesting   |
+-- |         | var)              |
+-- +---------+-------------------+
+--
+idToType :: Word8 -> VarType
+idToType 0x00 = VarValue VarReal
+idToType 0x01 = VarValue VarComplex
+idToType 0x02 = VarVector VarReal
+idToType 0x03 = VarVector VarComplex
+idToType 0x04 = VarList VarReal
+idToType 0x05 = VarList VarComplex
+idToType 0x06 = VarMatrix VarReal
+idToType 0x07 = VarMatrix VarComplex
+idToType 0x08 = VarConstant VarReal
+idToType 0x09 = VarConstant VarComplex
+idToType 0x0a = VarEquation
+idToType 0x0c = VarString
+idToType 0x0d = VarGDBFunc
+idToType 0x0e = VarGDBPolar
+idToType 0x0f = VarGDBParam
+idToType 0x10 = VarGDBDiff
+idToType 0x11 = VarPicture
+idToType 0x12 = VarProgram
+idToType 0x15 = VarDirectory
+idToType 0x17 = VarSettingsFunc
+idToType 0x18 = VarSettingsPolar
+idToType 0x19 = VarSettingsParam
+idToType 0x1a = VarSettingsDiff
+idToType 0x1b = VarSavedWinSize
+idToType 0x1d = VarMemory
+idToType 0x1e = VarUnknown
+idToType _ = VarUnknown
+
+-- | Convert the variable type into
+-- its ID. See `idToType`.
+typeToId :: VarType -> Word8
+typeToId (VarValue VarReal) = 0x00
+typeToId (VarValue VarComplex) = 0x01
+typeToId (VarVector VarReal) = 0x02
+typeToId (VarVector VarComplex) = 0x03
+typeToId (VarList VarReal) = 0x04
+typeToId (VarList VarComplex) = 0x05
+typeToId (VarMatrix VarReal) = 0x06
+typeToId (VarMatrix VarComplex) = 0x07
+typeToId (VarConstant VarReal) = 0x08
+typeToId (VarConstant VarComplex) = 0x09
+typeToId VarEquation = 0x0a
+typeToId VarString = 0x0c
+typeToId VarGDBFunc = 0x0d
+typeToId VarGDBPolar = 0x0e
+typeToId VarGDBParam = 0x0f
+typeToId VarGDBDiff = 0x10
+typeToId VarPicture = 0x11
+typeToId VarProgram = 0x12
+typeToId VarDirectory = 0x15
+typeToId VarSettingsFunc = 0x17
+typeToId VarSettingsPolar = 0x18
+typeToId VarSettingsParam = 0x19
+typeToId VarSettingsDiff = 0x1a
+typeToId VarSavedWinSize = 0x1b
+typeToId VarMemory = 0x1d
+typeToId VarUnknown = 0x1e
+
+
+-- | Convert a variable type to
+-- its textual representation.
+showType :: VarType -> Text
+showType VarUnknown = "Unknown"
+showType (VarValue VarReal) = "Real Value"
+showType (VarValue VarComplex) = "Complex Value"
+showType (VarVector VarReal) = "Real Vector"
+showType (VarVector VarComplex) = "Complex Vector"
+showType (VarList VarReal) = "Real List"
+showType (VarList VarComplex) = "Complex List"
+showType (VarMatrix VarReal) = "Real Matrix"
+showType (VarMatrix VarComplex) = "Complex Matrix"
+showType (VarConstant VarReal) = "Real Constant"
+showType (VarConstant VarComplex) = "Complex Constant"
+showType VarEquation = "Equation"
+showType VarString = "String"
+showType VarGDBFunc = "Function GDB"
+showType VarGDBPolar = "Polar GDB"
+showType VarGDBParam = "Parametric GDB"
+showType VarGDBDiff = "Differential Equation GDB"
+showType VarPicture = "Picture"
+showType VarProgram = "Program"
+showType VarDirectory = "Directory"
+showType VarSettingsFunc = "Function Settings"
+showType VarSettingsPolar = "Polar Settings"
+showType VarSettingsParam = "Parametric Settings"
+showType VarSettingsDiff = "Differential Equation Settings"
+showType VarSavedWinSize = "Saved Window Size"
+showType VarMemory = "Memory Backup"
+
diff --git a/src/Data/TI85/IO.hs b/src/Data/TI85/IO.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/TI85/IO.hs
@@ -0,0 +1,90 @@
+-- | Various utilities for printing file and
+-- variable data.
+module Data.TI85.IO where
+
+import Prelude hiding (putStrLn)
+import Control.Monad
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as BS
+import Data.Text (Text, intercalate, pack)
+import Data.Text.Encoding (decodeLatin1)
+import Data.Text.IO (putStrLn)
+import Data.Word
+import Numeric (showHex)
+
+import Data.TI85.Encoding
+import Data.TI85.File
+import Data.TI85.Parsers
+import Data.TI85.Var
+
+-- | A backup file contains a variable table that lists
+-- all user data, and indexes into the user memory area.
+printVariableTable :: Word16 -> VarTable -> IO ()
+printVariableTable baseAddr vars = do
+    forM_ vars printEntry
+  where
+    printEntry (VarTableEntry idNum addr _ name) = do
+        let idName = (showType.idToType) idNum
+        let offset = addr - baseAddr 
+        putStrLn $ "Name: " <> tiDecode name <> "\n" <>
+            "Type: " <> idName <> "\n" <>
+            "Addr: " <> pack (showHex addr "") <> " (offset " <> (pack.show) offset <> ")\n"
+
+-- | User the variable table to lookup variable data
+-- in user memory and display it.
+extractVariableTable :: Word16 -> TIBackupData -> IO ()
+extractVariableTable baseAddr tiBackup = do
+    let table = varTable tiBackup
+    let userData = data2 tiBackup
+    let vars = readUserMem baseAddr table userData
+    forM_ table $ \entry@(VarTableEntry idNum addr _nameLen name) -> do
+        let var = readVarMem baseAddr entry userData
+        putStrLn $ "\n" <> (showType . idToType ) idNum <> " \"" <> tiDecode name <> "\""
+            <> " at 0x" <> hexify addr <> " (0x" <> hexify (addr-baseAddr) <> "):\n"
+        printVariable var
+  where
+    hexify :: Word16 -> Text
+    hexify w = pack (showHex w "")
+
+-- | Print file metadata.
+printFileSummary :: TIFile -> IO ()
+printFileSummary tiFile =
+    let hdr = tiHeader tiFile
+        check = tiChecksum tiFile
+        sig = decodeLatin1 $ hdrSig hdr
+        comment = decodeLatin1 $ hdrComment hdr
+        fileType = case tiData tiFile of
+            BackupData _ -> "Backup"
+            VariableData _ -> "Variable"
+    in do
+        putStrLn $
+            "\nTI " <> fileType <> " File <" <> sig <> ">\n" <>
+            "\"" <> comment <> "\"\n\n"
+        case tiData tiFile of
+            BackupData backupData -> printBackupSummary backupData
+            VariableData variableData -> printVariableSummary variableData
+
+-- | Print backup-specific top-level iniformation.
+printBackupSummary :: TIBackupData -> IO ()
+printBackupSummary tiBackup = do
+    let backupHdr = backupHeader tiBackup
+    let data2Addr = hdrData2Addr backupHdr
+    putStrLn $ pack $ "Data Section 1 (" <> show (data1Len tiBackup) <> "):"
+    putStrLn $ pack $ "Data Section 2 (" <> show (data2Len tiBackup) <> "):"
+    putStrLn $ pack $ "Data 2 Address: " <> showHex data2Addr "\n"
+
+-- | Print variable-specific metadata.
+printVariableSummary :: TIVarData -> IO ()
+printVariableSummary (TIVarData vars) =
+    putStrLn $
+        "Variables:\n" <>
+        intercalate "\n" (map varSummary vars)
+  where
+    varSummary :: TIVar -> Text
+    varSummary var =
+        let varIdStr = (pack.show.fromEnum) (varId var)
+            varIdType = (showType.idToType) (varId var)
+        in  "\tName: " <> tiDecode (varName var) <> "\n" <>
+            "\tType: " <> varIdType <> " (" <> varIdStr <> ")\n" <>
+            "\tLength : " <> (pack.show.fromEnum) (varDataLen var) <> "\n"
+
diff --git a/src/Data/TI85/Parsers.hs b/src/Data/TI85/Parsers.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/TI85/Parsers.hs
@@ -0,0 +1,539 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | This module contains the highest-level user-facing functions.
+-- Typically, code using this library will want to call `readTIFile`
+-- on a file name.
+module Data.TI85.Parsers (
+    -- * General TI Files
+    readTIFile,
+    parseTIHeader,
+    parseTIFile,
+    -- * Backup Files
+    -- ** High-level Parsers
+    parseTIBackupHeader,
+    readUserMem,
+    -- ** Lower-level Parsers
+    readVarMem,
+    extractVar,
+    -- * Variable Files
+    -- ** High-level Parsers
+    readVariable,
+    parseVariable,
+    -- ** Lower-level Parsers
+    parseProgram,
+    parseTINumber,
+    parseToken
+    ) where
+
+import Prelude hiding (take,takeWhile,putStrLn)
+import Data.Char
+import Data.Bits
+import Data.Word
+import Data.Array.Unboxed (Array, UArray, array, listArray, (!))
+import Data.ByteString (ByteString, foldr')
+import qualified Data.ByteString as BS
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Text.IO (putStrLn)
+import Data.Attoparsec.ByteString
+import Data.Attoparsec.Combinator (lookAhead)
+import Control.Applicative
+import Control.Monad (guard, when, forM)
+
+import Data.TI85.Var
+import Data.TI85.Var.Pic
+import Data.TI85.Encoding
+import Data.TI85.Token
+import Data.TI85.File
+
+bytes2Int :: ByteString -> Int
+bytes2Int = BS.foldr' (\w x -> 256*x + fromEnum w) 0
+
+anyWord16 :: Parser Word16
+anyWord16 = do
+    bytes <- take 2
+    let val = bytes2Int bytes
+    return (toEnum val)
+
+-- | Parse a general TI file, which might be
+-- a variable file or backup file.
+parseTIFile :: Parser TIFile
+parseTIFile = do
+    header <- parseTIHeader
+    (checksum, calcSum) <- lookAhead (calculateSum $ hdrDataLen header)
+
+    when (calcSum /= checksum) (error "Checkum check failed")
+
+    contents <- 
+        BackupData <$> parseTIBackupData
+        <|> VariableData <$> parseTIVarData
+
+    return $ TIFile {
+        tiHeader = header,
+        tiData = contents,
+        tiChecksum = checksum
+        }
+  where
+    calculateSum :: Word16 -> Parser (Word16,Word16)
+    calculateSum len = do
+        bytes <- take (fromEnum len)
+        let sum = foldr' (\w s -> fromEnum w + s) (0::Int) bytes
+        checksum <- anyWord16
+        return (checksum, toEnum $ sum .&. 0xffff)
+
+-- | Read a general TI file, which might be
+-- a variable file or backup file.
+readTIFile :: FilePath -> IO TIFile
+readTIFile fileName = do
+    contents <- BS.readFile fileName
+    let tiFile = parseOnly parseTIFile contents
+    either error return tiFile
+
+-- | The backup header is a second header after
+-- the top-level file header (`TIFile`).
+-- It contains sizes of the three data sections
+-- and the location of user memory in RAM (which
+-- can use used with the variable table to look
+-- up variable data).
+parseTIBackupHeader :: Parser TIBackupHeader
+parseTIBackupHeader = do
+    dataOffset <- string "\x09\x00"
+    len1 <- anyWord16
+    typeId <- word8 0x1d
+    len2 <- anyWord16
+    len3 <- anyWord16
+    addr <- anyWord16
+    return $ TIBackupHeader {
+        hdrDataLenOffset = 0x9,
+        hdrData1Len = len1,
+        hdrTypeID = typeId,
+        hdrData2Len = len2,
+        hdrData3Len = len2,
+        hdrData2Addr = addr
+        }
+
+parseVarTableEntry :: Parser VarTableEntry
+parseVarTableEntry = do
+    entId <- anyWord8
+    entAddr <- anyWord16
+    len <- anyWord8
+    name <- take (fromEnum len)
+    return $ VarTableEntry {
+        entryId = entId,
+        entryAddr = entAddr,
+        entryNameLen = len,
+        entryName = name
+        }
+
+parseTIBackupData :: Parser TIBackupData
+parseTIBackupData = do
+    backupHdr <- parseTIBackupHeader
+    len1 <- anyWord16
+    data1 <- take $ fromEnum len1
+    len2 <- anyWord16
+    data2 <- take $ fromEnum len2
+    len3 <- anyWord16
+    data3 <- take $ fromEnum len3
+    let vars = parseOnly (many' parseVarTableEntry) (BS.reverse data3)
+    return $ TIBackupData {
+        backupHeader = backupHdr,
+        data1Len = len1,
+        data1 = data1,
+        data2Len = len2,
+        data2 = data2,
+        varTableLen = len3,
+        varTable = either error id vars
+        }
+
+-- |The TI-85 header is common between backup files
+-- and variable files.
+--
+-- +--------+---+---------------------------------+
+-- | 8 Byte | 3 | 42 Byte                         |
+-- +========+===+=================================+
+-- |**TI85**|xyz| Comment                         |
+-- +--------+---+---------------------------------+
+--
+-- where @xyz@ is always @0x1a,0x0c,0x00@.
+parseTIHeader :: Parser TIHeader
+parseTIHeader = do
+    string "**TI85**"
+    string "\x1a\x0c\x00"
+    rawComment <- take 42
+    dataLen <- anyWord16
+    return $ TIHeader {
+        hdrSig = "**TI85**",
+        hdrSig2 = "\x1a\x0c\x00",
+        hdrComment = BS.takeWhile (/= 0x0) rawComment,
+        hdrDataLen = dataLen
+        }
+
+parseTIVar :: Parser TIVar
+parseTIVar = do
+    offset <- anyWord16
+    len <- anyWord16
+    varType <- anyWord8
+    nameLen <- anyWord8
+    name <- take (fromEnum nameLen)
+    dataLen <- anyWord16
+    var <- take (fromEnum dataLen)
+    return $ TIVar {
+       varOffset = offset,
+       varLen = len,
+       varId = varType,
+       varNameLen = nameLen,
+       varName = name,
+       varDataLen = dataLen,
+       varData = var
+       }
+
+parseTIVarData :: Parser TIVarData
+parseTIVarData = TIVarData <$> many1' parseTIVar
+
+-- |Programs are either plain text, ot encoded in a
+-- tokenized format. See "Data.TI85.Token" for the 
+-- mapping.
+parseProgram :: Parser Program
+parseProgram = do
+    len <- fromEnum <$> anyWord16
+    encoding <- peekWord8'
+    case encoding of
+        0x00 -> do
+            anyWord8
+            PlainText <$> parsePlaintext (len-1)
+        _ -> Tokenized <$> parseTokenized len
+
+parsePlaintext :: Int -> Parser Text
+parsePlaintext len = do
+    bytes <- take len
+    return $ tiDecode bytes
+
+parseTokenized :: Int -> Parser [Token]
+parseTokenized len = do
+    bytes <- take len
+    let tokenResult = parseOnly (many' parseToken) bytes
+    either error return tokenResult
+        
+-- |Interpret data as a token (or token stream)
+parseToken :: Parser Token
+parseToken = do
+    tokenByte <- anyWord8
+    let token = tokenTable ! tokenByte
+    tokenText <- case token of
+        Invalid -> return T.empty
+        Fixed t -> return t
+        QuoteText -> do
+            t <- zeroTerminated
+            return $  '"' `T.cons` t `T.snoc` '"'
+        NameLength -> varLength
+        FixedLength n -> tiDecode <$> take n
+        Extended -> do
+            extToken <- anyWord8
+            case tokenTableExtended ! extToken of
+                Fixed ext -> return ext
+                _ -> return T.empty
+        Conversion -> do
+            unit1 <- varLength
+            unit2 <- varLength
+            return $ T.intercalate "->" [unit1,unit2]
+        Literal -> zeroTerminated
+        Label -> do
+            label <- zeroTerminated
+            return $ "Lbl " <> label
+        Goto -> do
+            mystery <- anyWord16
+            label <- zeroTerminated
+            return $ "Goto " <> label
+    return $ Token token tokenText
+  where
+    zeroTerminated = do
+        bytes <- takeWhile (/= 0x0)
+        zero <- word8 0x0
+        return $ tiDecode bytes
+    varLength = do
+        len <- anyWord8
+        bytes <- take (fromEnum len)
+        return $ tiDecode bytes
+
+bcd :: ByteString -> Double
+bcd bytes = snd $ BS.foldl' accDecimal (0,0.0) bytes
+  where
+    accDecimal :: (Int, Double) -> Word8 -> (Int, Double)
+    accDecimal (place, value) byte =
+        let (high,low) = toNibbles byte
+            highVal = fromIntegral high * 10.0 ** fromIntegral place
+            lowVal = fromIntegral low * 10.0 ** fromIntegral (place-1)
+        in (place-2, value+highVal+lowVal)
+    toNibbles :: Word8 -> (Word8,Word8)
+    toNibbles byte =
+        let low = byte .&. 0x0f
+            high = shiftR byte 4 .&. 0x0f
+        in (high,low)
+
+-- |Numeric values can either be real or complex, and most variable
+-- types can store either.
+parseTINumber :: Parser TINumber
+parseTINumber = do
+    (realFlags, realPart) <- parseReal
+    let isComplex = testBit realFlags 0
+    if isComplex
+        then do
+            (_imagFlags, imagPart) <- parseReal
+            return $ TIComplex realPart imagPart
+        else return $ TIReal realPart
+  where
+    parseReal :: Parser (Word8, Double)
+    parseReal = do
+        flags <- anyWord8
+        expRaw <- anyWord16
+        let exp = fromEnum expRaw - 0xFC00
+        mantissa <- bcd <$> take 7
+        let sign = testBit flags 7
+        let value = mantissa * 10 ** fromIntegral exp
+        let signedValue = if sign
+            then -1.0 * value
+            else value
+        return (flags,signedValue)
+
+parsePicture :: Parser TIBitmap
+parsePicture = do
+    word8 0xf0
+    word8 0x03
+    pic <- take 1008
+    case fromBytes pic of
+        Just tiPic -> return tiPic
+        Nothing -> fail "Wrong size of picture bitmap"
+
+-- |Parser for the elements contained a variable file
+-- that has possibly more than one 
+parseVariable :: VarType -> Parser Variable
+parseVariable (VarValue _) = TIScalar <$> parseTINumber
+parseVariable (VarVector _) = do
+    alwaysOne <- anyWord8
+    len <- fromEnum <$> anyWord8
+    vals <- count len parseTINumber
+    return $ TIVector vals
+parseVariable (VarList _) = do
+    len <- fromEnum <$> anyWord16
+    vals <- count len parseTINumber
+    return $ TIList vals
+parseVariable (VarMatrix _) = do
+    numCols <- fromEnum <$> anyWord8
+    numRows <- fromEnum <$> anyWord8
+    vals <- count numRows (count numCols parseTINumber)
+    return $ TIMatrix vals
+parseVariable (VarConstant _) = TIConstant <$> parseTINumber
+parseVariable VarEquation = do
+    len <- fromEnum <$> anyWord16
+    tokens <- parseTokenized len
+    let tokenText = map (\(Token _ t) -> t) tokens
+    let eqText = T.concat tokenText
+    return $ TIEquation eqText
+parseVariable VarString = do
+    len <- fromEnum <$> anyWord16
+    TIString <$> parsePlaintext len
+parseVariable VarProgram = TIProgram <$> parseProgram
+parseVariable VarPicture = TIPicture <$> parsePicture
+parseVariable VarSettingsFunc = TIFuncSettings <$> parseFuncSettings
+parseVariable VarSettingsPolar = TIPolarSettings <$> parsePolarSettings
+parseVariable VarSettingsParam = TIParamSettings <$> parseParamSettings
+parseVariable VarSettingsDiff = TIDiffEqSettings <$> parseDiffEqSettings
+parseVariable VarSavedWinSize = TIZRCL <$> parseWinSettings
+parseVariable VarGDBFunc = TIFuncGDB <$> parseFuncGDB
+parseVariable VarUnknown = return $ TIString "?"
+parseVariable _ = return $ TIString "(not implemented)"
+
+parseModeSettings :: Parser ModeSettings
+parseModeSettings = do
+    modeByte <- anyWord8
+    let drawDot = testBit modeByte 0
+        simulG = testBit modeByte 1
+        gridOn = testBit modeByte 2
+        polarGC = testBit modeByte 3
+        coordOff = testBit modeByte 4
+        axesOff = testBit modeByte 5
+        labelOb = testBit modeByte 6
+    return $ ModeSettings {
+        modeDrawDot = drawDot,
+        modeSimulG = simulG,
+        modeGridOn = gridOn,
+        modePolarGC = polarGC,
+        modeCoordOff = coordOff,
+        modeAxesOff = axesOff,
+        modeLabelOn = labelOb
+        }
+
+parseFuncGDB :: Parser (GDB Func)
+parseFuncGDB = do
+    len <- anyWord16
+    mode <- parseModeSettings
+    settings <- parseBareFuncSettings
+    numFunc <- fromEnum <$> satisfy (<=99)
+    lib <- count numFunc parseFuncEntry
+    return $ GDB {
+        gdbMode = mode,
+        gdbSettings = settings,
+        gdbLib = lib
+        }
+
+parseFuncEntry :: Parser (GDBLibEntry Func)
+parseFuncEntry = do
+    idByte <- anyWord8
+    let selected = testBit idByte 7
+        funcId = idByte .&. 0x7f
+    (TIEquation eqn) <- parseVariable VarEquation
+    return $ GDBLibEntry {
+        libId = fromEnum funcId,
+        libSelected = selected,
+        libEqn = eqn
+        }
+{-
+0 	2 bytes 	Length, in bytes, of GDB, minus two.
+2 	1 byte 	Mode settings (see mode setting table below)
+3 	10 bytes 	A real number: xMin
+13 (Dh) 	10 bytes 	A real number: xMax
+23 (17h) 	10 bytes 	A real number: xScl
+33 (21h) 	10 bytes 	A real number: yMin
+43 (2Bh) 	10 bytes 	A real number: yMax
+53 (35h) 	10 bytes 	A real number: yScl
+63 (3Fh) 	10 bytes 	A real number: xRes
+73 (49h) 	1 byte 	Number of functions defined (up to 99)
+74 (4Ah) 	n bytes 	Function table - several function definitions one after another (see function table below)
+-}
+
+parseBareFuncSettings :: Parser FuncSettings
+parseBareFuncSettings = do
+    FuncSettings
+        <$> parseTINumber -- fXMin
+        <*> parseTINumber -- fXMax
+        <*> parseTINumber -- fXScl
+        <*> parseTINumber -- fYMin
+        <*> parseTINumber -- fYMax
+        <*> parseTINumber -- fYScl
+
+parseFuncSettings :: Parser FuncSettings
+parseFuncSettings = do
+    word8 0x51
+    word8 0x00
+    anyWord8
+    val <- parseBareFuncSettings
+    take 20
+    return val
+
+parsePolarSettings :: Parser PolarSettings
+parsePolarSettings = do
+    word8 0x5b
+    word8 0x00
+    anyWord8
+    PolarSettings
+        <$> parseTINumber -- polThetaMin
+        <*> parseTINumber -- polThetaMax
+        <*> parseTINumber -- polThetaStep
+        <*> parseTINumber -- polXMin
+        <*> parseTINumber -- polXMax
+        <*> parseTINumber -- polXScl
+        <*> parseTINumber -- polYMin
+        <*> parseTINumber -- polYMax
+        <*> parseTINumber -- polYScl
+
+parseParamSettings :: Parser ParamSettings
+parseParamSettings = do
+    word8 0x5b
+    word8 0x00
+    anyWord8
+    ParamSettings
+        <$> parseTINumber -- parTMin
+        <*> parseTINumber -- parTMax
+        <*> parseTINumber -- parTStep
+        <*> parseTINumber -- parXMin
+        <*> parseTINumber -- parXMax
+        <*> parseTINumber -- parXScl
+        <*> parseTINumber -- parYMin
+        <*> parseTINumber -- parYMax
+        <*> parseTINumber -- parYScl
+
+parseDiffEqSettings :: Parser DiffEqSettings
+parseDiffEqSettings = do
+    word8 0x71
+    word8 0x00
+    anyWord8
+    DiffEqSettings
+        <$> parseTINumber -- diffTol
+        <*> parseTINumber -- diffTPlot
+        <*> parseTINumber -- diffTMin
+        <*> parseTINumber -- diffTMax
+        <*> parseTINumber -- diffTStep
+        <*> parseTINumber -- diffXMin
+        <*> parseTINumber -- diffXMax
+        <*> parseTINumber -- diffXScl
+        <*> parseTINumber -- diffYMin
+        <*> parseTINumber -- diffYMax
+        <*> parseTINumber -- diffYScl
+        <*> parseAxis -- diffXAxis
+        <*> parseAxis -- diffYAxis
+  where
+    parseAxis :: Parser DiffEqAxis
+    parseAxis = do
+        axisCode <- anyWord8
+        return $ case axisCode of
+            0x0 -> AxisT
+            w | w >= 0x10 && w <= 0x19 ->
+                AxisQ (toEnum (fromEnum w - 0x10))
+            w | w >= 0x20 && w <= 0x29 ->
+                AxisQ' (toEnum (fromEnum w - 0x20))
+            _ -> error "Invalid axis code"
+
+parseWinSettings :: Parser SavedWinSettings
+parseWinSettings = do
+    word8 0x82
+    word8 0x00
+    SavedWinSettings
+        <$> parseTINumber -- zThetaMin
+        <*> parseTINumber -- zThetaMax
+        <*> parseTINumber -- zThetaStep
+        <*> parseTINumber -- ztPlot
+        <*> parseTINumber -- ztMin
+        <*> parseTINumber -- ztMax
+        <*> parseTINumber -- ztStep
+        <*> parseTINumber -- zxMin
+        <*> parseTINumber -- zxMax
+        <*> parseTINumber -- zxScl
+        <*> parseTINumber -- zyMin
+        <*> parseTINumber -- zyMax
+        <*> parseTINumber -- zyScl
+
+parseUserMem :: Word16 -> VarTable -> Parser [Variable]
+parseUserMem baseAddr vars = do
+    forM vars (extractVar baseAddr)
+
+-- | Parse a variable out of user memory, without
+-- consuming any input (hence "extract"). This allows
+-- many calls on the same memory. The base address
+-- is required for converting the variable addresses
+-- in the table to offsets into the user memory backup.
+extractVar :: Word16 -> VarTableEntry -> Parser Variable
+extractVar baseAddr (VarTableEntry idNum addr _ name) = do
+    let offset = fromEnum $ addr - baseAddr
+    lookAhead $ take offset *> parseVariable (idToType idNum)
+
+-- | Given a bytestring of user memory, look up the variable
+-- table entry and return a `Variable`.
+readVarMem :: Word16 -> VarTableEntry -> ByteString -> Variable
+readVarMem baseAddr entry userData =
+    let var = parseOnly (extractVar baseAddr entry) userData
+    in either error id var
+
+-- | Read all ofthe variables contained in a user memory
+-- backup, according to a variable table.
+readUserMem :: Word16 -> VarTable -> ByteString -> [Variable]
+readUserMem baseAddr table userData =
+    let vars = parseOnly (parseUserMem baseAddr table) userData
+    in either error id vars
+
+-- |Convert raw variable data into a Variable.
+readVariable :: TIVar -> Variable
+readVariable var =
+    let varType = idToType (varId var)
+        parsedVar = parseOnly (parseVariable varType) (varData var)
+    in either error id parsedVar
+
diff --git a/src/Data/TI85/Token.hs b/src/Data/TI85/Token.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/TI85/Token.hs
@@ -0,0 +1,348 @@
+
+-- | As a kind of object file or byte code, TI-85 programs can be
+-- represented as list of tokens.
+--
+-- The contents of this module describe the possible tokens, and a way
+-- to convert from byte to token.
+module Data.TI85.Token where
+
+import Data.Word
+import Data.Array.Unboxed (Array, array)
+import Data.Text (Text)
+
+-- | There are several types of tokens. Most come
+-- with some instructions for what do to with the
+-- bytes following the token.
+data TokenDef = Invalid
+    | Fixed Text      -- ^ Representation of specific text
+    | QuoteText       -- ^ Quoted string
+    | NameLength      -- ^ A string specified by length
+    | FixedLength Int -- ^ A fixed-length string
+    | Extended        -- ^ Look up the next token in the extended token table
+    | Conversion      -- ^ Unit conversion
+    | Literal         -- ^ Literal value, represented as text
+    | Label           -- ^ Lbl, with a text label
+    | Goto            -- ^ Goto, with a text label
+    deriving Show
+
+-- | Mapping from byte to TokenDef.
+tokenTable :: Array Word8 TokenDef
+tokenTable = array (0x00,0xff) [
+    (0x00, Invalid),
+    (0x01, Fixed "▸Rec"),
+    (0x02, Fixed "▸Pol"),
+    (0x03, Fixed "▸Cyl"),
+    (0x04, Fixed "▸Sph"),
+    (0x05, Fixed "▸DMS"),
+    (0x06, Fixed "▸Bin"),
+    (0x07, Fixed "▸Hex"),
+    (0x08, Fixed "▸Oct"),
+    (0x09, Fixed "▸Dec"),
+    (0x0A, Fixed "▸Frac"),
+    (0x0B, Fixed "→"),
+    (0x0C, Fixed "["),
+    (0x0D, Fixed "]"),
+    (0x0E, Fixed "{"),
+    (0x0F, Fixed "}"),
+    (0x10, Fixed "("),
+    (0x11, Fixed ")"),
+    (0x12, Fixed "round"),
+    (0x13, Fixed "max"),
+    (0x14, Fixed "min"),
+    (0x15, Fixed "mod"),
+    (0x16, Fixed "cross"),
+    (0x17, Fixed "dot"),
+    (0x18, Fixed "aug"),
+    (0x19, Fixed "rSwap"),
+    (0x1A, Fixed "rAdd"),
+    (0x1B, Fixed "multR"),
+    (0x1C, Fixed "mRAdd"),
+    (0x1D, Fixed "sub"),
+    (0x1E, Fixed "lcm"),
+    (0x1F, Fixed "gcd"),
+    (0x20, Fixed "simult"),
+    (0x21, Fixed "inter"),
+    (0x22, Fixed "pEval"),
+    (0x23, Fixed "randM"),
+    (0x24, Fixed "seq"),
+    (0x25, Fixed "evalF"),
+    (0x26, Fixed "fnInt"),
+    (0x27, Fixed "arc"),
+    (0x28, Fixed "fMin"),
+    (0x29, Fixed "fMax"),
+    (0x2A, Fixed "der1"),
+    (0x2B, Fixed "der2"),
+    (0x2C, Fixed "nDer"),
+    (0x2D, QuoteText),
+    (0x2E, Fixed "∠"),
+    (0x2F, Fixed ","),
+    (0x30, Fixed " or "),
+    (0x31, Fixed " xor "),
+    (0x32, NameLength),
+    (0x33, FixedLength 1),
+    (0x34, FixedLength 2),
+    (0x35, FixedLength 3),
+    (0x36, FixedLength 4),
+    (0x37, FixedLength 5),
+    (0x38, FixedLength 6),
+    (0x39, FixedLength 7),
+    (0x3A, FixedLength 8),
+    (0x3B, NameLength),
+    (0x3C, NameLength),
+    (0x3D, Extended),
+    (0x3E, Conversion),
+    (0x3F, Fixed "="),
+    (0x40, Fixed " and "),
+    (0x41, Fixed "rand"),
+    (0x42, Fixed "π"),
+    (0x43, Fixed "getKy"),
+    (0x44, Literal),
+    (0x45, Fixed "%"),
+    (0x46, Fixed "!"),
+    (0x47, Fixed "ʳ"),
+    (0x48, Fixed "°"),
+    (0x49, Fixed "⁻¹"),
+    (0x4A, Fixed "²"),
+    (0x4B, Fixed "ᵀ"),
+    (0x4C, Fixed "Menu"),
+    (0x4D, Fixed "P2Reg"),
+    (0x4E, Fixed "P3Reg"),
+    (0x4F, Fixed "P4Reg"),
+    (0x50, Fixed "=="),
+    (0x51, Fixed "<"),
+    (0x52, Fixed ">"),
+    (0x53, Fixed "<="),
+    (0x54, Fixed ">="),
+    (0x55, Fixed "≠"),
+    (0x56, Fixed "Radian"),
+    (0x57, Fixed "Degree"),
+    (0x58, Fixed "Normal"),
+    (0x59, Fixed "Sci"),
+    (0x5A, Fixed "Eng"),
+    (0x5B, Fixed "Float"),
+    (0x5C, Fixed "Fix"),
+    (0x5D, Fixed "RectV"),
+    (0x5E, Fixed "CylV"),
+    (0x5F, Fixed "SphereV"),
+    (0x60, Fixed "+"),
+    (0x61, Fixed "-"),
+    (0x62, Fixed "Func"),
+    (0x63, Fixed "Param"),
+    (0x64, Fixed "Pol"),
+    (0x65, Fixed "DifEq"),
+    (0x66, Fixed "Bin"),
+    (0x67, Fixed "Oct"),
+    (0x68, Fixed "Hex"),
+    (0x69, Fixed "Dec"),
+    (0x6A, Fixed "RectC"),
+    (0x6B, Fixed "PolarC"),
+    (0x6C, Fixed "dxDer1"),
+    (0x6D, Fixed "dxNDer"),
+    (0x6E, Fixed ":"),
+    (0x6F, Fixed "\n"),
+    (0x70, Fixed "*"),
+    (0x71, Fixed "/"),
+    (0x72, Fixed "SeqG"),
+    (0x73, Fixed "SimulG"),
+    (0x74, Fixed "PolarGC"),
+    (0x75, Fixed "RectGC"),
+    (0x76, Fixed "CoordOn"),
+    (0x77, Fixed "CoordOff"),
+    (0x78, Fixed "DrawLine"),
+    (0x79, Fixed "DrawDot"),
+    (0x7A, Fixed "AxesOn"),
+    (0x7B, Fixed "AxesOff"),
+    (0x7C, Fixed "GridOn"),
+    (0x7D, Fixed "GridOff"),
+    (0x7E, Fixed "LabelOn"),
+    (0x7F, Fixed "LabelOff"),
+    (0x80, Fixed "nPr"),
+    (0x81, Fixed "nCr"),
+    (0x82, Fixed "Trace"),
+    (0x83, Fixed "ClDrw"),
+    (0x84, Fixed "ZStd"),
+    (0x85, Fixed "ZTrig"),
+    (0x86, Fixed "ZFit"),
+    (0x87, Fixed "ZIn"),
+    (0x88, Fixed "ZOut"),
+    (0x89, Fixed "ZSqr"),
+    (0x8A, Fixed "ZInt"),
+    (0x8B, Fixed "ZPrev"),
+    (0x8C, Fixed "ZDecm"),
+    (0x8D, Fixed "ZRcl"),
+    (0x8E, Fixed "PrtScrn"),
+    (0x8F, Fixed "DrawF"),
+    (0x90, Fixed "FnOn "),
+    (0x91, Fixed "FnOff "),
+    (0x92, Fixed "StPic"),
+    (0x93, Fixed "RcPic"),
+    (0x94, Fixed "StGDB"),
+    (0x95, Fixed "RcGDB"),
+    (0x96, Fixed "Line"),
+    (0x97, Fixed "Vert"),
+    (0x98, Fixed "PtOn"),
+    (0x99, Fixed "PtOff"),
+    (0x9A, Fixed "PtChg"),
+    (0x9B, Fixed "Shade"),
+    (0x9C, Fixed "Circl"),
+    (0x9D, Fixed "Axes"),
+    (0x9E, Fixed "TanLn"),
+    (0x9F, Fixed "DrInv"),
+    (0xA0, Fixed "√"),
+    (0xA1, Fixed "-"),
+    (0xA2, Fixed "abs"),
+    (0xA3, Fixed "iPart"),
+    (0xA4, Fixed "fPart"),
+    (0xA5, Fixed "int"),
+    (0xA6, Fixed "ln"),
+    (0xA7, Fixed "e^"),
+    (0xA8, Fixed "log"),
+    (0xA9, Fixed "10^"),
+    (0xAA, Fixed "sin "),
+    (0xAB, Fixed "sin⁻¹ "),
+    (0xAC, Fixed "cos "),
+    (0xAD, Fixed "cos⁻¹ "),
+    (0xAE, Fixed "tan "),
+    (0xAF, Fixed "tan⁻¹ "),
+    (0xB0, Fixed "sinh "),
+    (0xB1, Fixed "sinh⁻¹ "),
+    (0xB2, Fixed "cosh "),
+    (0xB3, Fixed "cosh⁻¹ "),
+    (0xB4, Fixed "tanh "),
+    (0xB5, Fixed "tanh⁻¹ "),
+    (0xB6, Fixed "sign "),
+    (0xB7, Fixed "det "),
+    (0xB8, Fixed "ident"),
+    (0xB9, Fixed "unitV"),
+    (0xBA, Fixed "norm"),
+    (0xBB, Fixed "rnorm"),
+    (0xBC, Fixed "cnorm"),
+    (0xBD, Fixed "ref"),
+    (0xBE, Fixed "rref"),
+    (0xBF, Fixed "dim"),
+    (0xC0, Fixed "dimL"),
+    (0xC1, Fixed "sum"),
+    (0xC2, Fixed "prod"),
+    (0xC3, Fixed "sortA"),
+    (0xC4, Fixed "sortD"),
+    (0xC5, Fixed "li▸vc"),
+    (0xC6, Fixed "vc▸li"),
+    (0xC7, Fixed "lngth"),
+    (0xC8, Fixed "conj"),
+    (0xC9, Fixed "real"),
+    (0xCA, Fixed "imag"),
+    (0xCB, Fixed "angle"),
+    (0xCC, Fixed "not"),
+    (0xCD, Fixed "rotR"),
+    (0xCE, Fixed "rotL"),
+    (0xCF, Fixed "shftR"),
+    (0xD0, Fixed "shftL"),
+    (0xD1, Fixed "eigVl"),
+    (0xD2, Fixed "eigVc"),
+    (0xD3, Fixed "cond"),
+    (0xD4, Fixed "poly"),
+    (0xD5, Fixed "fcstx"),
+    (0xD6, Fixed "fcsty"),
+    (0xD7, Fixed "eval "),
+    (0xD8, Fixed "If "),
+    (0xD9, Fixed "Then"),
+    (0xDA, Fixed "Else"),
+    (0xDB, Fixed "While "),
+    (0xDC, Fixed "Repeat "),
+    (0xDD, Fixed "For"),
+    (0xDE, Fixed "End"),
+    (0xDF, Fixed "Return"),
+    (0xE0, Label),
+    (0xE1, Goto),
+    (0xE2, Fixed "Pause"),
+    (0xE3, Fixed "Stop"),
+    (0xE4, Fixed "IS>"),
+    (0xE5, Fixed "DS<"),
+    (0xE6, Fixed "Input "),
+    (0xE7, Fixed "Prompt "),
+    (0xE8, Fixed "InpSt "),
+    (0xE9, Fixed "Disp "),
+    (0xEA, Fixed "DispG"),
+    (0xEB, Fixed "Outpt"),
+    (0xEC, Fixed "ClLCD"),
+    (0xED, Fixed "Eq▸St"),
+    (0xEE, Fixed "St▸Eq"),
+    (0xEF, Fixed "Fill"),
+    (0xF0, Fixed "^"),
+    (0xF1, Fixed "ˣ√"),
+    (0xF2, Fixed "Solver"),
+    (0xF3, Fixed "OneVar"),
+    (0xF4, Fixed "LinR"),
+    (0xF5, Fixed "ExpR"),
+    (0xF6, Fixed "LnR"),
+    (0xF7, Fixed "PwrR"),
+    (0xF8, Fixed "ShwSt"),
+    (0xF9, Fixed "Hist"),
+    (0xFA, Fixed "xyLine"),
+    (0xFB, Fixed "Scatter"),
+    (0xFC, Fixed "SortX"),
+    (0xFD, Fixed "SortY"),
+    (0xFE, Fixed "LU"),
+    (0xFF, Invalid)
+    ]
+
+-- | Extended table of tokens.
+-- When the token is 'Extended' this table
+-- is used to look up the final token.
+tokenTableExtended :: Array Word8 TokenDef
+tokenTableExtended = array (0x00,0x35) [
+    (0x00, Fixed "zxScl"),
+    (0x01, Fixed "zyScl"),
+    (0x02, Fixed "xScl"),
+    (0x03, Fixed "yScl"),
+    (0x04, Fixed "xMin"),
+    (0x05, Fixed "xMax"),
+    (0x06, Fixed "yMin"),
+    (0x07, Fixed "yMax"),
+    (0x08, Fixed "tMin"),
+    (0x09, Fixed "tMax"),
+    (0x0a, Fixed "tStep"),
+    (0x0b, Fixed "θStep"),
+    (0x0c, Fixed "ztStep"),
+    (0x0d, Fixed "zθStep"),
+    (0x0e, Fixed "tPlot"),
+    (0x0f, Fixed "θMin"),
+    (0x10, Fixed "θMax"),
+    (0x11, Fixed "zxMin"),
+    (0x12, Fixed "zxMax"),
+    (0x13, Fixed "zyMin"),
+    (0x14, Fixed "zyMax"),
+    (0x15, Fixed "ztPlot"),
+    (0x16, Fixed "zθMin"),
+    (0x17, Fixed "zθMax"),
+    (0x18, Fixed "ztMin"),
+    (0x19, Fixed "ztMax"),
+    (0x1a, Fixed "lower"),
+    (0x1b, Fixed "upper"),
+    (0x1c, Fixed "Δx"),
+    (0x1d, Fixed "Δy"),
+    (0x1e, Fixed "xFact"),
+    (0x1f, Fixed "yFact"),
+    (0x20, Fixed "difTol"),
+    (0x21, Fixed "tol"),
+    (0x22, Fixed "δ"),
+    (0x23, Fixed "Na"),
+    (0x24, Fixed "k"),
+    (0x25, Fixed "Cc"),
+    (0x26, Fixed "ec"),
+    (0x27, Fixed "Rc"),
+    (0x28, Fixed "Gc"),
+    (0x29, Fixed "g"),
+    (0x2a, Fixed "Me"),
+    (0x2b, Fixed "Mp"),
+    (0x2c, Fixed "Mn"),
+    (0x2d, Fixed "µ0"),
+    (0x2e, Fixed "ε0"),
+    (0x2f, Fixed "h"),
+    (0x30, Fixed "c"),
+    (0x31, Fixed "u"),
+    (0x32, Fixed "e"),
+    (0x33, Fixed "xStat"),
+    (0x34, Fixed "yStat"),
+    (0x35, Fixed "Plot")
+    ]
diff --git a/src/Data/TI85/Var.hs b/src/Data/TI85/Var.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/TI85/Var.hs
@@ -0,0 +1,463 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE TypeFamilies #-}
+
+-- | Specifics of the TI-85 variables themselves
+-- (i.e. not their representation in the file).
+module Data.TI85.Var (
+    -- * Types
+    Variable(..),
+    TINumber(..),
+    -- ** Program
+    Program(..),
+    Token(..),
+    -- ** Window Settings
+    FuncSettings(..),
+    PolarSettings(..),
+    ParamSettings(..),
+    SavedWinSettings(..),
+    -- *** Differential Equations
+    DiffEqSettings(..),
+    DiffEqAxis(..),
+    AxisInd(..),
+    -- ** Graph Database
+    ModeSettings(..),
+    GraphMode(..),
+    FuncEqn(..),
+    ParamEqn(..),
+    DiffEqEqn(..),
+    GDBLibEntry(..),
+    GDBEqn(..),
+    GDBSettings(..),
+    GDB(..),
+    HasGDB,
+
+    -- * Text Conversion
+    showVariable,
+    -- ** Variable-specific
+    showNumber,
+    showProgram,
+    showFuncSettings,
+    showPolarSettings,
+    showParamSettings,
+    showDiffEqSettings,
+    showWinSettings,
+    showGDB,
+    showGDBMode,
+
+    -- * IO
+    printVariable
+    ) where
+
+import Prelude hiding (concat, putStrLn)
+import Data.Text (Text, concat, pack, intercalate, unpack)
+import Data.Text.IO (putStrLn)
+import Data.TI85.Token (TokenDef)
+import Data.TI85.Var.Pic
+
+-- * Types
+
+-- | An instance of a token from the `TokenDef`
+-- table. This will include actual text that goes
+-- along with a token, when it is not a fixed-text
+-- token.
+data Token = Token TokenDef Text deriving Show
+
+-- | A program is either stored as plaintext
+-- (in the TI-86 codepage; see `Data.TI85.Encoding.tiDecode`)
+-- or a list of tokens. The two are represented here to
+-- maintain that information.
+data Program = PlainText Text
+    | Tokenized [Token]
+    deriving Show
+
+-- | Numerical variables are either Real or Complex.
+data TINumber = TIReal Double | TIComplex Double Double deriving Show
+
+-- ** Window Settings
+
+-- | Function window settings
+data FuncSettings = FuncSettings {
+    fXMin :: TINumber,
+    fXMax :: TINumber,
+    fXScl :: TINumber,
+    fYMin :: TINumber,
+    fYMax :: TINumber,
+    fYScl :: TINumber
+    }
+    deriving Show
+
+-- | Polar window settings
+data PolarSettings = PolarSettings {
+    polThetaMin :: TINumber,
+    polThetaMax :: TINumber,
+    polThetaStep :: TINumber,
+    polXMin :: TINumber,
+    polXMax :: TINumber,
+    polXScl :: TINumber,
+    polYMin :: TINumber,
+    polYMax :: TINumber,
+    polYScl :: TINumber
+    }
+    deriving Show
+
+-- | Parametric window settings
+data ParamSettings = ParamSettings {
+    parTMin :: TINumber,
+    parTMax :: TINumber,
+    parTStep :: TINumber,
+    parXMin :: TINumber,
+    parXMax :: TINumber,
+    parXScl :: TINumber,
+    parYMin :: TINumber,
+    parYMax :: TINumber,
+    parYScl :: TINumber
+    }
+    deriving Show
+
+-- | Differential equation axes can come
+-- with an index (e.g. Q1-Q9).
+data AxisInd = Axis0
+    | Axis1
+    | Axis2
+    | Axis3
+    | Axis4
+    | Axis5
+    | Axis6
+    | Axis7
+    | Axis8
+    | Axis9
+    deriving (Show,Eq,Enum)
+
+-- | Differential equation axis type
+data DiffEqAxis = AxisT
+    | AxisQ AxisInd
+    | AxisQ' AxisInd
+    deriving Show
+
+-- | Differential equation window settings
+data DiffEqSettings = DiffEqSettings {
+    diffTol :: TINumber,
+    diffTPlot :: TINumber,
+    diffTMin :: TINumber,
+    diffTMax :: TINumber,
+    diffTStep :: TINumber,
+    diffXMin :: TINumber,
+    diffXMax :: TINumber,
+    diffXScl :: TINumber,
+    diffYMin :: TINumber,
+    diffYMax :: TINumber,
+    diffYScl :: TINumber,
+    diffXAxis :: DiffEqAxis,
+    diffYAxis :: DiffEqAxis
+    }
+    deriving Show
+
+-- | Saved window settings, used for ZRCL.
+data SavedWinSettings = SavedWinSettings {
+    zThetaMin :: TINumber,
+    zThetaMax :: TINumber,
+    zThetaStep :: TINumber,
+    ztPlot :: TINumber,
+    ztMin :: TINumber,
+    ztMax :: TINumber,
+    ztStep :: TINumber,
+    zxMin :: TINumber,
+    zxMax :: TINumber,
+    zxScl :: TINumber,
+    zyMin :: TINumber,
+    zyMax :: TINumber,
+    zyScl :: TINumber
+    }
+    deriving Show
+
+data ModeSettings = ModeSettings {
+    modeDrawDot :: Bool,
+    modeSimulG :: Bool,
+    modeGridOn :: Bool,
+    modePolarGC :: Bool,
+    modeCoordOff :: Bool,
+    modeAxesOff :: Bool,
+    modeLabelOn :: Bool
+    }
+    deriving Show
+
+-- | There are four graph modes, each with
+-- its own set of window ranges and equation
+-- types.
+data GraphMode = Func | Polar | Param | DiffEq
+
+-- | Plain functions and Polar functions both use
+-- a single equation.
+type FuncEqn = Text
+
+-- | Parametric functions use a pair of equations
+data ParamEqn = ParamEqn {
+    xEqn :: Text,
+    yEqn :: Text
+    } deriving Show
+
+-- | Differential equations have a single equation
+-- paired with an initial condition.
+data DiffEqEqn = DiffEqEqn {
+    diffEqn :: Text,
+    diffIC :: Double
+    } deriving Show
+
+class HasGDB (a :: GraphMode) where
+    type GDBEqn a :: *
+    type GDBSettings a :: *
+
+    showGDBSettings :: GDB a -> Text
+    showGDBHeader :: GDB a -> Text
+
+    showGDBLib :: GDB a -> Text
+    showGDBLib gdb = intercalate "\n" (map showGDBEntry (gdbLib gdb))
+
+    showGDBEntry :: GDBLibEntry a -> Text
+
+instance HasGDB Func where
+    type GDBEqn Func = FuncEqn
+    type GDBSettings Func = FuncSettings
+
+    showGDBSettings gdb = showFuncSettings (gdbSettings gdb)
+    showGDBHeader _ = "ID\tSelected\tEquation\n"
+    showGDBEntry (GDBLibEntry entryId selected eqn) =
+        showText entryId <> "\t"
+        <> showText selected <> "\t"
+        <> eqn
+
+instance HasGDB Polar where
+    type GDBEqn Polar = FuncEqn
+    type GDBSettings Polar = PolarSettings
+    showGDBSettings gdb = showPolarSettings (gdbSettings gdb)
+    showGDBHeader _ = "ID\tSelected\tEquation\n"
+    showGDBEntry (GDBLibEntry entryId selected eqn) =
+        showText entryId <> "\t"
+        <> showText selected <> "\t"
+        <> eqn
+
+instance HasGDB Param where
+    type GDBEqn Param = ParamEqn
+    type GDBSettings Param = ParamSettings
+    showGDBSettings gdb = showParamSettings (gdbSettings gdb)
+    showGDBHeader _ = "ID\tSelected\tx-Equation\ty-Equation\n"
+    showGDBEntry (GDBLibEntry entryId selected eqn) =
+        showText entryId <> "\t"
+        <> showText selected <> "\t"
+        <> xEqn eqn <> "\t"
+        <> yEqn eqn
+
+instance HasGDB DiffEq where
+    type GDBEqn DiffEq = DiffEqEqn
+    type GDBSettings DiffEq = DiffEqSettings
+    showGDBSettings gdb = showDiffEqSettings (gdbSettings gdb)
+    showGDBHeader _ = "ID\tSelected\tEquation\tInitial Condition\n"
+    showGDBEntry (GDBLibEntry entryId selected eqn) =
+        showText entryId <> "\t"
+        <> showText selected <> "\t"
+        <> diffEqn eqn <> "\t"
+        <> showText (diffIC eqn)
+
+
+-- | A graph database entry, containing a
+-- function ID, whether or not it is currently
+-- selected, and the equations that define the
+-- function.
+data GDBLibEntry (a :: GraphMode) = GDBLibEntry {
+        libId :: Int,
+        libSelected :: Bool,
+        libEqn :: GDBEqn a
+        }
+
+-- | A graph database contains mode settings, window
+-- settings, and a library of functions. The latter two
+-- depend on the graphcs mode.
+data GDB (a :: GraphMode) = GDB {
+        gdbMode :: ModeSettings,
+        gdbSettings :: GDBSettings a,
+        gdbLib :: [GDBLibEntry a]
+        }
+
+instance HasGDB a => Show (GDB (a :: GraphMode)) where
+    show gdb = unpack $ showGDB gdb
+
+
+-- | Variables have a type and type-specific data.
+-- See also `Data.TI85.File.Variable.VarType`.
+data Variable =
+    TIScalar TINumber
+    | TIVector [TINumber]
+    | TIList [TINumber]
+    | TIMatrix [[TINumber]]
+    | TIConstant TINumber
+    | TIEquation Text
+    | TIString Text
+    | TIProgram Program
+    | TIPicture TIBitmap
+    | TIFuncSettings FuncSettings
+    | TIPolarSettings PolarSettings
+    | TIParamSettings ParamSettings
+    | TIDiffEqSettings DiffEqSettings
+    | TIZRCL SavedWinSettings
+    | TIFuncGDB (GDB Func)
+    | TIPolarGDB (GDB Polar)
+    | TIParamGDB (GDB Param)
+    | TIDiffEqGDB (GDB DiffEq)
+    deriving Show
+
+-- * Text Conversion
+
+-- | Utility for converting a showable
+-- to Text.
+showText :: Show a => a -> Text
+showText = pack.show
+
+-- | Convert a TINumber to Text.
+showNumber :: TINumber -> Text
+showNumber (TIReal x) = showText x
+showNumber (TIComplex x y) = showText x <> "+" <> showText y <> "i"
+
+-- | Convert a Program to Text.
+showProgram :: Program -> Text
+showProgram (PlainText progText) = progText
+showProgram (Tokenized tokens) =
+    foldMap (\(Token _ t) -> t) tokens
+
+-- ** Window Settings
+
+-- | Function window settings.
+showFuncSettings :: FuncSettings -> Text
+showFuncSettings settings =
+    "\nxMin: " <> showNumber (fXMin settings)
+    <> "\nxMax: " <> showNumber (fXMax settings)
+    <> "\nxScl: " <> showNumber (fXScl settings)
+    <> "\nyMin: " <> showNumber (fYMin settings)
+    <> "\nyMax: " <> showNumber (fYMax settings)
+    <> "\nyScl: " <> showNumber (fYScl settings)
+
+-- | Polar window settings.
+showPolarSettings :: PolarSettings -> Text
+showPolarSettings settings =
+    "\nθMin: " <> showNumber (polThetaMin settings)
+    <> "\nθMax: " <> showNumber (polThetaMax settings)
+    <> "\nθStep: " <> showNumber (polThetaStep settings)
+    <> "\nxMin: " <> showNumber (polXMin settings)
+    <> "\nxMax: " <> showNumber (polXMax settings)
+    <> "\nxScl: " <> showNumber (polXScl settings)
+    <> "\nyMin: " <> showNumber (polYMin settings)
+    <> "\nyMax: " <> showNumber (polYMax settings)
+    <> "\nyScl: " <> showNumber (polYScl settings)
+
+-- | Parametric window settings.
+showParamSettings :: ParamSettings -> Text
+showParamSettings settings =
+    "\ntMin: " <> showNumber (parTMin settings)
+    <> "\ntMax: " <> showNumber (parTMax settings)
+    <> "\ntStep: " <> showNumber (parTStep settings)
+    <> "\nxMin: " <> showNumber (parXMin settings)
+    <> "\nxMax: " <> showNumber (parXMax settings)
+    <> "\nxScl: " <> showNumber (parXScl settings)
+    <> "\nyMin: " <> showNumber (parYMin settings)
+    <> "\nyMax: " <> showNumber (parYMax settings)
+    <> "\nyScl: " <> showNumber (parYScl settings)
+
+-- | DiffEq window settings.
+showDiffEqSettings :: DiffEqSettings -> Text
+showDiffEqSettings settings =
+    "\ndiffTol: " <> showNumber (diffTol settings)
+    <> "\ntPlot: " <> showNumber (diffTPlot settings)
+    <> "\ntMin: " <> showNumber (diffTMin settings)
+    <> "\ntMax: " <> showNumber (diffTMax settings)
+    <> "\ntStep: " <> showNumber (diffTStep settings)
+    <> "\nxMin: " <> showNumber (diffXMin settings)
+    <> "\nxMax: " <> showNumber (diffXMax settings)
+    <> "\nxScl: " <> showNumber (diffXScl settings)
+    <> "\nyMin: " <> showNumber (diffYMin settings)
+    <> "\nyMax: " <> showNumber (diffYMax settings)
+    <> "\nyScl: " <> showNumber (diffYScl settings)
+    <> "\nxAxis: " <> showAxis (diffXAxis settings)
+    <> "\nyAxis: " <> showAxis (diffYAxis settings)
+  where
+    showAxis :: DiffEqAxis -> Text
+    showAxis AxisT = "t"
+    showAxis (AxisQ ai) =
+        let ind = fromEnum ai
+        in if ind == 0
+            then "Q"
+            else "Q" <> showText ind
+    showAxis (AxisQ' ai) =
+        let ind = fromEnum ai
+        in if ind == 0
+            then "Q'"
+            else "Q'" <> showText ind
+
+-- | Saved window settings.
+showWinSettings :: SavedWinSettings -> Text
+showWinSettings settings =
+    "\nzθMin: " <> showNumber (zThetaMin settings)
+    <> "\nzθMax: " <> showNumber (zThetaMax settings)
+    <> "\nzθStep: " <> showNumber (zThetaStep settings)
+    <> "\nztPlot: " <> showNumber (ztPlot settings)
+    <> "\nztMin: " <> showNumber (ztMin settings)
+    <> "\nztMax: " <> showNumber (ztMax settings)
+    <> "\nztStep: " <> showNumber (ztStep settings)
+    <> "\nzxMin: " <> showNumber (zxMin settings)
+    <> "\nzxMax: " <> showNumber (zxMax settings)
+    <> "\nzxScl: " <> showNumber (zxScl settings)
+    <> "\nzyMin: " <> showNumber (zyMin settings)
+    <> "\nzyMax: " <> showNumber (zyMax settings)
+    <> "\nzyScl: " <> showNumber (zyScl settings)
+
+showGDBMode :: ModeSettings -> Text
+showGDBMode mode =
+    "Dot/Line: " <> if modeDrawDot mode then "Dot" else "Line" <> "\n"
+    <> "Simul/Seq: " <> if modeSimulG mode then "Simul" else "Seq" <> "\n"
+    <> "Grid: " <> showText (modeGridOn mode) <> "\n"
+    <> "Polar: " <> showText (modePolarGC mode) <> "\n"
+    <> "Coord: " <> showText (not $ modeCoordOff mode) <> "\n"
+    <> "Axes: " <> showText (not $ modeAxesOff mode) <> "\n"
+    <> "Label: " <> showText (modeLabelOn mode) <> "\n"
+
+showGDB :: HasGDB (a :: GraphMode) => GDB (a :: GraphMode) -> Text
+showGDB gdb =
+    "Mode Settings:\n"
+    <> showGDBMode (gdbMode gdb) <> "\n"
+    <> "Window Settings:"
+    <> showGDBSettings gdb <> "\n\n"
+    <> "Library:\n"
+    <> showGDBHeader gdb
+    <> showGDBLib gdb
+
+-- | Convert a Variable to Text
+showVariable :: Variable -> Text
+showVariable (TIScalar tn) = showNumber tn
+showVariable (TIVector tns) =
+    let nums = map showNumber tns
+    in "<" <> intercalate "," nums <> ">"
+showVariable (TIList tns) =
+    let nums = map showNumber tns
+    in "[" <> intercalate "," nums <> "]"
+showVariable (TIMatrix tnss) =
+    let rows = [ intercalate "," $ map showNumber row | row <- tnss ]
+    in "<<" <> intercalate "\n  " rows <> ">>"
+showVariable (TIConstant tn) = showNumber tn
+showVariable (TIEquation txt) = txt
+showVariable (TIString txt) = txt
+showVariable (TIProgram pro) = showProgram pro
+showVariable (TIPicture pic) = showAsciiArt pic
+showVariable (TIFuncSettings settings) = showFuncSettings settings
+showVariable (TIPolarSettings settings) = showPolarSettings settings
+showVariable (TIParamSettings settings) = showParamSettings settings
+showVariable (TIDiffEqSettings settings) = showDiffEqSettings settings
+showVariable (TIZRCL settings) = showWinSettings settings
+showVariable (TIFuncGDB gdb) = showGDB gdb
+showVariable (TIPolarGDB gdb) = showGDB gdb
+showVariable (TIParamGDB gdb) = showGDB gdb
+showVariable (TIDiffEqGDB gdb) = showGDB gdb
+
+-- * IO
+
+-- | Print a textual representation of a Variable.
+printVariable :: Variable -> IO ()
+printVariable = putStrLn.showVariable
+
diff --git a/src/Data/TI85/Var/Pic.hs b/src/Data/TI85/Var/Pic.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/TI85/Var/Pic.hs
@@ -0,0 +1,101 @@
+-- | Picture variable types and utilities.
+module Data.TI85.Var.Pic (
+    -- * TI Bitmap
+    TIBitmap,
+    emptyBitmap,
+    fromBytes,
+    toBytes,
+    -- * Display utilities
+    showAsciiArt,
+    writePicPng
+    ) where
+
+import Prelude hiding (take, drop)
+import Data.Text (Text, pack, intercalate, take, drop)
+import Data.Text.Encoding (decodeLatin1)
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as BS
+import Codec.Picture
+import Data.Bits
+import Data.Word
+import Data.Vector.Storable hiding (map,take,drop)
+import Numeric (showHex)
+
+import Data.TI85.Encoding (tiDecode)
+
+-- | A TI Picture variable is encoded as a packed bitmap,
+-- and is always 128x63 binary pixels.
+newtype TIBitmap = TIBitmap ByteString deriving Show
+
+_bmCols = 128
+_bmRows = 63
+_bmSizeBytes = 1008
+
+-- | A blank (all zero) bitmap
+emptyBitmap :: TIBitmap
+emptyBitmap = TIBitmap (BS.replicate _bmSizeBytes 0x0)
+
+-- | Create a bitmap from a packed `ByteString`. If the data
+-- is the wrong size, returns `Nothing`.
+fromBytes :: ByteString -> Maybe TIBitmap
+fromBytes bytes = if BS.length bytes == 1008
+    then Just (TIBitmap bytes)
+    else Nothing
+
+-- | Extract the raw `ByteString` from a bitmap.
+toBytes :: TIBitmap -> ByteString
+toBytes (TIBitmap bytes) = bytes
+
+-- Expands an 8-bit word into an 8-element ByteString
+explodeWord :: Word8 -> ByteString
+explodeWord w =
+    let (bits,_) = BS.unfoldrN 8 (\x -> Just (x .&. 1, shiftR x 1)) w
+    in BS.reverse bits
+
+-- Very approximate calculator-like colors
+screenColorMap :: Word8 -> Pixel8
+screenColorMap 0x0 = 0xc6
+screenColorMap _ = 0x39
+
+-- Convert a `ByteString`, where each bit represents a pixel into
+-- a vector of pixels suitable for use in a `DynamicImage`.
+-- Bits are mapped to pixels using a vaguely calculator-like
+-- color map (light gray background, dark gray foreground).
+bytesToPixels :: ByteString -> Vector Pixel8
+bytesToPixels =  fromList . map screenColorMap . BS.unpack . BS.concatMap explodeWord
+
+-- Create an image out of a packed `ByteString`.
+-- See `bytesToPixels`
+bytesToImage :: ByteString -> Int -> Int -> DynamicImage
+bytesToImage bytes width height =
+    let pixels = bytesToPixels bytes
+    in ImageY8 (Image width height pixels)
+
+-- | Create a text-based representation of a picture.
+-- Background pixels are rendered as spaces, while
+-- foreground are rendered as full block glyphs (█).
+showAsciiArt :: TIBitmap -> Text
+showAsciiArt (TIBitmap bytes) =
+    let bits = BS.concatMap explodeWord bytes
+        --hexChars = pack $ BS.foldr' showHex "" bits
+        --imgChars = chunk _bmCols hexChars []
+        glyphs = tiDecode $ BS.map toGlyph bits
+        imgChars = chunk _bmCols glyphs []
+    in intercalate "\n" imgChars
+  where
+    toGlyph :: Word8 -> Word8
+    toGlyph 0x0 = 0x20
+    toGlyph _ = 0xdf
+
+    chunk :: Int -> Text -> [Text] -> [Text]
+    chunk n "" chunks = chunks
+    chunk n xs chunks = take n xs : chunk n (drop n xs) chunks
+
+-- | Save a bitmap as a PNG image.
+-- Bits are mapped to pixels using a vaguely calculator-like
+-- color map (light gray background, dark gray foreground).
+writePicPng :: FilePath -> TIBitmap -> IO ()
+writePicPng path (TIBitmap bitmap) =
+    let img = bytesToImage bitmap _bmCols _bmRows
+    in savePngImage path img
+
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,37 @@
+{-# LANGUAGE OverloadedStrings #-}
+import Hedgehog
+import qualified Hedgehog.Gen as Gen
+import qualified Hedgehog.Range as Range
+
+import Data.Word
+
+import Data.TI85
+
+invalidID = [0x0b,0x13,0x14,0x16,0x1c]
+
+validTypeID :: Word8 -> Bool
+validTypeID val
+    | val >= 0x00 && val <= 0x1e = val `notElem` invalidID
+    | otherwise = False
+
+-- | Generate a valid variable type ID
+genTypeID :: Gen Word8
+genTypeID = do
+    Gen.filter validTypeID $ Gen.word8 (Range.linear 0x00 0x1e)
+
+
+-- | Check ID mapping syncrhonization
+idMapping :: Property
+idMapping = property $ do
+    idVal <- forAll genTypeID
+    (typeToId.idToType) idVal === idVal
+
+tests :: IO Bool
+tests = checkParallel $ Group "Test.TI85" [
+    ("idMapping", idMapping)
+    ]
+
+main :: IO ()
+main = do
+    tests
+    return ()
